stackchain-dashboard/tests/test_detail_watch.py
timmy eea9097740
All checks were successful
CI / lint (pull_request) Successful in 3m25s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 5m13s
CI / release-candidate (pull_request) Has been skipped
test: cover detail watch in packaged mobile CI
2026-08-23 15:05:19 +00:00

187 lines
7.7 KiB
Python

import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).parents[1]
MODULE = ROOT / "frontend" / "search-preview.js"
def run(script: str) -> dict:
harness = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state = {{ requests:[], renders:[], refreshes:0 }};
const feature = createDetailWatch({{
fetchJson: async (path, options={{}}) => {{
state.requests.push({{path,method:options.method || 'GET'}});
return {{watching:false,following_synced:true}};
}},
refreshFollowing: async () => {{ state.refreshes += 1; }},
onState: (status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{ {script} }})().catch(error => {{ console.error(error); process.exit(1); }});
"""
completed = subprocess.run(["node", "-e", harness], text=True, capture_output=True, check=True)
return json.loads(completed.stdout)
def test_detail_watch_loads_authoritative_typed_subscription_state():
result = run("""
await feature.open({repository:'stackchain/api',kind:'pull',number:84,state:'open'});
process.stdout.write(JSON.stringify(state));
""")
assert result["requests"] == [{
"path": "api/v1/repos/stackchain/api/issues/84/preview/subscription?kind=pull",
"method": "GET",
}]
assert result["renders"] == [
{"status": "loading", "watching": False},
{"status": "ready", "watching": False},
]
assert result["refreshes"] == 0
def test_detail_watch_mutation_is_single_flight_and_refreshes_following_after_confirmation():
script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{requests:[],renders:[],refreshes:0}};
let finishMutation;
const feature=createDetailWatch({{
fetchJson:async (path,options={{}}) => {{
state.requests.push({{path,method:options.method || 'GET'}});
if (!options.method) return {{watching:false}};
return new Promise(resolve => {{finishMutation=() => resolve({{watching:true,following_synced:true,following_count:3}});}});
}},
refreshFollowing:async () => {{state.refreshes += 1;}},
onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
const item={{repository:'stackchain/api',kind:'issue',number:42,state:'open'}};
await feature.open(item);
const first=feature.toggle();
const second=feature.toggle();
await new Promise(resolve => setImmediate(resolve));
state.samePromise=first === second;
finishMutation();
await Promise.all([first,second]);
process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["samePromise"] is True
assert [request["method"] for request in result["requests"]] == ["GET", "PUT"]
assert result["refreshes"] == 1
assert result["renders"][-2]["status"] == "watching"
assert result["renders"][-1]["status"] == "watched"
assert result["renders"][-1]["watching"] is True
def test_detail_watch_partial_sync_never_claims_success_or_refreshes_following():
script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{renders:[],refreshes:0}};
const feature=createDetailWatch({{
fetchJson:async (_path,options={{}}) => options.method
? {{watching:true,following_synced:false,error:'Following could not sync.'}}
: {{watching:false}},
refreshFollowing:async () => {{state.refreshes += 1;}},
onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
await feature.open({{repository:'stackchain/api',kind:'issue',number:42,state:'open'}});
try {{ await feature.toggle(); }} catch (error) {{ state.rejected=error.message; }}
process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["rejected"] == "Following could not sync."
assert result["refreshes"] == 0
assert result["renders"][-1]["status"] == "error"
assert result["renders"][-1]["watching"] is False
assert all(state["status"] != "watched" for state in result["renders"])
def test_detail_watch_is_wired_into_both_mobile_work_details_and_bundle():
html = (ROOT / "frontend" / "index.html").read_text()
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
css = (ROOT / "frontend" / "dashboard.css").read_text()
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
workflow = (ROOT / ".gitea" / "workflows" / "ci.yml").read_text()
search_preview = (ROOT / "frontend" / "search-preview.js").read_text()
assert '<button id="watch-issue-detail" type="button">Watch issue</button>' in html
assert '<button id="watch-pull-detail" type="button">Watch pull request</button>' in html
assert 'id="issue-watch-status"' in html
assert 'id="pull-watch-status"' in html
assert 'static/search-preview.js' in bundle
assert 'root.createDetailWatch' in search_preview
assert 'function bindDetailWatch(' in dashboard
assert 'issueDetailWatch.open(item)' in dashboard
assert 'pullDetailWatch.open(item)' in dashboard
assert '#watch-issue-detail' in css
assert '#watch-pull-detail' in css
assert 'tests/e2e/test_mobile_detail_watch_release.py' in workflow
def test_detail_watch_ignores_a_late_subscription_response_for_the_previous_item():
script = f"""
require({json.dumps(str(MODULE))});
const createDetailWatch = globalThis.createDetailWatch;
const state={{renders:[]}};
const completions={{}};
const feature=createDetailWatch({{
fetchJson:path => new Promise(resolve => {{completions[path]=resolve;}}),
onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
const first=feature.open({{repository:'stackchain/api',kind:'issue',number:1,state:'open'}});
const second=feature.open({{repository:'stackchain/api',kind:'issue',number:2,state:'open'}});
completions[Object.keys(completions)[1]]({{watching:true}});
await second;
completions[Object.keys(completions)[0]]({{watching:false}});
await first;
process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
ready = [state for state in result["renders"] if state["status"] == "ready"]
assert len(ready) == 1
assert ready[0]["watching"] is True
def test_detail_watch_reports_subscription_load_failure_as_recoverable_error():
script = f"""
require({json.dumps(str(MODULE))});
const state={{renders:[]}};
const feature=globalThis.createDetailWatch({{
fetchJson:async () => {{throw new Error('temporarily unavailable');}},
onState:(status,watching,error) => state.renders.push({{status,watching,error:error?.message}}),
}});
(async () => {{
try {{ await feature.open({{repository:'stackchain/api',kind:'issue',number:1,state:'open'}}); }}
catch (error) {{ state.rejected=error.message; }}
process.stdout.write(JSON.stringify(state));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["rejected"] == "temporarily unavailable"
assert result["renders"][-1] == {
"status": "error", "watching": False, "error": "temporarily unavailable"
}