import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] STATUS = ROOT / "frontend" / "live-data-status.js" HTML = ROOT / "frontend" / "index.html" CSS = ROOT / "frontend" / "dashboard.css" DASHBOARD = ROOT / "frontend" / "dashboard.js" def run_node(script: str) -> dict: result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) return json.loads(result.stdout) def test_live_data_status_summarizes_each_feed_without_claiming_live(): script = f""" const status = require({json.dumps(str(STATUS))}); const healthy = {{fresh_for_seconds:8, sections:{{ context:{{age_seconds:1}}, notifications:{{age_seconds:2}}, events:{{age_seconds:3}} }}}}; const oneDelayed = {{fresh_for_seconds:8, retry_in_seconds:30, sections:{{ context:{{age_seconds:1}}, notifications:{{age_seconds:14, stale:true, retry_in_seconds:30}}, events:{{age_seconds:3}} }}}}; const twoDelayed = {{fresh_for_seconds:8, sections:{{ context:{{age_seconds:12, degraded:true}}, notifications:{{age_seconds:14, stale:true}}, events:{{age_seconds:3}} }}}}; process.stdout.write(JSON.stringify({{ healthy:status.describe(healthy), one:status.describe(oneDelayed), two:status.describe(twoDelayed), unavailable:status.describe({{}}), }})); """ result = run_node(script) assert result["healthy"]["summary"] == "Live" assert [feed["state"] for feed in result["healthy"]["feeds"]] == ["live"] * 3 assert result["one"]["summary"] == "Updates delayed" assert result["one"]["nextRetrySeconds"] == 30 assert result["one"]["feeds"][1] == { "key": "notifications", "label": "Updates", "state": "delayed", "ageSeconds": 14 } assert result["two"]["summary"] == "2 data feeds delayed" assert result["unavailable"]["summary"] == "Live data unavailable" def test_live_data_status_controller_is_single_flight_and_reports_result(): script = f""" const status = require({json.dumps(str(STATUS))}); let resolveRefresh; let calls = 0; const states = []; const button = {{disabled:false}}; const output = {{textContent:''}}; const controller = status.createRefreshController({{ button, output, refresh:() => {{ calls += 1; return new Promise(resolve => {{ resolveRefresh = resolve; }}); }}, onState:value => states.push(value), }}); (async () => {{ const first = controller.run(); const second = controller.run(); const pending = {{calls, disabled:button.disabled, text:output.textContent}}; resolveRefresh({{context:{{}}}}); await Promise.all([first, second]); process.stdout.write(JSON.stringify({{pending, calls, disabled:button.disabled, text:output.textContent, states}})); }})(); """ assert run_node(script) == { "pending": {"calls": 1, "disabled": True, "text": "Refreshing live data…"}, "calls": 1, "disabled": False, "text": "Live data refreshed.", "states": ["refreshing", "success"], } def test_live_data_status_sheet_pauses_today_contains_focus_and_closes_through_back(): script = f""" const status = require({json.dumps(str(STATUS))}); function element(name) {{ return {{name, hidden:false, inert:false, listeners:{{}}, focused:0, addEventListener(type, fn) {{ this.listeners[type] = fn; }}, focus() {{ this.focused += 1; }}, }}; }} const trigger = element('trigger'); trigger.attrs = {{}}; trigger.setAttribute = (name, value) => trigger.attrs[name] = value; const close = element('close'); const refresh = element('refresh'); const back = element('return'); const sheet = element('sheet'); sheet.hidden = true; sheet.querySelectorAll = () => [close, refresh, back]; const header = element('header'); const main = element('main'); const listeners = {{}}; const history = {{state:null, pushes:0, backs:0, pushState(state) {{ this.state=state; this.pushes += 1; }}, back() {{ this.backs += 1; }}, }}; const timerView = {{begins:[], finishes:0, beginDetour(reason) {{ this.begins.push(reason); return {{identity:'issue:r:42', reason}}; }}, finishDetour() {{ this.finishes += 1; return {{resumed:true}}; }}, }}; const paused = element('paused'); paused.hidden = true; const controller = status.createSheetController({{ sheet, trigger, closeButton:close, returnButton:back, pausedStatus:paused, timerView, history, historyTarget:{{addEventListener:(name, fn) => listeners[name]=fn}}, escapeTarget:{{addEventListener:(name, fn) => listeners[name]=fn}}, backgroundElements:[header, main], }}); controller.start(); controller.open(); const opened = {{hidden:sheet.hidden, expanded:trigger.attrs['aria-expanded'], paused:paused.hidden, inert:[header.inert, main.inert], begins:timerView.begins, pushes:history.pushes, closeFocused:close.focused}}; let prevented = 0; listeners.keydown({{key:'Tab', target:back, shiftKey:false, preventDefault:()=>prevented++}}); const trapped = {{prevented, closeFocused:close.focused}}; controller.close(); const requested = {{backs:history.backs, finishes:timerView.finishes}}; history.state = null; listeners.popstate({{state:null}}); process.stdout.write(JSON.stringify({{opened, trapped, requested, closed:{{hidden:sheet.hidden, paused:paused.hidden, expanded:trigger.attrs['aria-expanded'], inert:[header.inert, main.inert], finishes:timerView.finishes, triggerFocused:trigger.focused}}}})); """ result = run_node(script) assert result == { "opened": { "hidden": False, "expanded": "true", "paused": False, "inert": [True, True], "begins": ["live-data-status"], "pushes": 1, "closeFocused": 1, }, "trapped": {"prevented": 1, "closeFocused": 2}, "requested": {"backs": 1, "finishes": 0}, "closed": { "hidden": True, "paused": True, "expanded": "false", "inert": [False, False], "finishes": 1, "triggerFocused": 1, }, } def test_live_data_status_without_running_today_hides_return_and_does_not_resume(): script = f""" const status = require({json.dumps(str(STATUS))}); function element(hidden=false) {{ return {{hidden, inert:false, listeners:{{}}, attrs:{{}}, addEventListener(name, fn) {{ this.listeners[name]=fn; }}, focus() {{}}, setAttribute(name, value) {{ this.attrs[name]=value; }}, querySelectorAll() {{ return []; }} }}; }} const sheet=element(true), trigger=element(), close=element(), returnButton=element(); let finishes=0; const controller=status.createSheetController({{ sheet,trigger,closeButton:close,returnButton, timerView:{{beginDetour:()=>null,finishDetour:()=>finishes++}}, }}); controller.start(); controller.open(); const opened={{returnHidden:returnButton.hidden, expanded:trigger.attrs['aria-expanded']}}; controller.close(); process.stdout.write(JSON.stringify({{opened, finishes}})); """ assert run_node(script) == { "opened": {"returnHidden": True, "expanded": "true"}, "finishes": 0, } def test_live_data_status_has_accessible_mobile_safe_sheet_contract(): html = HTML.read_text() css = CSS.read_text() dashboard = DASHBOARD.read_text() status_source = STATUS.read_text() assert 'id="open-live-data-status"' in html assert 'aria-controls="live-data-status-sheet"' in html assert 'id="live-data-status-sheet"' in html assert 'aria-labelledby="live-data-status-heading"' in html assert 'id="live-data-status-feeds"' in html assert 'id="refresh-live-data"' in html assert 'id="return-from-live-data-status"' in html assert 'id="live-data-status-today-paused"' in html assert '' in html assert ".live-data-status-panel" in css assert "width:min(560px,100%)" in css assert "min-height:44px" in css assert "liveDataStatus.describe" in dashboard assert "liveDataStatus.createRefreshController" in dashboard assert "liveDataStatus.mount" in dashboard assert "createSheetController" in status_source assert "backgroundElements:[qs('header'), qs('main'), qs('#mobile-task-dock')]" in status_source assert "contextPoller.getState()" in dashboard