430 lines
22 KiB
Python
430 lines
22 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_retries_one_stale_revision_when_active_intent_is_unchanged():
|
|
result = run_controller("""
|
|
const requests=[];
|
|
let attempts=0;
|
|
const weekState={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
|
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
]};
|
|
const week={
|
|
load:async()=>weekState,
|
|
review:()=>({days:[{...weekState.days[0],label:'Tue',planned_minutes:0}]}),
|
|
adopt:value=>Object.assign(weekState,value),
|
|
};
|
|
const api=async(url,options)=>{
|
|
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
|
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:[
|
|
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
]};
|
|
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
|
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
|
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[
|
|
{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}
|
|
]}};
|
|
};
|
|
const controller=createReschedule({week,api,getToday:()=>null,adoptToday:()=>{},operationId:()=> 'move-active'});
|
|
await controller.open('active');
|
|
let message='',confirmed=null;
|
|
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
|
console.log(JSON.stringify({message,confirmed,requests}));
|
|
""")
|
|
|
|
assert result["message"] == ""
|
|
assert result["confirmed"]["today"]["revision"] == 6
|
|
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
|
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
|
assert result["requests"][0]["operation_id"] == result["requests"][1]["operation_id"]
|
|
|
|
|
|
def test_reschedule_controller_rebases_admitted_move_once_after_stale_revision():
|
|
result = run_controller("""
|
|
const values=new Map();
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
const requests=[];let attempts=0;
|
|
const initialWeek={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
|
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
]};
|
|
const week={load:async()=>initialWeek,review:()=>({days:[{...initialWeek.days[0],label:'Tue',planned_minutes:0}]}),adopt:()=>{}};
|
|
const api=async(url,options)=>{
|
|
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
|
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:initialWeek.days};
|
|
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
|
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
|
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[]}};
|
|
};
|
|
const controller=createReschedule({week,api,adoptToday:()=>{},storage,getLogin:()=> 'timmy',operationId:()=> 'move-active'});
|
|
await controller.open('active');
|
|
let message='',confirmed=null;
|
|
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
|
console.log(JSON.stringify({message,confirmed,requests,remaining:values.size}));
|
|
""")
|
|
|
|
assert result["message"] == ""
|
|
assert result["confirmed"]["today"]["revision"] == 6
|
|
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
|
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
|
assert result["remaining"] == 0
|
|
|
|
|
|
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,days:[]}),review:()=>({days})},api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
|
|
let offlineOpened=null;try{offlineOpened=await offline.open('active');}catch(_error){}
|
|
console.log(JSON.stringify({opened,fullError,overError,offlineOpened,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["offlineOpened"]["offline"] is True
|
|
assert result["calls"] == 0
|
|
|
|
|
|
def test_reschedule_controller_persists_offline_move_before_optimistic_adoption():
|
|
result = run_controller("""
|
|
const values=new Map();
|
|
const events=[];
|
|
const storage={
|
|
getItem:key=>values.get(key)||null,
|
|
setItem:(key,value)=>{events.push('persist');values.set(key,value);},
|
|
removeItem:key=>values.delete(key),
|
|
};
|
|
const today={revision:4,ids:['active','other'],capacity_minutes:120,estimates:{active:45,other:20}};
|
|
const weekState={revision:7,timezone:'UTC',offline_snapshot:true,days:[
|
|
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
]};
|
|
const week={load:async()=>weekState,review:()=>({days:[
|
|
{plan_date:'2026-08-25',label:'Tue',ids:[],planned_minutes:0,capacity_minutes:90,estimates:{}}
|
|
]}),adopt:value=>events.push(['week',value])};
|
|
const api=async()=>{const error=new Error('offline');error.status=0;throw error;};
|
|
const controller=createReschedule({week,api,getToday:()=>today,adoptToday:value=>events.push(['today',value]),
|
|
storage,getLogin:()=> 'Timmy',operationId:()=> 'move-active',now:()=>123});
|
|
const opened=await controller.open('active');
|
|
const confirmed=await controller.confirm('2026-08-25',45);
|
|
console.log(JSON.stringify({opened,confirmed,events,keys:[...values.keys()],record:JSON.parse([...values.values()][0])}));
|
|
""")
|
|
|
|
assert result["opened"]["offline"] is True
|
|
assert result["confirmed"]["sync_pending"] is True
|
|
assert result["events"][0] == "persist"
|
|
assert result["events"][1][0] == "today"
|
|
assert result["events"][1][1]["ids"] == ["other"]
|
|
assert result["events"][2][0] == "week"
|
|
assert result["events"][2][1]["days"][0]["ids"] == ["active"]
|
|
assert result["keys"] == ["stackchain.today-week-reschedule.v1.timmy"]
|
|
assert result["record"]["version"] == 2
|
|
assert result["record"]["records"][0]["body"]["operation_id"] == "move-active"
|
|
assert result["record"]["records"][0]["queued_at"] == 123
|
|
|
|
|
|
def test_reschedule_controller_restores_and_delivers_same_operation_after_reload():
|
|
result = run_controller("""
|
|
const key='stackchain.today-week-reschedule.v1.timmy';
|
|
const record={queued_at:123,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},
|
|
today:{revision:4,ids:['other'],estimates:{other:20}},
|
|
week:{revision:7,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}};
|
|
const values=new Map([[key,JSON.stringify(record)]]);const adopted=[];const requests=[];
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
const api=async(url,options)=>{requests.push({url,body:JSON.parse(options.body)});return {
|
|
today:{revision:5,ids:['other'],estimates:{other:20}},
|
|
week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}};};
|
|
const controller=createReschedule({week:{adopt:value=>adopted.push(['week',value])},api,
|
|
getToday:()=>null,adoptToday:value=>adopted.push(['today',value]),storage,getLogin:()=> 'timmy'});
|
|
const restored=controller.resume();
|
|
const delivered=await controller.flush();
|
|
console.log(JSON.stringify({restored,delivered,adopted,requests,remaining:values.size}));
|
|
""")
|
|
|
|
assert result["restored"]["sync_pending"] is True
|
|
assert result["adopted"][0][0] == "today"
|
|
assert result["adopted"][1][0] == "week"
|
|
assert result["requests"][0]["body"]["operation_id"] == "move-active"
|
|
assert result["delivered"]["sync_pending"] is False
|
|
assert result["remaining"] == 0
|
|
|
|
|
|
def test_reschedule_controller_keeps_admitted_operation_when_connection_drops_on_confirm():
|
|
result = run_controller("""
|
|
const values=new Map();const events=[];
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{events.push('persist');values.set(key,value);},removeItem:key=>values.delete(key)};
|
|
const weekState={revision:7,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}]};
|
|
const week={load:async()=>weekState,review:()=>({days:[{...weekState.days[0],label:'Tue',planned_minutes:0}]}),
|
|
adopt:value=>events.push(['week',value])};
|
|
let calls=0;const api=async url=>{if(url==='api/v1/today')return {revision:4,ids:['active'],estimates:{active:45}};
|
|
calls++;const error=new Error('connection dropped');error.status=0;throw error;};
|
|
const controller=createReschedule({week,api,getToday:()=>null,adoptToday:value=>events.push(['today',value]),
|
|
storage,getLogin:()=> 'timmy',operationId:()=> 'stable-operation'});
|
|
await controller.open('active');
|
|
const result=await controller.confirm('2026-08-25',45);
|
|
console.log(JSON.stringify({result,events,calls,pending:controller.pending()}));
|
|
""")
|
|
|
|
assert result["result"]["sync_pending"] is True
|
|
assert result["events"][0] == "persist"
|
|
assert result["calls"] == 1
|
|
assert result["pending"]["body"]["operation_id"] == "stable-operation"
|
|
|
|
|
|
def test_reschedule_controller_queues_two_offline_moves_without_replacing_the_first():
|
|
result = run_controller("""
|
|
const values=new Map();
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
let today={revision:4,ids:['active','other'],capacity_minutes:120,estimates:{active:45,other:20}};
|
|
let weekState={revision:7,timezone:'UTC',offline_snapshot:true,days:[
|
|
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
]};
|
|
const confirmedWeek=JSON.parse(JSON.stringify(weekState));let loads=0;
|
|
const week={load:async()=>{loads++;weekState=JSON.parse(JSON.stringify(confirmedWeek));return weekState;},state:()=>weekState,review:()=>({days:[
|
|
{...weekState.days[0],label:'Tue',planned_minutes:Object.values(weekState.days[0].estimates).reduce((a,b)=>a+b,0)}
|
|
]}),adopt:value=>{weekState={...value,offline_snapshot:true};}};
|
|
const api=async()=>{const error=new Error('offline');error.status=0;throw error;};
|
|
let n=0;
|
|
const controller=createReschedule({week,api,getToday:()=>today,adoptToday:value=>{today=value;},
|
|
storage,getLogin:()=> 'timmy',operationId:()=> `move-${++n}`,now:()=>100+n});
|
|
await controller.open('active');
|
|
await controller.confirm('2026-08-25',45);
|
|
await controller.open('other');
|
|
const confirmed=await controller.confirm('2026-08-25',20);
|
|
const stored=JSON.parse([...values.values()][0]);
|
|
console.log(JSON.stringify({confirmed,stored,today,weekState,loads}));
|
|
""")
|
|
|
|
assert [entry["body"]["operation_id"] for entry in result["stored"]["records"]] == [
|
|
"move-1",
|
|
"move-2",
|
|
]
|
|
assert result["confirmed"]["pending_count"] == 2
|
|
assert result["today"]["ids"] == []
|
|
assert result["weekState"]["days"][0]["ids"] == ["active", "other"]
|
|
assert result["loads"] == 1
|
|
|
|
|
|
def test_reschedule_controller_drains_fifo_and_rebases_each_next_move():
|
|
result = run_controller("""
|
|
const key='stackchain.today-week-reschedule.v1.timmy';
|
|
const make=(id,identity,today,week)=>({queued_at:1,body:{operation_id:id,identity,estimate_minutes:20,
|
|
plan_date:'2026-08-25',today_revision:today,week_revision:week,allow_over_capacity:false},
|
|
today:{revision:today,ids:[]},week:{revision:week,days:[]}});
|
|
const values=new Map([[key,JSON.stringify({version:2,records:[make('move-1','one',4,7),make('move-2','two',4,7)]})]]);
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
const requests=[];let active=0,maxActive=0;
|
|
const api=async(_url,options)=>{active++;maxActive=Math.max(maxActive,active);
|
|
const body=JSON.parse(options.body);requests.push(body);await new Promise(resolve=>setTimeout(resolve,1));active--;
|
|
const step=requests.length;return {today:{revision:4+step,ids:[]},week:{revision:7+step,days:[]}};};
|
|
const controller=createReschedule({week:{adopt:()=>{}},api,adoptToday:()=>{},storage,getLogin:()=> 'timmy'});
|
|
const delivered=await controller.flush();
|
|
console.log(JSON.stringify({requests,maxActive,delivered,remaining:values.size}));
|
|
""")
|
|
|
|
assert [request["operation_id"] for request in result["requests"]] == ["move-1", "move-2"]
|
|
assert result["requests"][1]["today_revision"] == 5
|
|
assert result["requests"][1]["week_revision"] == 8
|
|
assert result["maxActive"] == 1
|
|
assert result["delivered"]["pending_count"] == 0
|
|
assert result["remaining"] == 0
|
|
|
|
|
|
def test_reschedule_controller_restores_server_truth_and_keeps_intent_on_conflict():
|
|
result = run_controller("""
|
|
const key='stackchain.today-week-reschedule.v1.timmy';
|
|
const record={queued_at:123,body:{operation_id:'move-active'},today:{revision:4,ids:[]},week:{revision:7,days:[]}};
|
|
const values=new Map([[key,JSON.stringify(record)]]);const adopted=[];
|
|
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
const api=async url=>{
|
|
if(url==='api/v1/week/reschedule'){const error=new Error('conflict');error.status=409;throw error;}
|
|
if(url==='api/v1/today')return {revision:9,ids:['active'],estimates:{active:45}};
|
|
return {revision:12,timezone:'UTC',days:[]};
|
|
};
|
|
const controller=createReschedule({week:{adopt:value=>adopted.push(['week',value])},api,
|
|
adoptToday:value=>adopted.push(['today',value]),storage,getLogin:()=> 'timmy'});
|
|
let message='';try{await controller.flush();}catch(error){message=error.message;}
|
|
console.log(JSON.stringify({message,adopted,pending:controller.pending(),remaining:values.size}));
|
|
""")
|
|
|
|
assert result["message"] == "Plans changed on another device. Review and retry this saved move."
|
|
assert result["adopted"][0][1]["revision"] == 9
|
|
assert result["adopted"][1][1]["revision"] == 12
|
|
assert result["pending"]["body"]["operation_id"] == "move-active"
|
|
assert result["remaining"] == 1
|
|
|
|
|
|
def run_mounted_confirmation(*, fail_move: bool = False, fail_refresh: bool = False) -> dict:
|
|
failure = "true" if fail_move else "false"
|
|
refresh_failure = "true" if fail_refresh else "false"
|
|
scenario = """
|
|
const mount=require('./frontend/today-week-reschedule.js').mount;
|
|
class Element {
|
|
constructor(){this.listeners={};this.dataset={};this.children=[];this.open=false;this.disabled=false;this.value='';this.textContent='';}
|
|
addEventListener(type,handler){(this.listeners[type] ||= []).push(handler);}
|
|
async emit(type,event={preventDefault(){}}){for(const handler of this.listeners[type]||[])await handler(event);}
|
|
append(...children){this.children.push(...children);}
|
|
replaceChildren(...children){this.children=children;}
|
|
querySelectorAll(selector){return selector==='button' ? this.children : [];}
|
|
querySelector(){return this.children.find(child=>!child.disabled)||null;}
|
|
setAttribute(){}
|
|
showModal(){this.open=true;}
|
|
close(){this.open=false;}
|
|
focus(){}
|
|
click(){return this.emit('click');}
|
|
}
|
|
const selectors={
|
|
'#today-week-reschedule':new Element(), '#today-week-reschedule-days':new Element(),
|
|
'#today-week-reschedule-estimate':new Element(), '#today-week-reschedule-status':new Element(),
|
|
'#confirm-today-week-reschedule':new Element(), '[data-work-session-reschedule-week]':new Element(),
|
|
'#cancel-today-week-reschedule':new Element(), '[data-mobile-today-more]':new Element(),
|
|
};
|
|
const document={hidden:false,addEventListener(){},createElement:()=>new Element()};
|
|
const window={addEventListener(){},setTimeout(){}};
|
|
let finishMove,failMove;
|
|
const move=new Promise((resolve,reject)=>{finishMove=resolve;failMove=reject;});
|
|
const today={revision:4,ids:['active'],estimates:{active:30}};
|
|
const weekState={revision:7,offline_snapshot:false,days:[]};
|
|
const week={load:async()=>weekState,review:()=>({days:[{
|
|
plan_date:'2026-08-27',label:'Thu',ids:[],planned_minutes:0,capacity_minutes:90,eligible:true,
|
|
}]}),adopt(){}};
|
|
const api=async(url)=>url==='api/v1/today' ? today : move;
|
|
mount({qs:selector=>selectors[selector],document,window,week,api,getToday:()=>today,
|
|
adoptToday(){},currentTarget:()=>({identity:'active'}),closeActions(){},refresh:async()=>{
|
|
if(__FAIL_REFRESH__)throw new Error('Refresh unavailable.');
|
|
},warm(){},
|
|
continueToday:async()=>{},announce(){},schedule:callback=>callback()});
|
|
await selectors['[data-work-session-reschedule-week]'].emit('click');
|
|
await selectors['#today-week-reschedule-days'].children[0].emit('click');
|
|
const confirming=selectors['#confirm-today-week-reschedule'].emit('click');
|
|
await Promise.resolve();
|
|
const openWhileSaving=selectors['#today-week-reschedule'].open;
|
|
if(__FAIL_MOVE__){
|
|
const error=new Error('Move unavailable. Try again.');error.status=500;failMove(error);
|
|
}else{
|
|
finishMove({today:{revision:5,ids:[],estimates:{}},week:{revision:8,days:[]}});
|
|
}
|
|
await confirming;
|
|
console.log(JSON.stringify({openWhileSaving,openAfterSettled:selectors['#today-week-reschedule'].open}));
|
|
"""
|
|
return run_controller(
|
|
scenario.replace("__FAIL_MOVE__", failure).replace(
|
|
"__FAIL_REFRESH__", refresh_failure
|
|
)
|
|
)
|
|
|
|
|
|
def test_reschedule_dialog_closes_as_soon_as_a_valid_move_is_confirmed():
|
|
assert run_mounted_confirmation() == {
|
|
"openWhileSaving": False,
|
|
"openAfterSettled": False,
|
|
}
|
|
|
|
|
|
def test_reschedule_dialog_reopens_with_retry_state_when_confirm_fails():
|
|
assert run_mounted_confirmation(fail_move=True) == {
|
|
"openWhileSaving": False,
|
|
"openAfterSettled": True,
|
|
}
|
|
|
|
|
|
def test_reschedule_dialog_stays_closed_when_refresh_fails_after_confirm_succeeds():
|
|
assert run_mounted_confirmation(fail_refresh=True) == {
|
|
"openWhileSaving": False,
|
|
"openAfterSettled": False,
|
|
}
|
|
|
|
|
|
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 'restoreWhenOwned()' in CONTROLLER.read_text()
|
|
assert "runTodayTransition('next')" in dashboard
|
|
assert 'reschedule:()=>({storage,getLogin})' in (CONTROLLER.parent / "week-plan.js").read_text()
|
|
assert "addEventListener('online', flushPending)" in CONTROLLER.read_text()
|
|
assert '"static/today-week-reschedule.js"' in bundle
|