feat: report Today blockers and move on (Closes #1066)
This commit is contained in:
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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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 & 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
|
||||
|
||||
Reference in new issue
Block a user