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" PLAN_TODAY_PREVIEW = Path(__file__).parents[1] / "frontend" / "plan-today-preview.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}, } def test_plan_today_calculates_capacity_and_confirms_overcommitment_before_saving(): 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 + ':', save: plan => saved.push(plan), }}); planner.open([item(1), item(2)], [item(1), item(2)]); planner.setCapacity(90); planner.setEstimate('issue:stackchain/dashboard:1:', 60); planner.setEstimate('issue:stackchain/dashboard:2:', 45); const over = planner.snapshot(); const confirmation = planner.commit(); const stillOpen = planner.snapshot().open; const result = planner.commit({{confirmOverCapacity:true}}); process.stdout.write(JSON.stringify({{over, confirmation, stillOpen, result, saved}})); """ result = run_node(script) assert result["over"] == { "open": True, "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"], "count": 2, "limit": 5, "capacity_minutes": 90, "estimates": { "issue:stackchain/dashboard:1:": 60, "issue:stackchain/dashboard:2:": 45, }, "planned_minutes": 105, "remaining_minutes": -15, "unestimated_count": 0, "over_capacity": True, } assert result["confirmation"] == "confirm-over-capacity" assert result["stillOpen"] is True assert result["result"] == "saved" assert result["saved"] == [{ "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"], "capacity_minutes": 90, "estimates": { "issue:stackchain/dashboard:1:": 60, "issue:stackchain/dashboard:2:": 45, }, }] def test_plan_today_preview_preserves_draft_scroll_and_adds_item_once_on_return(): script = f""" const createPlanToday = require({json.dumps(str(PLAN_TODAY))}); const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))}); const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}}); const events = []; let scroll = 318; const planner = createPlanToday({{ identity: value => 'issue:stackchain/dashboard:' + value.number + ':', save: () => true, }}); planner.open([item(1)], [item(1), item(2)]); const preview = createPlanTodayPreview({{ planner, identity: value => 'issue:stackchain/dashboard:' + value.number + ':', getScroll: () => scroll, setScroll: value => {{ scroll = value; }}, onOpen: value => events.push(['open', value.number]), onClose: (value, trigger) => events.push(['close', value.number, trigger]), }}); preview.open(item(2), 'preview-2'); scroll = 0; const first = preview.close({{add:true}}); preview.open(item(2), 'preview-2'); scroll = 0; const second = preview.close({{add:true}}); process.stdout.write(JSON.stringify({{first, second, scroll, events, planner:planner.snapshot()}})); """ assert run_node(script) == { "first": "added", "second": "already-added", "scroll": 318, "events": [ ["open", 2], ["close", 2, "preview-2"], ["open", 2], ["close", 2, "preview-2"], ], "planner": { "open": True, "ids": ["issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:2:"], "count": 2, "limit": 5, }, } def test_plan_today_preview_requires_explicit_override_for_blocked_or_unknown_issue(): script = f""" const createPlanToday = require({json.dumps(str(PLAN_TODAY))}); const createPlanTodayPreview = require({json.dumps(str(PLAN_TODAY_PREVIEW))}); const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}}); const planner = createPlanToday({{ identity: value => 'issue:stackchain/dashboard:' + value.number + ':', save: () => true, }}); planner.open([], [item(2), item(3)]); const preview = createPlanTodayPreview({{ planner, identity: value => 'issue:stackchain/dashboard:' + value.number + ':', }}); preview.open(item(2)); preview.setDependencies({{available:true, dependencies:[{{repository:'stackchain/api', number:9, title:'Restore API', state:'open'}}]}}); const blockedState = preview.snapshot(); const blocked = preview.close({{add:true}}); const overridden = preview.close({{add:true, override:true}}); preview.open(item(3)); preview.setDependencies({{available:false, dependencies:[]}}); const unavailableState = preview.snapshot(); const unavailable = preview.close({{add:true}}); const unavailableOverride = preview.close({{add:true, override:true}}); process.stdout.write(JSON.stringify({{blockedState, blocked, overridden, unavailableState, unavailable, unavailableOverride, planner:planner.snapshot()}})); """ assert run_node(script) == { "blockedState": { "open": True, "item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 2, "title": "Issue 2"}, "trigger": None, "scroll": 0, "dependencies_available": True, "dependencies": [{"repository": "stackchain/api", "number": 9, "title": "Restore API", "state": "open"}], "requires_override": True, }, "blocked": "blocked", "overridden": "added", "unavailableState": { "open": True, "item": {"kind": "issue", "repository": "stackchain/dashboard", "number": 3, "title": "Issue 3"}, "trigger": None, "scroll": 0, "dependencies_available": False, "dependencies": [], "requires_override": True, }, "unavailable": "dependencies-unavailable", "unavailableOverride": "added", "planner": { "open": True, "ids": ["issue:stackchain/dashboard:2:", "issue:stackchain/dashboard:3:"], "count": 2, "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="plan-today-available"' in html assert 'inputmode="numeric" min="15" max="1440"' in html assert 'data-plan-estimate="' in html assert 'aria-label="Estimate for ' in html assert "formatPlanMinutes(state.planned_minutes)" in html assert "planToday.setCapacity" in html assert "planToday.setEstimate" in html assert "todaySync.enqueueConfiguration" in html assert "todayWork.runway(todayMyWork, state.index - 1)" in html assert "resetPlanTodayConfirmation()" in html assert "result === 'confirm-over-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 assert ".plan-preview-actions { position:fixed; z-index:76;" in html assert '' in html assert 'data-plan-preview="' in html assert 'id="add-plan-preview"' in html assert "taskOverlayHistory.open('plan-today-preview')" in html assert "planTodayPreview.close({ add:true })" in html assert 'id="issue-blockers"' in html assert "planTodayPreview.setDependencies({" in html assert "Add blocked item anyway & back" in html assert "Blocker status unavailable" in html assert ".issue-blockers { max-width:100%; overflow-x:hidden;" in html assert ".issue-blocker { min-width:0; overflow-wrap:anywhere;" in html assert "Preview unavailable offline" in html @pytest.mark.anyio async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(): html = await dashboard() assert "taskOverlayHistory.open('plan-today')" in html assert "previous === 'plan-today' && kind !== 'plan-today'" in html assert "kind === 'plan-today' && previous !== 'plan-today'" in html assert "openPlanToday(planTodayTrigger, false)" in html assert "closePlanToday(false)" in html assert "if (result === 'saved')" in html assert "taskOverlayHistory.leave();" in html def test_plan_today_controller_is_available_in_the_offline_shell(): source = SERVICE_WORKER.read_text() assert "stackchain-dashboard-shell-v87" in source assert "BASE + 'static/plan-today.js'" in source assert "BASE + 'static/plan-today-preview.js'" in source