185 lines
7.5 KiB
Python
185 lines
7.5 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()
|
|
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
|
|
|
|
|
|
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"
|
|
}
|