stackchain-dashboard/tests/test_week_plan_frontend.py
timmy 7d8db5f459
Some checks failed
CI / lint (pull_request) Successful in 3m39s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 4m42s
CI / release-candidate (pull_request) Has been skipped
feat: plan the next seven days on mobile (Closes #1177)
2026-08-20 11:17:41 +00:00

108 lines
4.3 KiB
Python

import json
import subprocess
from pathlib import Path
FRONTEND = Path(__file__).parents[1] / "frontend"
CONTROLLER = FRONTEND / "week-plan.js"
def run_controller(scenario: str) -> dict:
harness = f"""
const createWeekPlan = require({json.dumps(str(CONTROLLER))});
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
"""
completed = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
return json.loads(completed.stdout)
def test_week_controller_lists_seven_local_dates_and_saves_one_without_changing_other_days():
result = run_controller("""
const requests=[];
const initial={revision:4,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}},
{plan_date:'2026-08-23',ids:['issue:r:3:'],capacity_minutes:90,estimates:{'issue:r:3:':45}}
]};
const fetchJson=async(url,options={})=>{
requests.push({url,method:options.method||'GET',body:options.body?JSON.parse(options.body):null});
if(!options.method)return initial;
return {revision:5,timezone:'UTC',days:JSON.parse(options.body).days};
};
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
const dates=week.dates();
const saved=await week.saveDay('2026-08-22',{ids:['issue:r:2:'],capacity_minutes:120,estimates:{'issue:r:2:':50}});
console.log(JSON.stringify({dates,saved,requests}));
""")
assert [item["date"] for item in result["dates"]] == [
"2026-08-21", "2026-08-22", "2026-08-23", "2026-08-24",
"2026-08-25", "2026-08-26", "2026-08-27",
]
assert [day["plan_date"] for day in result["saved"]["days"]] == [
"2026-08-21", "2026-08-22", "2026-08-23"
]
assert result["requests"][1]["body"]["base_revision"] == 4
assert result["requests"][1]["body"]["days"][0]["ids"] == ["issue:r:1:"]
assert result["requests"][1]["body"]["days"][2]["ids"] == ["issue:r:3:"]
def test_week_controller_preserves_both_versions_when_another_device_changes_the_week():
result = run_controller("""
let calls=0;
const remote={revision:8,timezone:'UTC',days:[{plan_date:'2026-08-22',ids:['server'],capacity_minutes:60,estimates:{}}]};
const fetchJson=async(_url,options={})=>{
calls+=1;
if(options.method==='PUT'){const error=new Error('changed');error.status=409;throw error;}
return calls===1?{revision:7,timezone:'UTC',days:[]}:remote;
};
const week=createWeekPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
await week.load();
try { await week.saveDay('2026-08-21',{ids:['phone'],capacity_minutes:90,estimates:{}}); } catch(_error) {}
console.log(JSON.stringify({conflict:week.conflict(),state:week.state()}));
""")
assert result["conflict"]["local"]["days"][0]["ids"] == ["phone"]
assert result["conflict"]["remote"]["days"][0]["ids"] == ["server"]
assert result["state"]["revision"] == 7
def test_week_controller_promotes_only_the_earliest_due_plan_with_stable_identity():
result = run_controller("""
const requests=[];
const week=createWeekPlan({fetchJson:async(url,options={})=>{
requests.push({url,body:options.body?JSON.parse(options.body):null});
return {revision:9,ids:['oldest']};
},localDate:()=> '2026-08-23',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['oldest'],capacity_minutes:60,estimates:{}},
{plan_date:'2026-08-22',ids:['newer'],capacity_minutes:60,estimates:{}}
]});
const promoted=await week.promote(3);
console.log(JSON.stringify({promoted,requests}));
""")
assert result["requests"] == [{
"url": "api/v1/week/promote",
"body": {
"promotion_id": "week-2026-08-21-r6",
"week_revision": 6,
"plan_date": "2026-08-21",
"today_revision": 3,
},
}]
def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe():
index = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
assert 'data-mobile-queue="week"' in index
assert 'id="week-plan-dates"' in index
assert '<script src="static/week-plan.js"></script>' in index
assert "name === 'week' ? openWeekPlanner" in dashboard
assert "weekWorkflow.save" in dashboard
assert ".week-plan-dates button { min-height:44px;" in css
assert "overflow-x:auto" in css