feat: export Week Ahead as calendar blocks (Closes #1198)
This commit is contained in:
1 parent
875f4eea29
commit
dd3016b96b
11 files changed
+306
-9
No files matched your search
@@ -102,6 +102,20 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
bounds = confirm.bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
confirm.click()
|
||||
expect(page.locator("#week-calendar-handoff")).to_be_visible()
|
||||
expect(page.locator("#week-review")).to_be_hidden()
|
||||
start = page.locator("[data-week-calendar-start]").first
|
||||
expect(start).to_have_value("09:00")
|
||||
expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text("09:00–09:30 · 30 min")
|
||||
for control in (start, page.locator("#back-to-week-review-from-calendar"), page.locator("#share-week-calendar")):
|
||||
bounds = control.bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
page.locator("#back-to-week-review-from-calendar").click()
|
||||
expect(page.locator("#week-review")).to_be_visible()
|
||||
confirm.click()
|
||||
with page.expect_download() as download_info:
|
||||
page.locator("#share-week-calendar").click()
|
||||
assert download_info.value.suggested_filename.startswith("stackchain-week-ahead-")
|
||||
expect(page.locator("#plan-today-sheet")).to_be_hidden()
|
||||
page.wait_for_function("() => document.querySelector('#mobile-week-summary').textContent !== 'Loading Week Ahead…'")
|
||||
assert saved and len(saved[-1]["days"]) == 3
|
||||
|
||||
@@ -1328,6 +1328,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/plan-today-readiness.js",
|
||||
"/dashboard/static/plan-today-preview.js",
|
||||
"/dashboard/static/tomorrow-plan.js",
|
||||
"/dashboard/static/week-calendar.js",
|
||||
"/dashboard/static/week-plan.js",
|
||||
"/dashboard/static/search-week-plan.js",
|
||||
"/dashboard/static/today-sync.js",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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-7-20260821-issue-stackchain-dashboard-12@stackchain" 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_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 "Titles, references, dates, and times are included" in index
|
||||
assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
|
||||
assert "weekCalendar.open(weekPlan.state())" in dashboard
|
||||
assert "weekCalendar.close()" in dashboard
|
||||
assert '"static/week-calendar.js", "static/week-plan.js"' in bundle
|
||||
@@ -16,6 +16,25 @@ const createWeekPlan = require({json.dumps(str(CONTROLLER))});
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_week_controller_requires_executable_days_before_calendar_handoff():
|
||||
result = run_controller("""
|
||||
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
|
||||
week.adopt({revision:7,timezone:'UTC',days:[
|
||||
{plan_date:'2026-08-21',ids:['over'],capacity_minutes:30,estimates:{over:45}},
|
||||
{plan_date:'2026-08-22',ids:['missing-estimate'],capacity_minutes:60,estimates:{}},
|
||||
{plan_date:'2026-08-23',ids:['missing-capacity'],capacity_minutes:null,estimates:{'missing-capacity':20}},
|
||||
]});
|
||||
console.log(JSON.stringify(week.review()));
|
||||
""")
|
||||
|
||||
assert result["can_confirm"] is False
|
||||
assert result["blockers"] == [
|
||||
{"type": "over-capacity", "plan_date": "2026-08-21", "minutes": 15},
|
||||
{"type": "missing-estimate", "plan_date": "2026-08-22", "id": "missing-estimate"},
|
||||
{"type": "missing-capacity", "plan_date": "2026-08-23"},
|
||||
]
|
||||
|
||||
|
||||
def test_week_controller_lists_seven_local_dates_and_saves_one_without_changing_other_days():
|
||||
result = run_controller("""
|
||||
const requests=[];
|
||||
|
||||
Reference in new issue
Block a user