import json import subprocess from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "week-calendar-import.js" FRONTEND = MODULE.parent def run_import(scenario: str) -> dict: harness = f""" const calendarImport = require({json.dumps(str(MODULE))}); (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_calendar_import_unions_busy_time_within_working_hours_across_local_days(): result = run_import(""" const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:one\r\nSUMMARY:Private planning\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:two\r\nSUMMARY:Secret customer call\r\nATTENDEE:mailto:private@example.com\r\nDTSTART:20260821T103000\r\nDTEND:20260821T120000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:outside\r\nDTSTART:20260821T070000\r\nDTEND:20260821T080000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:overnight\r\nLOCATION:Private office\r\nDTSTART:20260821T163000\r\nDTEND:20260822T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); console.log(JSON.stringify({days,serialized:JSON.stringify(days)})); """) assert result["days"][0] == { "plan_date": "2026-08-21", "busy_minutes": 150, "capacity_minutes": 330 } assert result["days"][1] == { "plan_date": "2026-08-22", "busy_minutes": 60, "capacity_minutes": 420 } assert [day["capacity_minutes"] for day in result["days"][2:]] == [480] * 5 for private_value in ("Private planning", "Secret customer call", "private@example.com", "Private office"): assert private_value not in result["serialized"] def test_calendar_import_rejects_invalid_and_oversized_files_without_returning_capacity(): result = run_import(""" const errors=[]; for(const source of ['not a calendar','BEGIN:VCALENDAR\\r\\n'+('X'.repeat(1024*1024))+'\\r\\nEND:VCALENDAR']) { try { calendarImport.review(source,{dates:['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']}); } catch(error) { errors.push(error.message); } } console.log(JSON.stringify({errors})); """) assert result["errors"] == [ "Choose a valid .ics calendar file.", "Calendar files must be 1 MB or smaller.", ] def test_calendar_import_workflow_reviews_then_applies_once_without_persisting_calendar_text(): result = run_import(""" const elements=new Map(); const element=(value='')=>({value,hidden:false,textContent:'',innerHTML:'',disabled:false,files:[],listeners:{}, addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}}); for(const selector of ['#week-capacity-import','#week-capacity-review','#week-capacity-days','#week-capacity-status', '#week-capacity-file','#week-capacity-start','#week-capacity-end','#apply-week-capacities','#cancel-week-capacity-import']) elements.set(selector,element()); elements.get('#week-capacity-start').value='09:00';elements.get('#week-capacity-end').value='17:00'; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; let staged=[],flushes=0,appliedCalls=0; const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})), stageCapacities:value=>{staged.push(value);return {sync_pending:true};},flush:async()=>{flushes+=1;}}; const workflow=calendarImport.createWorkflow({controller,qs:selector=>elements.get(selector),onApplied:()=>{appliedCalls+=1;}}); workflow.open(); const source=`BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Do not retain me\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nEND:VCALENDAR`; const reviewed=workflow.review(source); await workflow.apply(); console.log(JSON.stringify({reviewed,staged,flushes,appliedCalls,rootHidden:elements.get('#week-capacity-import').hidden, status:elements.get('#week-capacity-status').textContent,markup:elements.get('#week-capacity-days').innerHTML, fileValue:elements.get('#week-capacity-file').value,workflowState:workflow.state()})); """) assert result["reviewed"][0]["capacity_minutes"] == 420 assert len(result["staged"]) == 1 assert len(result["staged"][0]) == 7 assert result["flushes"] == 1 assert result["appliedCalls"] == 1 assert result["rootHidden"] is True assert result["fileValue"] == "" assert result["workflowState"] is None assert "Do not retain me" not in json.dumps(result) def test_week_ahead_exposes_private_calendar_capacity_import_in_the_packaged_planning_flow(): html = (FRONTEND / "index.html").read_text() dashboard = (FRONTEND / "dashboard.js").read_text() css = (FRONTEND / "dashboard.css").read_text() assert 'id="open-week-capacity-import"' in html assert 'id="week-capacity-import"' in html assert 'id="week-capacity-file"' in html and 'accept=".ics,text/calendar"' in html assert 'id="week-capacity-start"' in html and 'id="week-capacity-end"' in html assert 'id="week-capacity-review"' in html assert 'id="apply-week-capacities"' in html assert '' in html assert html.index('static/week-calendar-import.js') < html.index('static/week-plan.js') week_plan = (FRONTEND / "week-plan.js").read_text() assert "weekCalendarImport.mount" in week_plan assert "weekCalendarImport.mount" not in dashboard assert "open-week-capacity-import" in MODULE.read_text() assert ".week-capacity-import" in css assert "overflow-x:hidden" in css