feat: report Today blockers and move on (Closes #1066)
CI / lint (pull_request) Successful in 2m41s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 2m7s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 06:38:55 +00:00
1 parent 339baab7ee
commit 4009a4a4c1
9 files changed
+265 -5

No files matched your search

@@ -255,3 +255,70 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
fake.shutdown()
fake.server_close()
fake_thread.join(timeout=5)
def test_release_artifact_posts_blocker_defers_and_opens_next_mobile_issue(tmp_path: Path):
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
assert len(archives) == 1
fake = FakeGiteaServer(("127.0.0.1", 0))
thread = threading.Thread(target=fake.serve_forever, daemon=True)
thread.start()
fake_url = f"http://127.0.0.1:{fake.server_port}"
browser_errors: list[str] = []
try:
with release_server(archives[0], tmp_path, fake_url) as origin, sync_playwright() as playwright:
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
page = browser.new_page(viewport={"width": 390, "height": 844}, ignore_https_errors=True)
page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error)))
page.goto(origin + "/", wait_until="networkidle")
page.locator('input[name="device_label"]').fill("Blocker flow phone")
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle")
expect(page.locator("#my-work-status")).to_contain_text("2")
page.locator('[data-mobile-task="work"]').click()
page.locator("#plan-today-available").fill("90")
page.locator("#plan-today-available").press("Tab")
page.locator("#build-today-plan").click()
missing = page.locator("[data-plan-missing-estimate]")
expect(missing).to_have_count(2)
for _ in range(2):
missing.nth(0).fill("30")
missing.nth(0).press("Tab")
page.locator("#save-and-start-today").click()
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
page.locator("#close-issue-sheet").click()
if page.locator("#plan-today-sheet").is_visible():
page.locator("#cancel-plan-today").click()
expect(page.locator("[data-mobile-today-hud]")).to_be_visible()
page.locator("[data-mobile-today-update]").click()
expect(page.locator("#today-progress-sheet")).to_be_visible()
page.locator("#today-progress-body").fill("Blocked waiting for the design owner")
page.locator("#today-blocker-return-at").fill("2099-08-19T09:00")
blocker = page.locator("#post-today-blocker")
bounds = blocker.bounding_box()
assert bounds and bounds["height"] >= 44
blocker.click()
expect(page.locator("#today-progress-sheet")).to_be_hidden()
expect(page.locator("#issue-sheet-title")).to_have_text("Polish desktop filters")
state = page.evaluate("""() => ({
later: JSON.parse(localStorage.getItem('stackchain.later-work.v1.timmy') || '{}'),
today: JSON.parse(localStorage.getItem('stackchain.today-work.v1.timmy') || '[]'),
outbox: JSON.parse(localStorage.getItem('stackchain.authored-outbox.v1') || 'null'),
})""")
assert state["today"] == ["issue:acme/mobile:42:"]
assert state["later"]["issue:acme/mobile:41:"].startswith("2099-08-19T09:00")
comments = [item for item in state["outbox"]["items"] if item["kind"] == "issue-comment"]
assert len(comments) == 1
assert comments[0]["number"] == 41
assert comments[0]["body"] == "Blocked waiting for the design owner"
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
assert browser_errors == []
browser.close()
finally:
fake.shutdown()
fake.server_close()
thread.join(timeout=5)
+2
View File
@@ -74,6 +74,8 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert b"function createConversationPager" not in first.feature_bundles["comment-actions"].runtime_bytes
assert b"function createMobileDeliveryRecovery" not in first.runtime_bytes
assert b"function createMobileDeliveryRecovery" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createDetailDefer" not in first.runtime_bytes
assert b"function createDetailDefer" in first.feature_bundles["today-timer"].runtime_bytes
assert b"gitea_time_logged" not in first.runtime_bytes
assert b"gitea_time_logged" in security_center.runtime_bytes
# Core mobile workflows stay below 99 KiB, including synced Search views and Update decisions.
+96
View File
@@ -179,6 +179,50 @@ process.stdout.write(JSON.stringify({{error,deliveryAdmitted,calls,cleanupCalls,
assert output["after"] == ""
def test_blocker_retry_moves_on_without_admitting_the_comment_twice():
script = f"""
const createProgress = require({json.dumps(str(TODAY_PROGRESS))});
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 admissions=[]; const transitions=[]; let transitionFails=true;
const options={{
storage,getLogin:()=> 'timmy',makeId:()=> 'blocker-op-1',
admit:async message=>{{admissions.push(message);return {{background:true}};}},
}};
let progress=createProgress(options);
const target={{identity:'issue:stackchain/dashboard:1066:',kind:'issue',repository:'stackchain/dashboard',number:1066}};
const until='2026-08-19T09:00:00.000Z';
const transition=async(value,wakeAt)=>{{transitions.push([value.identity,wakeAt]);return !transitionFails;}};
let firstError=''; let admitted=false;
try {{await progress.postBlocker(target,'Waiting for the API owner',[],null,until,transition);}}
catch(caught){{firstError=caught.message;admitted=caught.deliveryAdmitted===true;}}
progress=createProgress(options);
transitionFails=false;
const recovered=await progress.postBlocker(target,undefined,[],null,until,transition);
process.stdout.write(JSON.stringify({{
firstError,admitted,admissions,transitions,recovered,after:progress.load(target.identity),stored:[...values.values()],
}}));
"""
output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})")
assert output["firstError"] == "Blocker was posted, but Today could not move on. Retry the planning step."
assert output["admitted"] is True
assert len(output["admissions"]) == 1
assert output["admissions"][0] == {
"kind": "issue-comment",
"repository": "stackchain/dashboard",
"number": 1066,
"body": "Waiting for the API owner",
"operationId": "blocker-op-1",
}
assert output["transitions"] == [
["issue:stackchain/dashboard:1066:", "2026-08-19T09:00:00.000Z"],
["issue:stackchain/dashboard:1066:", "2026-08-19T09:00:00.000Z"],
]
assert output["recovered"]["alreadyAdmitted"] is True
assert output["after"] == ""
assert output["stored"] == []
def test_progress_rejects_inactive_or_unsupported_targets_without_mutation():
script = f"""
const createProgress = require({json.dumps(str(TODAY_PROGRESS))});
@@ -281,6 +325,54 @@ const view=createView({{
}
def test_progress_sheet_posts_blocker_then_defers_and_continues_exact_active_item():
script = f"""
const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))});
class Element {{
constructor() {{ this.hidden=false;this.value='';this.textContent='';this.open=false;this.disabled=false;this.listeners={{}}; }}
addEventListener(name,callback) {{ this.listeners[name]=callback; }}
click() {{ return this.listeners.click?.(); }}
showModal() {{ this.open=true; }} close() {{ this.open=false; }} focus() {{}}
}}
const selectors=['#today-progress-sheet','#today-progress-body','#today-progress-status','[data-mobile-today-update]',
'#today-progress-target','#cancel-today-progress','#save-today-progress','#post-today-progress',
'#today-blocker-return-at','#post-today-blocker'];
const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()]));
const item={{kind:'issue',repository:'stackchain/dashboard',number:1066,title:'Blocked flow'}};
const target={{identity:'issue:stackchain/dashboard:1066:',kind:'issue',repository:'stackchain/dashboard',number:1066,label:'#1066',title:'Blocked flow',item}};
const calls=[]; const announcements=[];
const progress={{
load:()=>'',save:()=>true,
postBlocker:async(...args)=>{{calls.push(args);return {{background:true}};}},
}};
createView({{
progress,currentTarget:()=>target,qs:selector=>elements[selector],
photos:{{open:async()=>{{}},checkpoint:async()=>{{}},serialize:async()=>[]}},
moveOn:async(value,until)=>{{calls.push(['move-on',value.identity,until]);return true;}},
announce:value=>announcements.push(value),
}});
(async()=>{{
await elements['[data-mobile-today-update]'].click();
elements['#today-progress-body'].value='Waiting for API access';
elements['#today-blocker-return-at'].value='2099-08-19T09:00';
await elements['#post-today-blocker'].click();
process.stdout.write(JSON.stringify({{
posted:calls[0].slice(0,3),transitionResult:await calls[0][5](target,calls[0][4]),
moveCall:calls[1],announcements,closed:!elements['#today-progress-sheet'].open,
}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
output = run_node(script)
assert output["posted"][0]["identity"] == "issue:stackchain/dashboard:1066:"
assert output["posted"][1] == "Waiting for API access"
assert output["posted"][2] == []
assert output["moveCall"][0:2] == ["move-on", "issue:stackchain/dashboard:1066:"]
assert output["moveCall"][2].startswith("2099-08-19T09:00")
assert output["transitionResult"] is True
assert output["announcements"] == ["Blocker queued, deferred to Later, and Today moved on."]
assert output["closed"] is True
def test_recent_activity_loads_the_exact_issue_newest_page_before_rendering():
script = f"""
const {{createActivity}}=require({json.dumps(str(TODAY_PROGRESS))});
@@ -473,6 +565,9 @@ def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
assert 'maxlength="2000"' in html
assert 'id="save-today-progress"' in html
assert 'id="post-today-progress"' in html
assert 'id="today-blocker-return-at"' in html
assert 'id="post-today-blocker"' in html
assert 'Post blocker &amp; move on' in html
assert '<script src="static/today-progress.js"></script>' in html
assert '"static/today-progress.js"' in bundle
assert "workSession.target('continue')" in dashboard
@@ -485,6 +580,7 @@ def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware():
assert "voice:todayProgressVoice" in dashboard
assert "mountTodayProgressActivity" in dashboard
assert "activity:mountTodayProgressActivity" in dashboard
assert "moveOn:(target,until) => detailDefer.deferUntil(target.item,until)" in dashboard
assert "fetchReviewJson,actionHydrator" in dashboard
assert ".today-progress-activity-list" in css
assert ".today-progress-activity-actions button" in css