import json import subprocess from pathlib import Path FRONTEND = Path(__file__).parents[1] / "frontend" HYDRATOR = FRONTEND / "conversation-action-hydrator.js" def run_node(script: str): result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(result.stdout) def test_conversation_paints_before_optional_actions_load_and_hydrates_once(): script = f""" const createConversationActionHydrator = require({json.dumps(str(HYDRATOR))}); const events = []; let finishLoad; const hydrator = createConversationActionHydrator({{ load: () => {{ events.push('load'); return new Promise(resolve => {{ finishLoad = resolve; }}); }}, activate: () => ({{name:'actions'}}), }}); const root = {{}}; const retry = {{hidden:false}}; const status = {{textContent:'2 messages loaded.'}}; const state = {{comments:[1,2]}}; const pending = hydrator.show({{ root, state, retry, status, paint: (_state, actions) => events.push(actions ? 'paint-actions' : 'paint-core'), wire: () => events.push('wire'), }}); events.push('returned'); finishLoad(); pending.then(() => {{ hydrator.show({{ root, state, retry, status, paint: (_state, actions) => events.push(actions ? 'paint-actions-again' : 'paint-core-again'), wire: () => events.push('wire-again'), }}).then(() => process.stdout.write(JSON.stringify({{events,retry,status:status.textContent}}))); }}); """ output = run_node(script) assert output == { "events": [ "paint-core", "load", "returned", "wire", "paint-actions", "paint-actions-again", ], "retry": {"hidden": True}, "status": "2 messages loaded.", } def test_failed_comment_actions_keep_conversation_usable_and_retry_in_place(): script = f""" const createConversationActionHydrator = require({json.dumps(str(HYDRATOR))}); const events = []; let attempt = 0; const hydrator = createConversationActionHydrator({{ load: async () => {{ attempt += 1; if (attempt === 1) throw new Error('chunk unavailable'); }}, activate: () => ({{name:'actions'}}), }}); const root = {{}}; const retry = {{hidden:true}}; const status = {{textContent:'2 messages loaded.'}}; const options = {{ root, retry, status, state:{{comments:[1,2]}}, paint: (_state, actions) => events.push(actions ? 'actions' : 'core'), wire: () => events.push('wire'), }}; (async () => {{ const failed = await hydrator.show(options); events.push('reply-still-usable'); const recovered = await hydrator.show(options); process.stdout.write(JSON.stringify({{ failed, recovered, attempt, events, retry, status:status.textContent, }})); }})(); """ output = run_node(script) assert output == { "failed": False, "recovered": True, "attempt": 2, "events": ["core", "reply-still-usable", "core", "wire", "actions"], "retry": {"hidden": True}, "status": "2 messages loaded.", } def test_other_focused_surfaces_can_request_the_same_lazy_action_controller(): script = f""" const createConversationActionHydrator = require({json.dumps(str(HYDRATOR))}); let loads=0; const controller={{name:'shared-actions'}}; const hydrator=createConversationActionHydrator({{ load:async()=>{{loads++;}}, activate:()=>controller, }}); (async()=>{{ const [first,second]=await Promise.all([hydrator.get(),hydrator.get()]); process.stdout.write(JSON.stringify({{loads,same:first===second,name:first.name}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == {"loads": 1, "same": True, "name": "shared-actions"} def test_issue_pull_and_update_conversations_trigger_optional_actions_not_startup(): html = (FRONTEND / "index.html").read_text() javascript = (FRONTEND / "dashboard.js").read_text() assert '' in html assert html.index("static/conversation-action-hydrator.js") < html.index("static/dashboard.js") assert "await commentActionFeatures.run('comment-actions'" not in javascript assert "load: () => commentActionFeatures.load('comment-actions')" in javascript assert "createConversationActionHydrator" in javascript for kind in ("issue", "pull", "update"): assert f"retry-{kind}-comment-actions" in html assert f"showConversationWithActions('{kind}'" in javascript assert f"qs('#retry-{kind}-comment-actions').addEventListener" in javascript def test_comment_action_retry_is_a_phone_sized_inline_control(): css = (FRONTEND / "dashboard.css").read_text() assert ".conversation-actions-retry" in css 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' ? '' : ''}}; 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], }