110 lines
5.2 KiB
Python
110 lines
5.2 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
CONTROLLER = Path(__file__).parents[1] / "frontend" / "today-week-reschedule.js"
|
|
INDEX = CONTROLLER.parent / "index.html"
|
|
CSS = CONTROLLER.parent / "dashboard.css"
|
|
DASHBOARD = CONTROLLER.parent / "dashboard.js"
|
|
BUNDLE = CONTROLLER.parents[1] / "src" / "frontend_bundle.py"
|
|
|
|
|
|
def run_controller(scenario: str) -> dict:
|
|
harness = f"""
|
|
const createReschedule = 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_reschedule_controller_reviews_load_and_atomically_confirms_selected_day():
|
|
result = run_controller("""
|
|
const requests=[];
|
|
const weekState={revision:7,offline_snapshot:false,days:[]};
|
|
const review={days:[
|
|
{plan_date:'2026-08-24',label:'Mon, Aug 24',ids:['one'],planned_minutes:30,capacity_minutes:60,overloaded:false},
|
|
{plan_date:'2026-08-25',label:'Tue, Aug 25',ids:[],planned_minutes:0,capacity_minutes:90,overloaded:false}
|
|
]};
|
|
const week={load:async()=>weekState,review:()=>review,adopt:value=>{weekState.revision=value.revision;weekState.days=value.days;}};
|
|
const api=async(url,options)=>{
|
|
if(url==='api/v1/today')return {revision:4,ids:['active','other'],estimates:{active:45}};
|
|
requests.push({url,body:JSON.parse(options.body)});return {
|
|
today:{revision:5,ids:['other'],capacity_minutes:120,estimates:{other:20}},
|
|
week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}
|
|
};};
|
|
let adoptedToday=null;
|
|
const controller=createReschedule({week,api,getToday:()=>({revision:4,ids:['active','other'],estimates:{active:45}}),
|
|
adoptToday:value=>{adoptedToday=value;},operationId:()=> 'move-active'});
|
|
const opened=await controller.open('active');
|
|
const confirmed=await controller.confirm('2026-08-25',45);
|
|
console.log(JSON.stringify({opened,confirmed,requests,adoptedToday,weekState}));
|
|
""")
|
|
|
|
assert result["opened"]["estimate_minutes"] == 45
|
|
assert result["opened"]["days"][0]["load"] == "30 / 60 min"
|
|
assert result["opened"]["days"][1]["eligible"] is True
|
|
assert result["requests"] == [{
|
|
"url": "api/v1/week/reschedule",
|
|
"body": {
|
|
"operation_id": "move-active",
|
|
"identity": "active",
|
|
"estimate_minutes": 45,
|
|
"plan_date": "2026-08-25",
|
|
"today_revision": 4,
|
|
"week_revision": 7,
|
|
"allow_over_capacity": False,
|
|
},
|
|
}]
|
|
assert result["adoptedToday"]["ids"] == ["other"]
|
|
assert result["weekState"]["revision"] == 8
|
|
assert result["confirmed"]["today"]["revision"] == 5
|
|
|
|
|
|
def test_reschedule_controller_blocks_offline_full_and_overloaded_days_without_mutation():
|
|
result = run_controller("""
|
|
let calls=0;
|
|
const days=[
|
|
{plan_date:'2026-08-24',label:'Mon',ids:['1','2','3','4','5'],planned_minutes:50,capacity_minutes:60},
|
|
{plan_date:'2026-08-25',label:'Tue',ids:['1'],planned_minutes:50,capacity_minutes:60}
|
|
];
|
|
const week={load:async()=>({revision:2,offline_snapshot:false}),review:()=>({days}),adopt:()=>{}};
|
|
const api=async url=>{if(url==='api/v1/today')return {revision:3,ids:['active'],estimates:{active:30}};calls++;};
|
|
const controller=createReschedule({week,api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
|
|
const opened=await controller.open('active');
|
|
let fullError='',overError='';
|
|
try{await controller.confirm('2026-08-24',30);}catch(error){fullError=error.message;}
|
|
try{await controller.confirm('2026-08-25',30);}catch(error){overError=error.message;}
|
|
const offline=createReschedule({week:{load:async()=>({revision:2,offline_snapshot:true}),review:()=>({days})},api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
|
|
let offlineError='';try{await offline.open('active');}catch(error){offlineError=error.message;}
|
|
console.log(JSON.stringify({opened,fullError,overError,offlineError,calls}));
|
|
""")
|
|
|
|
assert result["opened"]["days"][0]["eligible"] is False
|
|
assert result["fullError"] == "That Week Ahead day already has five items."
|
|
assert result["overError"] == "That move exceeds the day's capacity. Confirm overload before rescheduling."
|
|
assert result["offlineError"] == "Reconnect before rescheduling Today into Week Ahead."
|
|
assert result["calls"] == 0
|
|
|
|
|
|
def test_mobile_active_today_reschedule_dialog_is_touch_safe_and_wired_into_release_bundle():
|
|
index = INDEX.read_text()
|
|
css = CSS.read_text()
|
|
dashboard = DASHBOARD.read_text()
|
|
bundle = BUNDLE.read_text()
|
|
|
|
assert 'data-work-session-reschedule-week' in index
|
|
assert 'id="today-week-reschedule"' in index
|
|
assert 'id="today-week-reschedule-days"' in index
|
|
assert 'id="today-week-reschedule-estimate"' in index
|
|
assert 'id="confirm-today-week-reschedule"' in index
|
|
assert 'id="cancel-today-week-reschedule"' in index
|
|
assert '.today-week-reschedule-days button { min-height:44px;' in css
|
|
assert '.today-week-reschedule-panel { width:min(100%,560px);' in css
|
|
assert 'x:currentTodayProgressTarget' in dashboard
|
|
assert "runTodayTransition('next')" in dashboard
|
|
assert '"static/today-week-reschedule.js"' in bundle
|