108 lines
4.3 KiB
Python
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
|