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_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