feat: reflow overloaded Week Ahead plans (Closes #1226)
CI / lint (pull_request) Successful in 3m55s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m47s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 15:34:31 +00:00
1 parent b7dd9757aa
commit ffba6f1212
6 files changed
+306 -9

No files matched your search

+30 -6
View File
@@ -47,8 +47,8 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
route.fulfill(status=200, content_type="application/json", body=json.dumps({
"revision": 0, "timezone": None, "days": [{
"plan_date": (date.today() + timedelta(days=1)).isoformat(),
"ids": ["issue:acme/mobile:41:"], "capacity_minutes": 60,
"estimates": {"issue:acme/mobile:41:": 30},
"ids": ["issue:acme/mobile:41:", "issue:acme/mobile:42:"], "capacity_minutes": 60,
"estimates": {"issue:acme/mobile:41:": 45, "issue:acme/mobile:42:": 45},
}],
}))
@@ -76,6 +76,30 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(page.locator("#confirm-week-plan")).to_be_hidden()
edit_week = page.locator("#edit-week-plan")
expect(edit_week).to_be_visible()
reflow = page.locator("#open-week-reflow")
expect(reflow).to_be_visible()
bounds = reflow.bounding_box()
assert bounds and bounds["height"] >= 44
reflow.click()
preview = page.locator("#week-reflow-review")
expect(preview).to_be_visible()
expect(page.locator("#week-reflow-days")).to_contain_text("1 item · 45 of 60 min")
expect(page.locator("#week-reflow-unscheduled")).to_have_text(
"1 item cannot fit: Polish desktop filters. It will remain in My Work."
)
for control in (page.locator("#cancel-week-reflow"), page.locator("#apply-week-reflow")):
control_bounds = control.bounding_box()
assert control_bounds and control_bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#cancel-week-reflow").click()
expect(preview).to_be_hidden()
assert saved == [], "cancelling reflow must not write"
reflow.click()
page.locator("#apply-week-reflow").click()
expect(page.locator("#week-review-status")).to_have_text("Week Ahead reflowed and saved.")
expect(preview).to_be_hidden()
assert len(saved) == 1
assert [day["ids"] for day in saved[-1]["days"]][:2] == [["issue:acme/mobile:41:"], []]
planned_item = cards.first.locator("[data-week-open-item]")
expect(planned_item).to_have_count(1)
planned_bounds = planned_item.bounding_box()
@@ -88,7 +112,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(page.locator("#issue-sheet")).not_to_have_class(re.compile(r"\bopen\b"))
expect(overview).to_be_visible()
expect(planned_item).to_be_focused()
assert saved == [], "opening and inspecting Week Ahead must not write"
assert len(saved) == 1, "opening and inspecting Week Ahead must not add a write"
for control in (cards.first.locator("[data-week-edit-day]"), edit_week):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
@@ -118,7 +142,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(page.locator("#week-review-days .week-review-day")).to_have_count(7)
first_day = page.locator("#week-review-days .week-review-day").first
expect(first_day).to_contain_text("Ship mobile capture")
expect(first_day).to_contain_text("acme/mobile #41 · Issue · 30 min")
expect(first_day).to_contain_text("acme/mobile #41 · Issue · 45 min")
expect(first_day.locator("code")).to_have_count(0)
edit = first_day.locator("[data-week-edit-day]")
bounds = edit.bounding_box()
@@ -190,7 +214,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
page.locator("#apply-week-capacities").click()
expect(capacity_import).to_be_hidden()
expect(page.locator("#week-review")).to_be_visible()
expect(first_day).to_contain_text("30 of 420 min")
expect(first_day).to_contain_text("45 of 420 min")
page.wait_for_function("() => !document.querySelector('#confirm-week-plan').disabled")
confirm = page.locator("#confirm-week-plan")
@@ -201,7 +225,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
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("10:00–10:30 · 30 min")
expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text("10:00–10:45 · 45 min")
expect(page.locator("#week-calendar-status")).to_contain_text("Planning around imported busy time")
for control in (start, page.locator("#back-to-week-review-from-calendar"), page.locator("#share-week-calendar")):
bounds = control.bounding_box()
+169
View File
@@ -170,6 +170,110 @@ console.log(JSON.stringify({applied,writes,state:week.state(),pending:week.pendi
assert result["pending"]["days"] == result["state"]["days"]
def test_week_controller_previews_and_atomically_applies_a_capacity_safe_reflow():
result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['one','two'],capacity_minutes:45,estimates:{one:30,two:30}},
{plan_date:'2026-08-22',ids:['three'],capacity_minutes:60,estimates:{three:20}},
{plan_date:'2026-08-23',ids:['large'],capacity_minutes:40,estimates:{large:70}},
{plan_date:'2026-08-24',ids:[],capacity_minutes:60,estimates:{}},
{plan_date:'2026-08-25',ids:[],capacity_minutes:0,estimates:{}},
{plan_date:'2026-08-26',ids:[],capacity_minutes:0,estimates:{}},
{plan_date:'2026-08-27',ids:[],capacity_minutes:0,estimates:{}}
]});
const before=week.state();
const preview=week.previewReflow();
const unchanged=week.state();
const applied=week.applyReflow();
console.log(JSON.stringify({before,preview,unchanged,applied,writes,pending:week.pending()}));
""")
assert result["preview"]["blockers"] == []
assert result["preview"]["unscheduled"] == [
{"id": "large", "estimate": 70, "from_date": "2026-08-23"}
]
assert [day["ids"] for day in result["preview"]["days"]] == [
["one"], ["two", "three"], [], [], [], [], []
]
assert result["unchanged"] == result["before"]
assert result["applied"]["sync_pending"] is True
assert result["writes"] == 1
assert result["pending"]["days"] == result["preview"]["days"]
def test_week_controller_reflow_never_places_more_than_five_items_on_a_day():
result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
const estimates=Object.fromEntries(Array.from({length:7},(_,index)=>['item-'+index,10]));
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:Object.keys(estimates),capacity_minutes:120,estimates},
{plan_date:'2026-08-22',ids:[],capacity_minutes:120,estimates:{}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")
assert [day["ids"] for day in result["days"][:2]] == [
["item-0", "item-1", "item-2", "item-3", "item-4"],
["item-5", "item-6"],
]
assert result["unscheduled"] == []
def test_week_controller_reflow_keeps_only_the_first_chronological_assignment():
result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['one','shared'],capacity_minutes:120,estimates:{one:30,shared:20}},
{plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:20,two:30}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")
assert [item for day in result["days"] for item in day["ids"]] == ["one", "shared", "two"]
assert result["unscheduled"] == []
def test_week_controller_reflow_never_backfills_later_work_ahead_of_earlier_work():
result = run_controller("""
const week=createWeekPlan({fetchJson:async()=>({}),localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['large','small'],capacity_minutes:30,estimates:{large:50,small:20}},
{plan_date:'2026-08-22',ids:[],capacity_minutes:60,estimates:{}},
{plan_date:'2026-08-23',ids:[],capacity_minutes:30,estimates:{}}
]});
console.log(JSON.stringify(week.previewReflow()));
""")
assert [day["ids"] for day in result["days"][:3]] == [[], ["large"], ["small"]]
def test_week_controller_blocks_reflow_when_work_has_no_estimate():
result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;values.set(key,value);},removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['known','unknown'],capacity_minutes:90,estimates:{known:30}},
{plan_date:'2026-08-22',ids:[],capacity_minutes:90,estimates:{}}
]});
const before=week.state();const preview=week.previewReflow();const applied=week.applyReflow();
console.log(JSON.stringify({before,preview,applied,after:week.state(),writes}));
""")
assert result["preview"]["blockers"] == [{
"type": "missing-estimate", "id": "unknown", "plan_date": "2026-08-21"
}]
assert result["preview"]["days"] == []
assert result["applied"] is False
assert result["after"] == result["before"]
assert result["writes"] == 0
def test_week_controller_places_new_work_or_explicitly_moves_existing_work_without_duplicates():
result = run_controller("""
const values=new Map();
@@ -291,6 +395,55 @@ console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,ope
assert result["editWeekLabel"] == "Edit week"
def test_week_overview_previews_cancels_and_applies_one_reflow():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',dataset:{},listeners:{},
addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;},querySelectorAll:()=>[],querySelector:()=>null});
for(const selector of ['#week-plan-dates','#week-review-days','#week-review-duplicates','#week-review','#week-review-status',
'#confirm-week-plan','#edit-week-plan','#week-plan-progress','#save-today-plan','#mobile-week-summary','#my-work-action-status',
'#week-offline-snapshot','#retry-week-live','#open-week-capacity-import','#open-week-reflow','#week-reflow-review',
'#week-reflow-days','#week-reflow-unscheduled','#cancel-week-reflow','#apply-week-reflow'])elements.set(selector,makeElement());
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index===0?['one','two']:[],
capacity_minutes:index===0?30:60,estimates:index===0?{one:30,two:30}:{},planned_minutes:index===0?60:0,overloaded:index===0})),
duplicates:[],blockers:[{type:'over-capacity',plan_date:dates[0],minutes:30}],can_confirm:false};
const preview={days:dates.map((date,index)=>({plan_date:date,ids:index<2?[["one"],["two"]][index]:[],capacity_minutes:index===0?30:60,
estimates:index===0?{one:30}:index===1?{two:30}:{}})),blockers:[],unscheduled:[{id:'large',estimate:90,from_date:dates[0]}]};
let applied=0,flushed=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
pass:()=>({}),load:async()=>({}),summary:()=>applied?'2 items across 2 days':'2 items across 1 day',review:()=>review,
pending:()=>false,offline:()=>false,move:()=>true,previewReflow:()=>preview,applyReflow:()=>{applied+=1;return {};},
flush:async()=>{flushed+=1;return {};}};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getItem:id=>({kind:'issue',title:id==='large'?'Large task':id,repository:'r',number:1}),
openPlanner:()=>{},setReviewMode:()=>{},escapeHtml:value=>value,escapeAttribute:value=>value,
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
const offered=!elements.get('#open-week-reflow').hidden;
await elements.get('#open-week-reflow').listeners.click();
const firstPreview={hidden:elements.get('#week-reflow-review').hidden,days:elements.get('#week-reflow-days').innerHTML,
unscheduled:elements.get('#week-reflow-unscheduled').textContent};
await elements.get('#cancel-week-reflow').listeners.click();
const afterCancel={hidden:elements.get('#week-reflow-review').hidden,applied,flushed};
await elements.get('#open-week-reflow').listeners.click();
await elements.get('#apply-week-reflow').listeners.click();
console.log(JSON.stringify({offered,firstPreview,afterCancel,applied,flushed,status:elements.get('#week-review-status').textContent,
reflowHidden:elements.get('#week-reflow-review').hidden}));
""")
assert result["offered"] is True
assert result["firstPreview"]["hidden"] is False
assert "Day 1" in result["firstPreview"]["days"]
assert "1 item · 30 of 30 min" in result["firstPreview"]["days"]
assert result["firstPreview"]["unscheduled"] == "1 item cannot fit: Large task. It will remain in My Work."
assert result["afterCancel"] == {"hidden": True, "applied": 0, "flushed": 0}
assert result["applied"] == 1
assert result["flushed"] == 1
assert result["status"] == "Week Ahead reflowed and saved."
assert result["reflowHidden"] is True
def test_week_overview_confirms_and_starts_the_next_day_early_from_empty_today():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
@@ -1095,6 +1248,22 @@ def test_mobile_week_ahead_review_is_rendered_touch_safe_and_confirmed_explicitl
assert ".week-review-mode .mobile-plan-today-nav" in css
def test_mobile_week_ahead_reflow_has_a_reviewed_touch_safe_flow():
index = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
for element_id in [
"open-week-reflow", "week-reflow-review", "week-reflow-days",
"week-reflow-unscheduled", "cancel-week-reflow", "apply-week-reflow",
]:
assert f'id="{element_id}"' in index
assert ".week-reflow-actions button" in css
rule = css.split(".week-reflow-actions button", 1)[1].split("}", 1)[0]
assert "min-height:48px" in rule
assert ".week-reflow-review" in css
assert "overflow-x:hidden" in css
def test_mobile_week_ahead_has_a_touch_safe_offline_snapshot_notice_and_retry():
index = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()