stackchain-dashboard/tests/test_mobile_start_day.py
timmy c5d31c1972
All checks were successful
CI / lint (pull_request) Successful in 2m4s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 57s
CI / release-candidate (pull_request) Has been skipped
feat: send mobile conversation photo bundles (Closes #943)
2026-08-16 06:06:07 +00:00

287 lines
10 KiB
Python

import json
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
START_DAY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-start-day.js"
def run_node(script: str) -> dict:
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_start_day_briefing_guides_urgent_review_before_today():
script = f"""
const createStartDay = require({json.dumps(str(START_DAY))});
let counts = {{agenda:2, attention:3, update:4, filed:1, today:5}};
const opened = [];
const controller = createStartDay({{
getCounts: () => counts,
openQueue: name => opened.push(name),
}});
const first = controller.briefing();
controller.startNext();
counts = {{agenda:0, attention:3, update:4, filed:1, today:5}};
const second = controller.briefing();
controller.startNext();
counts = {{agenda:0, attention:0, update:0, filed:0, today:5}};
const ready = controller.briefing();
controller.startNext();
process.stdout.write(JSON.stringify({{first, second, ready, opened}}));
"""
result = run_node(script)
assert result == {
"first": {
"total": 10,
"next": "agenda",
"label": "Review Agenda",
"summary": "10 items before Today · 5 planned",
"phases": [
{"name": "agenda", "label": "Agenda", "count": 2},
{"name": "attention", "label": "Attention", "count": 3},
{"name": "update", "label": "Updates", "count": 4},
{"name": "filed", "label": "Filed", "count": 1},
],
},
"second": {
"total": 8,
"next": "attention",
"label": "Review Attention",
"summary": "8 items before Today · 5 planned",
"phases": [
{"name": "attention", "label": "Attention", "count": 3},
{"name": "update", "label": "Updates", "count": 4},
{"name": "filed", "label": "Filed", "count": 1},
],
},
"ready": {
"total": 0,
"next": "today",
"label": "Continue Today",
"summary": "Review clear · 5 planned",
"phases": [],
},
"opened": ["agenda", "attention", "today"],
}
def test_start_day_view_renders_refreshed_phases_and_launches_primary_action():
script = f"""
const createStartDay = require({json.dumps(str(START_DAY))});
const elements = {{
summary: {{textContent:''}},
phases: {{textContent:''}},
action: {{textContent:'', listeners:{{}}, addEventListener(name, fn) {{ this.listeners[name] = fn; }}}},
}};
const opened = [];
let counts = {{agenda:0, attention:2, update:1, filed:0, today:3}};
const controller = createStartDay({{
getCounts: () => counts,
openQueue: name => opened.push(name),
elements,
}});
controller.start();
const first = {{summary:elements.summary.textContent, phases:elements.phases.textContent, action:elements.action.textContent}};
elements.action.listeners.click();
counts = {{agenda:0, attention:0, update:0, filed:0, today:3}};
controller.render();
const ready = {{summary:elements.summary.textContent, phases:elements.phases.textContent, action:elements.action.textContent}};
process.stdout.write(JSON.stringify({{first, ready, opened}}));
"""
assert run_node(script) == {
"first": {
"summary": "3 items before Today · 3 planned",
"phases": "Attention 2 · Updates 1",
"action": "Review Attention",
},
"ready": {
"summary": "Review clear · 3 planned",
"phases": "All urgent queues reviewed",
"action": "Continue Today",
},
"opened": ["attention"],
}
def test_prepare_today_pass_persists_per_account_and_hands_off_using_live_counts():
script = f"""
const createStartDay = require({json.dumps(str(START_DAY))});
const saved = new Map();
const storage = {{
getItem: key => saved.has(key) ? saved.get(key) : null,
setItem: (key, value) => saved.set(key, value),
removeItem: key => saved.delete(key),
}};
let login = 'timmy';
let day = '2026-08-15';
let counts = {{agenda:2, attention:1, update:1, filed:1, today:3}};
const opened = [];
const handoffs = [];
const controller = createStartDay({{
storage,
getLogin: () => login,
getDay: () => day,
getCounts: () => counts,
openQueue: name => opened.push(name),
onHandoff: state => handoffs.push({{next:state.next, label:state.label}}),
}});
const started = controller.startNext();
counts = {{agenda:0, attention:0, update:1, filed:1, today:3}};
const handed = controller.completePhase('agenda');
const resumed = createStartDay({{
storage,
getLogin: () => login,
getDay: () => day,
getCounts: () => counts,
openQueue: name => opened.push(name),
}}).state();
login = 'alexander';
const isolated = createStartDay({{
storage,
getLogin: () => login,
getDay: () => day,
getCounts: () => counts,
openQueue: name => opened.push(name),
}}).state();
login = 'timmy';
day = '2026-08-16';
const expired = createStartDay({{
storage,
getLogin: () => login,
getDay: () => day,
getCounts: () => counts,
openQueue: name => opened.push(name),
}}).state();
day = '2026-08-15';
const finished = controller.finish();
const cleared = controller.state();
process.stdout.write(JSON.stringify({{started, handed, resumed, isolated, expired, finished, cleared, opened, handoffs}}));
"""
assert run_node(script) == {
"started": "agenda",
"handed": True,
"resumed": {"active": True, "next": "update", "label": "Review Updates"},
"isolated": {"active": False, "next": "update", "label": "Review Updates"},
"expired": {"active": False, "next": "update", "label": "Review Updates"},
"finished": True,
"cleared": {"active": False, "next": "update", "label": "Review Updates"},
"opened": ["agenda"],
"handoffs": [{"next": "update", "label": "Review Updates"}],
}
def test_prepare_today_attention_hands_off_once_only_after_authoritative_refresh():
script = f"""
const createStartDay = require({json.dumps(str(START_DAY))});
const saved = new Map();
const storage = {{
getItem: key => saved.has(key) ? saved.get(key) : null,
setItem: (key, value) => saved.set(key, value),
removeItem: key => saved.delete(key),
}};
let counts = {{agenda:0, attention:1, update:2, filed:0, today:3}};
const opened = [];
const handoffs = [];
const controller = createStartDay({{
storage,
getLogin: () => 'timmy',
getDay: () => '2026-08-15',
getCounts: () => counts,
openQueue: name => opened.push(name),
onHandoff: current => handoffs.push({{next:current.next, label:current.label}}),
}});
const launchedAttention = controller.startNext();
counts = {{agenda:0, attention:0, update:2, filed:0, today:3}};
const ignoredPartial = controller.reconcile({{authoritative:false}});
const handedToUpdates = controller.reconcile({{authoritative:true}});
const duplicateRefresh = controller.reconcile({{authoritative:true}});
const launchedUpdates = controller.startNext();
counts = {{agenda:0, attention:0, update:0, filed:0, today:3}};
const handedToToday = controller.reconcile({{authoritative:true}});
const duplicateReadyRefresh = controller.reconcile({{authoritative:true}});
process.stdout.write(JSON.stringify({{
launchedAttention, ignoredPartial, handedToUpdates, duplicateRefresh,
launchedUpdates, handedToToday, duplicateReadyRefresh, opened, handoffs,
}}));
"""
assert run_node(script) == {
"launchedAttention": "attention",
"ignoredPartial": False,
"handedToUpdates": True,
"duplicateRefresh": False,
"launchedUpdates": "update",
"handedToToday": True,
"duplicateReadyRefresh": False,
"opened": ["attention", "update"],
"handoffs": [
{"next": "update", "label": "Review Updates"},
{"next": "today", "label": "Continue Today"},
],
}
def test_prepare_today_checkpoint_uses_the_device_local_day():
script = f"""
process.env.TZ = 'Pacific/Honolulu';
const RealDate = Date;
global.Date = class extends RealDate {{
constructor(...args) {{ super(...(args.length ? args : ['2026-08-16T01:00:00Z'])); }}
}};
const createStartDay = require({json.dumps(str(START_DAY))});
const saved = new Map();
const storage = {{
getItem: key => saved.has(key) ? saved.get(key) : null,
setItem: (key, value) => saved.set(key, value),
removeItem: key => saved.delete(key),
}};
createStartDay({{
storage,
getLogin: () => 'timmy',
getCounts: () => ({{agenda:1}}),
openQueue: () => {{}},
}}).startNext();
process.stdout.write(JSON.stringify(JSON.parse(saved.get('stackchain.mobile-start-day.v1'))));
"""
assert run_node(script) == {"login": "timmy", "day": "2026-08-15", "phase": "agenda"}
@pytest.mark.anyio
async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile_bundle():
html = await dashboard()
service_worker = (START_DAY.parent / "service-worker.js").read_text()
assert 'class="mobile-start-day"' in html
assert 'id="mobile-start-day-summary"' in html
assert 'id="mobile-start-day-phases"' in html
assert 'id="mobile-start-day-action"' in html
assert 'id="finish-mobile-start-day"' in html
assert '<script src="static/mobile-start-day.js"></script>' in html
assert "const mobileStartDay = createMobileStartDay({" in html
assert "getLogin: () => confirmedOwnerLogin" in html
assert "onHandoff: current =>" in html
assert "mobileStartDay.completePhase('agenda')" in html
assert "mobileStartDay.completePhase('update')" in html
assert "mobileStartDay.completePhase('filed')" in html
assert "mobileStartDay.reconcile({authoritative:authoritativeMyWorkRefresh})" in html
assert "mobileStartDay.finish()" in html
assert "openQueue: name =>" in html
assert "if (sheet.open) sheet.close();" in html
assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
assert "mobileStartDay.render();" in html
assert ".mobile-start-day-action { width:100%; min-height:48px;" in html
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
assert "stackchain-dashboard-shell-v111" in service_worker