import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard PLAN_TODAY = Path(__file__).parents[1] / "frontend" / "plan-today.js" SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.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_plan_today_drafts_capacity_order_and_cancel_without_writing(): script = f""" const createPlanToday = require({json.dumps(str(PLAN_TODAY))}); const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}}); const saved = []; const planner = createPlanToday({{ identity: value => 'issue:stackchain/dashboard:' + value.number + ':', limit: 3, save: ids => saved.push(ids), }}); planner.open([item(1), item(2)], [item(1), item(2), item(3), item(4)]); const add = planner.toggle(item(3)); const full = planner.toggle(item(4)); const moved = planner.move('issue:stackchain/dashboard:3:', 'up'); const snapshot = planner.snapshot(); planner.cancel(); process.stdout.write(JSON.stringify({{add, full, moved, snapshot, afterCancel:planner.snapshot(), saved}})); """ assert run_node(script) == { "add": "added", "full": "full", "moved": True, "snapshot": { "open": True, "ids": [ "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:3:", "issue:stackchain/dashboard:2:", ], "count": 3, "limit": 3, }, "afterCancel": {"open": False, "ids": [], "count": 0, "limit": 3}, "saved": [], } def test_plan_today_saves_exact_draft_and_starts_first_item_only_after_success(): script = f""" const createPlanToday = require({json.dumps(str(PLAN_TODAY))}); const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}}); const calls = []; const planner = createPlanToday({{ identity: value => 'issue:stackchain/dashboard:' + value.number + ':', save: ids => {{ calls.push(['save', ...ids]); return true; }}, start: first => calls.push(['start', first.number]), }}); planner.open([item(1)], [item(1), item(2)]); planner.toggle(item(2)); const result = planner.commit({{start:true}}); process.stdout.write(JSON.stringify({{result, calls, snapshot:planner.snapshot()}})); """ assert run_node(script) == { "result": "saved", "calls": [ ["save", "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"], ["start", 1], ], "snapshot": {"open": False, "ids": [], "count": 0, "limit": 5}, } @pytest.mark.anyio async def test_mobile_dashboard_wires_focused_plan_today_sheet(): html = await dashboard() assert '' in html assert 'id="plan-today"' in html assert 'id="plan-today-sheet" role="dialog"' in html assert 'id="plan-today-capacity"' in html assert 'id="save-and-start-today"' in html assert "const planToday = createPlanToday({" in html assert "todaySync.enqueue('remove'" in html assert "todaySync.enqueue('add'" in html assert "qs('#plan-today').addEventListener('click'" in html assert ".plan-today-actions { position:sticky; bottom:0;" in html assert ".plan-today-actions button { min-height:44px;" in html assert "padding-bottom:calc(12px + env(safe-area-inset-bottom))" in html assert ".plan-today-item { grid-template-columns:1fr; }" in html assert ".plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr);" in html def test_plan_today_controller_is_available_in_the_offline_shell(): source = SERVICE_WORKER.read_text() assert "stackchain-dashboard-shell-v59" in source assert "BASE + 'static/plan-today.js'" in source