feat: act on Search and Following comments (Closes #1388)
CI / lint (pull_request) Successful in 3m36s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 6m19s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-25 09:11:14 +00:00
1 parent 0682bbcc2c
commit 7662cca9e6
9 files changed
+202 -46

No files matched your search

@@ -75,3 +75,41 @@ def test_comment_reactions_are_touch_safe_and_focus_preserving_at_320px():
["api/v1/repos/stackchain/api/issues/7/comments/91/reactions/heart", "PUT"],
]
browser.close()
def test_search_following_conversation_actions_are_touch_safe_at_320px():
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 320, "height": 568})
page.set_content(
'<main><section id="search-preview-conversation" class="search-preview-conversation">'
'<div id="search-preview-comments"></div>'
'<div id="search-preview-conversation-status"></div>'
'<button id="retry-search-preview-conversation" hidden></button>'
'<button id="load-older-search-preview-comments" hidden></button>'
'</section></main>'
)
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "search-preview.js")
page.add_script_tag(path=FRONTEND / "comment-actions.js")
page.evaluate(
"""
const actions=createCommentActions({fetchJson:async()=>({}),getLogin:()=> 'timmy'});
renderSearchPreviewConversation({status:'ready',comments:[
{id:41,author:'timmy',body:'Correct this from Following'},
{id:42,author:'alexander',body:'React without leaving Search'},
]},document,value=>String(value),value=>String(value),value=>String(value),actions);
"""
)
expect(page.locator(".search-preview-comment.issue-comment")).to_have_count(2)
expect(page.get_by_role("button", name="Edit")).to_have_count(1)
expect(page.get_by_role("button", name="Delete")).to_have_count(1)
expect(page.get_by_role("button", name="React to this comment")).to_have_count(2)
for control in page.locator("[data-comment-action], [data-comment-reactions-open]").all():
box = control.bounding_box()
assert box is not None and box["height"] >= 44
assert page.evaluate(
"document.documentElement.scrollWidth > document.documentElement.clientWidth"
) is False
browser.close()
@@ -133,3 +133,75 @@ def test_comment_action_retry_is_a_phone_sized_inline_control():
rule = css.split(".conversation-actions-retry", 1)[1].split("}", 1)[0]
assert "min-height:44px" in rule
assert "max-width:100%" in rule
def test_search_and_following_preview_hydrates_owned_comment_actions_lazily():
preview = FRONTEND / "search-preview.js"
script = f"""
require({json.dumps(str(preview))});
const nodes = {{
'#search-preview-comments': {{innerHTML:'',textContent:''}},
'#search-preview-conversation-status': {{textContent:''}},
'#retry-search-preview-conversation': {{hidden:true}},
'#load-older-search-preview-comments': {{hidden:true,disabled:false}},
}};
const document = {{querySelector: selector => nodes[selector]}};
const actions = {{actionHtml: comment => comment.author === 'timmy'
? '<button data-comment-action="edit">Edit</button><button data-comment-reactions-open>React</button>'
: '<button data-comment-reactions-open>React</button>'}};
renderSearchPreviewConversation({{
status:'ready', reviewedAt:'2026-08-25T08:00:00Z',
comments:[
{{id:41,author:'timmy',body:'Mine',created_at:'2026-08-25T09:00:00Z'}},
{{id:42,author:'alexander',body:'Theirs',created_at:'2026-08-25T07:00:00Z'}},
],
}}, document, String, String, String, actions);
process.stdout.write(JSON.stringify({{html:nodes['#search-preview-comments'].innerHTML}}));
"""
output = run_node(script)["html"]
assert output.count('class="search-preview-comment issue-comment') == 2
assert output.count("data-comment-reactions-open") == 2
assert output.count('data-comment-action="edit"') == 1
assert 'data-comment-id="41"' in output
assert "new-since-review" in output
html = (FRONTEND / "index.html").read_text()
javascript = (FRONTEND / "dashboard.js").read_text()
assert 'id="retry-search-preview-comment-actions"' in html
assert "showSearchConversationWithActions" in javascript
assert "context:{kind:searchPreviewDetail.kind,item:searchPreviewDetail}" in javascript
def test_search_preview_comment_pager_updates_exact_visible_comment_in_place():
preview = FRONTEND / "search-preview.js"
script = f"""
const createSearchPreview = require({json.dumps(str(preview))});
(async () => {{
const states=[];
const controller=createSearchPreview({{
fetchJson:async item=>({{...item,title:'Visible'}}),
fetchConversation:async()=>({{comments:[
{{id:41,author:'timmy',body:'Old'}},{{id:42,author:'alexander',body:'Keep'}},
],older_page:null}}),
mutate:async()=>({{}}), onState:state=>states.push(state),
}});
await controller.open({{repository:'stackchain/api',number:9,kind:'pull'}});
await new Promise(resolve=>setTimeout(resolve,0));
const pager=controller.commentPager();
pager.replace({{id:41,author:'timmy',body:'Corrected'}});
const replaced=pager.snapshot().comments;
pager.remove(41);
process.stdout.write(JSON.stringify({{
replaced:replaced.map(comment=>[comment.id,comment.body]),
remaining:pager.snapshot().comments.map(comment=>comment.id),
published:states.at(-1).conversation.comments.map(comment=>comment.id),
}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
assert run_node(script) == {
"replaced": [[41, "Corrected"], [42, "Keep"]],
"remaining": [42],
"published": [42],
}
+1 -1
View File
@@ -221,7 +221,7 @@ def test_markdown_renderer_allows_only_safe_links_and_keeps_html_inert():
def test_all_read_only_work_bodies_use_the_shared_markdown_renderer():
dashboard = (FRONTEND / "dashboard.js").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text() + (FRONTEND / "conversation.js").read_text()
expected_paths = (
"renderMarkdown(comment.body || 'No comment body provided.')",
+2 -1
View File
@@ -82,6 +82,7 @@ async def test_dashboard_mounts_mobile_new_activity_positioning_flow():
assert '<script src="static/update-read-position.js"></script>' in html
assert 'id="jump-update-new-activity"' in html
assert "updateReadPosition.open(String(item.notification_id))" in html
assert "updateReadPosition.ready(String(selectedUpdate?.notification_id || ''), newest)" in html
source = html + (POSITIONER.parent / "conversation.js").read_text()
assert "updateReadPosition.ready(String(getSelectedUpdate()?.notification_id || ''),newest)" in source
assert ".update-new-activity" in html
assert "BASE + 'static/update-read-position.js'" in (POSITIONER.parent / "service-worker.js").read_text()