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-v85" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source