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) # Shared JS for a mock panel that supports querySelector + appendChild, # exercising the real runtime DOM transitions of renderBucketLatency. _MAKE_PANEL = """ function makePanel() { const heading = {localName:'h3', id:'bucket-latency-heading'}; const rows = {innerHTML:'', hidden:false, appendChild(child) { this.innerHTML += (child.outerHTML || String(child)); return child; }, querySelectorAll() { return []; }}; const panel = {hidden:false, querySelector(sel) { if (sel === '#bucket-latency-heading') return heading; if (sel === '#bucket-latency-rows') return rows; return null; }}; return {panel, rows, heading}; } """ _REQUIRE = "const status = require(" + json.dumps(str(STATUS)) + ");" def test_live_data_status_summarizes_each_feed_without_claiming_live(): script = _REQUIRE + """ 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, "latencyMs": None, } 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 = _REQUIRE + """ 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 = _REQUIRE + """ 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 = _REQUIRE + """ 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_describe_exposes_bounded_bucket_latency(): script = _REQUIRE + """ const measured = {fresh_for_seconds:8, latency_ms:{context:120, notifications:45}, sections:{ context:{age_seconds:1}, notifications:{age_seconds:14, stale:true, retry_in_seconds:30}, events:{age_seconds:3} }}; const unbounded = {fresh_for_seconds:8, latency_ms:{context:-5, events:9000000}, sections:{ context:{age_seconds:1}, events:{age_seconds:2}, notifications:{age_seconds:3} }}; const missing = {fresh_for_seconds:8, sections:{ context:{age_seconds:1}, events:{age_seconds:2}, notifications:{age_seconds:3} }}; process.stdout.write(JSON.stringify({ measured:status.describe(measured), unbounded:status.describe(unbounded), missing:status.describe(missing), })); """ result = run_node(script) assert result["measured"]["summary"] == "Updates delayed" assert [(feed["key"], feed["latencyMs"]) for feed in result["measured"]["feeds"]] == [ ("context", 120), ("notifications", 45), ("events", None), ] # Strict telemetry: negatives and huge values are rejected, never clipped. assert [(feed["key"], feed["latencyMs"]) for feed in result["unbounded"]["feeds"]] == [ ("context", None), ("notifications", None), ("events", None), ] assert all(feed["latencyMs"] is None for feed in result["missing"]["feeds"]) def test_live_data_status_renders_bucket_latency_panel_rows(): script = _REQUIRE + _MAKE_PANEL + """ const description = status.describe({fresh_for_seconds:8, latency_ms:{context:120, notifications:45, events:3000}, sections:{ context:{age_seconds:1}, notifications:{age_seconds:14, stale:true}, events:{age_seconds:3} }}); const {panel, rows, heading} = makePanel(); const rowsReturned = status.renderBucketLatency(description, panel); process.stdout.write(JSON.stringify({rows:rowsReturned, panelHidden:panel.hidden, html:rows.innerHTML, headingId:heading.id})); """ result = run_node(script) assert [row["label"] for row in result["rows"]] == ["Work", "Updates", "Activity"] assert result["rows"][0] == {"key": "context", "label": "Work", "latencyMs": 120, "stale": False} assert result["rows"][2]["latencyMs"] == 3000 assert result["panelHidden"] is False assert result["headingId"] == 'bucket-latency-heading' assert 'data-bucket="context"' in result["html"] assert 'role="listitem"' in result["html"] assert result["html"].count('role="listitem"') == 3 assert "120 ms" in result["html"] assert "3.0 s" in result["html"] # A stale feed's number is a last-known measurement and must say so. assert 'data-bucket="notifications"' in result["html"] assert "last known" in result["html"] def test_live_data_status_marks_unmeasured_buckets_honestly(): script = _REQUIRE + _MAKE_PANEL + """ const description = status.describe({fresh_for_seconds:8, latency_ms:{context:120}, sections:{ context:{age_seconds:1}, notifications:{age_seconds:2}, events:{age_seconds:3} }}); const {panel, rows, heading} = makePanel(); const rowsReturned = status.renderBucketLatency(description, panel); process.stdout.write(JSON.stringify({panelHidden:panel.hidden, html:rows.innerHTML})); """ result = run_node(script) assert result["panelHidden"] is False assert 'data-bucket="events"' in result["html"] assert "not measured" in result["html"] assert "—" not in result["html"] assert 'role="listitem"' in result["html"] def test_live_data_status_hides_latency_panel_without_any_measurement(): script = _REQUIRE + _MAKE_PANEL + """ const description = status.describe({fresh_for_seconds:8, sections:{ context:{age_seconds:1}, notifications:{age_seconds:2}, events:{age_seconds:3} }}); const {panel, rows, heading} = makePanel(); const rowsReturned = status.renderBucketLatency(description, panel); process.stdout.write(JSON.stringify({panelHidden:panel.hidden, html:rows.innerHTML, headingIntact:heading.id === 'bucket-latency-heading'})); """ result = run_node(script) assert result["panelHidden"] is True assert result["html"] == "" # Hiding must never damage the preserved heading / aria-labelledby target. assert result["headingIntact"] is True 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 'id="bucket-latency-panel"' 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 ".bucket-latency-panel" in css assert ".bucket-latency-row" in css assert ".bucket-latency-value.slow" in css # Latency rows must stay readable on narrow screens and to screen readers: # the panel is labelled, rows are announced as a list, values wrap. assert 'aria-labelledby="bucket-latency-heading"' in html assert 'id="bucket-latency-rows" role="list"' in html assert "bucket-latency-row { display:flex" in css assert ".bucket-latency-row strong, .bucket-latency-value { overflow-wrap:anywhere" in css # Rows must carry role=listitem so the list semantics hold at runtime. assert 'role="listitem"' in status_source assert "renderBucketLatency" in status_source assert "liveDataStatus.describe" in dashboard assert "liveDataStatus.createRefreshController" in dashboard assert "liveDataStatus.mount" in dashboard assert "renderBucketLatency(" in dashboard assert "#bucket-latency-panel" 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 def test_live_data_status_latency_lifecycle_no_data_measured_stale(): """Real DOM-style lifecycle across three states through the same panel: no measurement -> hide (heading intact); measured -> rows with role=listitem; stale-but-measured -> rows with 'last known'. The heading and aria-labelledby target survive every transition and rows always use role=listitem.""" script = _REQUIRE + _MAKE_PANEL + """ const noData = status.describe({fresh_for_seconds:8, sections:{ context:{age_seconds:1}, notifications:{age_seconds:2}, events:{age_seconds:3} }}); const measured = status.describe({fresh_for_seconds:8, latency_ms:{context:120, notifications:45, events:0}, sections:{ context:{age_seconds:1}, notifications:{age_seconds:14, stale:true}, events:{age_seconds:3} }}); const p1 = makePanel(); const p2 = makePanel(); const p3 = makePanel(); status.renderBucketLatency(noData, p1.panel); status.renderBucketLatency(measured, p2.panel); // stale-but-measured: reuse the same measured description (notifications stale). status.renderBucketLatency(measured, p3.panel); process.stdout.write(JSON.stringify({ noData: {hidden:p1.panel.hidden, rowsHtml:p1.rows.innerHTML, headingId:p1.heading.id}, measured: {hidden:p2.panel.hidden, rowsHtml:p2.rows.innerHTML, listitemCount:(p2.rows.innerHTML.match(/role="listitem"/g)||[]).length}, staleMeasured: {hidden:p3.panel.hidden, hasLastKnown:p3.rows.innerHTML.includes('last known'), headingId:p3.heading.id}, })); """ result = run_node(script) # No data: hidden, empty, heading preserved. assert result["noData"]["hidden"] is True assert result["noData"]["rowsHtml"] == "" assert result["noData"]["headingId"] == "bucket-latency-heading" # Measured: shown with 3 listitem rows, heading preserved. assert result["measured"]["hidden"] is False assert result["measured"]["listitemCount"] == 3 # Stale-but-measured: still shown with 'last known', heading preserved. assert result["staleMeasured"]["hidden"] is False assert result["staleMeasured"]["hasLastKnown"] is True assert result["staleMeasured"]["headingId"] == "bucket-latency-heading" def test_live_data_status_strict_latency_telemetry_rejects_malformed_values(): """Client telemetry accepts only finite nonnegative bounded integers. Rejects null, bool, strings, arrays, floats, negatives, Infinity, and huge values without Number() coercion or clipping.""" script = _REQUIRE + """ function latencyOf(value) { const desc = status.describe({fresh_for_seconds:8, latency_ms:{context:value}, sections:{context:{age_seconds:1}}}); return desc.feeds[0].latencyMs; } process.stdout.write(JSON.stringify({ null: latencyOf(null), boolTrue: latencyOf(true), boolFalse: latencyOf(false), stringNum: latencyOf("120"), stringWord: latencyOf("fast"), array: latencyOf([120]), float: latencyOf(120.9), negative: latencyOf(-5), infinity: latencyOf(Infinity), huge: latencyOf(99999999), zero: latencyOf(0), valid: latencyOf(450), })); """ result = run_node(script) assert result == { "null": None, "boolTrue": None, "boolFalse": None, "stringNum": None, "stringWord": None, "array": None, "float": None, "negative": None, "infinity": None, "huge": None, "zero": 0, "valid": 450, } def test_live_data_status_renders_submillisecond_honestly(): """Sub-millisecond latency (0 ms after rounding) renders as '<1 ms', never a fabricated exact '1 ms' floor and never '0 ms'.""" script = _REQUIRE + _MAKE_PANEL + """ const description = status.describe({fresh_for_seconds:8, latency_ms:{context:0, notifications:0, events:0}, sections:{ context:{age_seconds:1}, notifications:{age_seconds:2}, events:{age_seconds:3} }}); const {panel, rows} = makePanel(); status.renderBucketLatency(description, panel); process.stdout.write(JSON.stringify({html:rows.innerHTML})); """ result = run_node(script) assert "<1 ms" in result["html"] assert "1 ms" not in result["html"].replace("<1 ms", "") assert "0 ms" not in result["html"] assert 'role="listitem"' in result["html"]