168 lines
8.0 KiB
Python
168 lines
8.0 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
WEEK_CALENDAR = Path(__file__).parents[1] / "frontend" / "week-calendar.js"
|
|
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
|
BUNDLE = Path(__file__).parents[1] / "src" / "frontend_bundle.py"
|
|
|
|
|
|
def run_node(source: str):
|
|
completed = subprocess.run(["node", "-e", source], capture_output=True, text=True)
|
|
assert completed.returncode == 0, completed.stderr
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def test_week_calendar_builds_ordered_local_time_blocks_and_serializes_opaque_events():
|
|
plan = {
|
|
"timezone": "America/New_York",
|
|
"revision": 7,
|
|
"days": [{
|
|
"plan_date": "2026-08-21",
|
|
"ids": ["issue:stackchain/dashboard:12:", "pull:stackchain/api:7:"],
|
|
"capacity_minutes": 120,
|
|
"estimates": {
|
|
"issue:stackchain/dashboard:12:": 45,
|
|
"pull:stackchain/api:7:": 30,
|
|
},
|
|
}],
|
|
}
|
|
items = {
|
|
"issue:stackchain/dashboard:12:": {
|
|
"id": 401,
|
|
"title": "Plan, ship; verify",
|
|
"repository": "stackchain/dashboard",
|
|
"number": 12,
|
|
"url": "https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12",
|
|
},
|
|
"pull:stackchain/api:7:": {
|
|
"title": "API release",
|
|
"repository": "stackchain/api",
|
|
"number": 7,
|
|
"url": "https://forge.alexanderwhitestone.com/git/stackchain/api/pulls/7",
|
|
},
|
|
}
|
|
source = f"""
|
|
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
|
|
const plan={json.dumps(plan)}, items={json.dumps(items)};
|
|
const blocks=calendar.buildWeekBlocks(plan, {{'2026-08-21':'09:00'}}, id=>items[id]);
|
|
const text=calendar.serializeWeekCalendar(blocks, {{timezone:plan.timezone,revision:plan.revision,generatedAt:'20260820T120000Z'}});
|
|
console.log(JSON.stringify({{blocks,text}}));
|
|
"""
|
|
|
|
result = run_node(source)
|
|
assert [(block["start_time"], block["end_time"]) for block in result["blocks"]] == [
|
|
("09:00", "09:45"), ("09:45", "10:15")
|
|
]
|
|
text = result["text"]
|
|
assert "TZID:America/New_York" in text
|
|
assert "DTSTART;TZID=America/New_York:20260821T090000" in text
|
|
assert "DTEND;TZID=America/New_York:20260821T094500" in text
|
|
assert "DTSTART;TZID=America/New_York:20260821T094500" in text
|
|
assert "DTEND;TZID=America/New_York:20260821T101500" in text
|
|
assert "TRANSP:OPAQUE" in text
|
|
assert "UID:week-issue-stackchain-dashboard-12@stackchain" in text
|
|
assert "SEQUENCE:7" in text
|
|
assert "SUMMARY:Plan\\, ship\\; verify" in text
|
|
unfolded = text.replace("\r\n ", "")
|
|
assert "URL:https://forge.alexanderwhitestone.com/git/stackchain/stackchain-dashboard/issues/12" in unfolded
|
|
|
|
|
|
def test_week_calendar_reexports_update_stable_distinct_work_events():
|
|
source = f"""
|
|
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
|
|
const issue={{id:'issue:stackchain/dashboard:12:',title:'Ship it',repository:'stackchain/dashboard',number:12,
|
|
plan_date:'2026-08-21',start_time:'09:00',end_time:'09:30'}};
|
|
const moved={{...issue,plan_date:'2026-08-22',start_time:'14:00',end_time:'14:30'}};
|
|
const pull={{...moved,id:'pull:stackchain/dashboard:12:'}};
|
|
const first=calendar.serializeWeekCalendar([issue],{{timezone:'UTC',revision:7,generatedAt:'20260820T120000Z'}});
|
|
const second=calendar.serializeWeekCalendar([moved,pull],{{timezone:'UTC',revision:8,generatedAt:'20260821T120000Z'}});
|
|
const events=text=>text.split('BEGIN:VEVENT').slice(1).map(value=>({{
|
|
uid:value.match(/UID:([^\\r]+)/)[1],sequence:Number(value.match(/SEQUENCE:(\\d+)/)[1])
|
|
}}));
|
|
console.log(JSON.stringify({{first:events(first),second:events(second)}}));
|
|
"""
|
|
|
|
result = run_node(source)
|
|
assert result["first"] == [{
|
|
"uid": "week-issue-stackchain-dashboard-12@stackchain",
|
|
"sequence": 7,
|
|
}]
|
|
assert result["second"] == [
|
|
{"uid": "week-issue-stackchain-dashboard-12@stackchain", "sequence": 8},
|
|
{"uid": "week-pull-stackchain-dashboard-12@stackchain", "sequence": 8},
|
|
]
|
|
|
|
|
|
def test_week_calendar_places_whole_tasks_in_imported_free_windows_and_reports_unfitted_work():
|
|
source = f"""
|
|
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
|
|
const plan={{days:[{{plan_date:'2026-08-21',ids:['one','two','three'],estimates:{{one:45,two:90,three:400}}}}]}};
|
|
const items={{one:{{title:'First',repository:'r',number:1}},two:{{title:'Second',repository:'r',number:2}},
|
|
three:{{title:'Too large',repository:'r',number:3}}}};
|
|
const availability=[{{plan_date:'2026-08-21',free_windows:[{{start_time:'09:00',end_time:'10:00'}},{{start_time:'11:00',end_time:'17:00'}}]}}];
|
|
const result=calendar.buildWeekSchedule(plan,{{'2026-08-21':'09:00'}},id=>items[id],null,availability);
|
|
console.log(JSON.stringify(result));
|
|
"""
|
|
|
|
result = run_node(source)
|
|
assert [(block["id"], block["start_time"], block["end_time"]) for block in result["blocks"]] == [
|
|
("one", "09:00", "09:45"),
|
|
("two", "11:00", "12:30"),
|
|
]
|
|
assert result["blockers"] == [{
|
|
"id": "three", "title": "Too large", "plan_date": "2026-08-21", "minutes": 400
|
|
}]
|
|
|
|
|
|
def test_week_calendar_excludes_unselected_items_and_downloads_when_native_share_is_unavailable():
|
|
source = f"""
|
|
const calendar=require({json.dumps(str(WEEK_CALENDAR))});
|
|
const plan={{timezone:'UTC',revision:2,days:[{{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:60,estimates:{{one:20,two:25}}}}]}};
|
|
const items={{one:{{title:'Private',repository:'r',number:1,url:'https://forge/r/1'}},two:{{title:'Public',repository:'r',number:2,url:'https://forge/r/2'}}}};
|
|
const blocks=calendar.buildWeekBlocks(plan,{{'2026-08-21':'13:00'}},id=>items[id],new Set(['two']));
|
|
class FakeFile {{constructor(parts,name,options){{this.parts=parts;this.name=name;this.type=options.type;}}}}
|
|
const clicks=[],revoked=[];
|
|
(async()=>{{
|
|
const result=await calendar.deliverWeekCalendar({{text:'calendar',filename:'week.ics',navigator:{{}},FileCtor:FakeFile,
|
|
document:{{createElement:()=>({{click(){{clicks.push([this.download,this.href]);}}}})}},
|
|
urlApi:{{createObjectURL:()=> 'blob:week',revokeObjectURL:value=>revoked.push(value)}}}});
|
|
console.log(JSON.stringify({{blocks,result,clicks,revoked}}));
|
|
}})();
|
|
"""
|
|
|
|
result = run_node(source)
|
|
assert [block["id"] for block in result["blocks"]] == ["two"]
|
|
assert result["blocks"][0]["start_time"] == "13:00"
|
|
assert result["result"] == "downloaded"
|
|
assert result["clicks"] == [["week.ics", "blob:week"]]
|
|
assert result["revoked"] == ["blob:week"]
|
|
|
|
|
|
def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_controls():
|
|
index = INDEX.read_text()
|
|
dashboard = DASHBOARD.read_text()
|
|
bundle = BUNDLE.read_text()
|
|
|
|
assert '<script src="static/week-calendar.js"></script>' in index
|
|
assert 'id="week-calendar-handoff"' in index
|
|
assert 'id="week-calendar-days"' in index
|
|
assert 'id="share-week-calendar"' in index
|
|
assert 'id="back-to-week-review-from-calendar"' in index
|
|
assert 'id="export-saved-week-calendar"' in index
|
|
assert "Add saved week to calendar" in index
|
|
assert "Titles, references, dates, and times are included" in index
|
|
assert "Imported busy time is used only until this page reloads" in index
|
|
assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
|
|
assert "wc.open(weekPlan.state())" in dashboard
|
|
assert "getItem:weekFlow.i" in dashboard
|
|
assert "i:id=>getItem(id)||controller.item?.(id)" in (WEEK_CALENDAR.parent / "week-plan.js").read_text()
|
|
assert "#export-saved-week-calendar" in WEEK_CALENDAR.read_text()
|
|
assert "focusReturn=event.currentTarget" in WEEK_CALENDAR.read_text()
|
|
assert "wc.close()" in dashboard
|
|
assert "StackchainWeekCalendar.setAvailabilityProvider(workflow.availability)" in (WEEK_CALENDAR.parent / "week-calendar-import.js").read_text()
|
|
assert "Planning around imported busy time" in WEEK_CALENDAR.read_text()
|
|
assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle
|