Merge pull request 'Report a Today blocker, defer it, and continue' (#1067) from timmy/1066-today-blocker-move-on into main
This commit is contained in:
commit
1bd87fc33e
|
|
@ -122,7 +122,7 @@ as unknown rather than unblocked. Completing any Today item now exposes a 10-sec
|
|||
plan changes, and leaves the already-advanced work session on its current item; expiry, capacity, or a
|
||||
concurrently changed plan is reported without overwriting newer work. Starting a Today work session also
|
||||
stores an account-bound checkpoint on the current device and starts an account-bound actual-time timer for the exact item. The sticky mobile session controls show elapsed time beside the estimate and let the operator pause or resume it. An opt-in, privacy-safe lock-screen notification mirrors the current pause/resume control and adds **Finish current**: its opaque one-shot action is bound to the exact active item, reuses **Done & next** or recap, and never changes the underlying Gitea issue or pull request. Switching items preserves each item's elapsed value, while wall-clock checkpoints keep a running timer accurate through app backgrounding, reloads, and installed-app restarts without double counting. **End session** stops accumulation but retains measured time with the private device data. The recap identifies each item by title and repository, reports per-item estimate variance, and **Save recap & adjust plan** continues into the current ordered Today plan without changing Gitea time entries. Eligible non-zero rows also offer an unchecked **Log Xm to Gitea** control. **Log selected time to Gitea** saves the recap and sends only those corrected durations to each canonical issue or pull request; confirmed account-scoped receipts prevent a completed row from being posted again, while definite failures retain the draft for an explicit retry. If the upstream response is lost after sending, Stackchain marks the row for verification in Gitea instead of risking an automatic duplicate. Actual time appears in planning as an explicit estimate recommendation; it changes only the planning draft until the operator chooses **Save plan** or **Save & start**. After the recap is confirmed, this recommendation handoff remains account-bound on the device through reloads, app restarts, planner cancellation, and failed plan admission. Opening **Plan Today** resumes it without reposting the recap; a successful plan save clears it, while **Discard recap feedback** removes only the handoff and leaves recap history unchanged. The recap and any corrected actual minutes are also saved as an account-bound device draft: an offline save failure can survive a reload and retry with the same idempotent session ID, while another account cannot view it. The draft and timer are cleared only after the account confirms the recap.
|
||||
A running issue or pull request also exposes **Add update** without advancing Today. The operator can type or dictate a progress note, review and explicitly append, replace, or discard the transcript, add up to five photo-evidence items, then save privately or admit the exact comment to durable delivery. Final transcripts—not audio—are bounded to 2,000 characters and isolated by confirmed account and Today item; closing the sheet aborts listening while leaving text and photos usable.
|
||||
A running issue or pull request also exposes **Add update** without advancing Today. The operator can type or dictate a progress note, review and explicitly append, replace, or discard the transcript, add up to five photo-evidence items, then save privately or admit the exact comment to durable delivery. When work is blocked, **Post blocker & move on** requires a future return time, admits the comment before any planning change, defers the item to Later, and continues the existing Today session. An admitted planning transition remains checkpointed for retry, so a storage or Today-removal failure cannot post the blocker twice. Final transcripts—not audio—are bounded to 2,000 characters and isolated by confirmed account and Today item; closing the sheet aborts listening while leaving text and photos usable.
|
||||
After a reload or installed-app
|
||||
restart, **Resume Today** reopens the saved item (or the next surviving item if work changed). In an open
|
||||
assigned issue, the mobile detail sheet renders Markdown checklist items as touch-safe controls and keeps
|
||||
|
|
|
|||
|
|
@ -288,6 +288,9 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.today-progress-evidence { display:grid; gap:8px; min-width:0; margin-top:12px; }
|
||||
.today-progress-evidence .issue-attachment-preview { width:100%; box-sizing:border-box; }
|
||||
.today-progress-evidence .issue-evidence-note textarea { min-height:72px; }
|
||||
.today-progress-blocker { display:grid; gap:8px; margin-top:14px; padding:12px; border:1px solid #875f2a; border-radius:12px; background:#21180d; }
|
||||
.today-progress-blocker input, .today-progress-blocker button { box-sizing:border-box; width:100%; min-height:44px; }
|
||||
.today-progress-blocker p { margin:0; }
|
||||
.today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; }
|
||||
.today-progress-actions button { width:100%; }
|
||||
.today-progress-panel .voice-conversation { margin-top:10px; }
|
||||
|
|
|
|||
|
|
@ -1959,7 +1959,7 @@
|
|||
return {
|
||||
identity:todayWork.identity(item), kind:item.kind, repository:item.repository,
|
||||
number:item.number, label:item.key || item.repository + '#' + item.number,
|
||||
title:item.title || '',
|
||||
title:item.title || '', item,
|
||||
};
|
||||
}
|
||||
const todayProgressView = createTodayProgressView({
|
||||
|
|
@ -1968,6 +1968,7 @@
|
|||
activity:mountTodayProgressActivity(qs,fetchReviewJson,actionHydrator,renderMarkdown),
|
||||
announce:message => { qs('#my-work-action-status').textContent = message; },
|
||||
onAdmitted:() => refreshMyWorkView(),
|
||||
moveOn:(target,until) => detailDefer.deferUntil(target.item,until),
|
||||
});
|
||||
function selectTodayWork() {
|
||||
qs('[data-work-filter="today"]').click();
|
||||
|
|
|
|||
|
|
@ -1601,6 +1601,13 @@
|
|||
</div>
|
||||
</section>
|
||||
<p class="small muted">Save privately on this device, or post to the exact Gitea item. Today keeps running either way.</p>
|
||||
<section class="today-progress-blocker" aria-labelledby="today-progress-blocker-title">
|
||||
<strong id="today-progress-blocker-title">Blocked?</strong>
|
||||
<label for="today-blocker-return-at">Return this item to Later at</label>
|
||||
<input id="today-blocker-return-at" type="datetime-local" />
|
||||
<button id="post-today-blocker" type="button">Post blocker & move on</button>
|
||||
<p class="small muted">Posts this update first, then defers the item and continues Today.</p>
|
||||
</section>
|
||||
<p id="today-progress-status" class="small" role="status" aria-live="polite"></p>
|
||||
<div class="today-progress-actions">
|
||||
<button id="save-today-progress" type="button">Save to this item</button>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,54 @@ maxLength = 2000, maxItems = 20 }) {
|
|||
return admission;
|
||||
}
|
||||
|
||||
return { load, save, discard:identity => save(identity, ''), post };
|
||||
async function postBlocker(target, value, attachments = [], completeEvidence, until, transition) {
|
||||
if (!validTarget(target)) throw new Error('An active Today issue or pull request is required.');
|
||||
if (typeof transition !== 'function') throw new Error('Today planning is unavailable.');
|
||||
let record = read()[target.identity];
|
||||
let admission = { background:true, alreadyAdmitted:true };
|
||||
if (record?.blocker_pending !== true) {
|
||||
const body = String(value ?? record?.body ?? '').trim();
|
||||
if (!body) throw new Error('Describe what is blocking this Today item.');
|
||||
const evidence = (Array.isArray(attachments) ? attachments : [attachments]).filter(Boolean).slice(0, 5);
|
||||
if (!save(target.identity, body, evidence)) {
|
||||
throw new Error('Blocker update could not be saved on this device.');
|
||||
}
|
||||
record = read()[target.identity];
|
||||
if (typeof admit !== 'function') throw new Error('Progress update delivery is unavailable.');
|
||||
admission = await admit({
|
||||
kind:target.kind + '-comment', repository:target.repository, number:target.number,
|
||||
body:record.body, operationId:record.operation_id,
|
||||
...(evidence.length ? {attachments:evidence} : {}),
|
||||
});
|
||||
const drafts = read();
|
||||
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
||||
drafts[target.identity] = { ...drafts[target.identity], blocker_pending:true, blocker_until:String(until || '') };
|
||||
write(drafts);
|
||||
}
|
||||
} else {
|
||||
until = record.blocker_until;
|
||||
}
|
||||
try {
|
||||
if (typeof completeEvidence === 'function') await completeEvidence();
|
||||
} catch (_error) {
|
||||
const error = new Error('Blocker is already queued; photo cleanup is pending.');
|
||||
error.deliveryAdmitted = true;
|
||||
throw error;
|
||||
}
|
||||
if (await transition(target, until) !== true) {
|
||||
const error = new Error('Blocker was posted, but Today could not move on. Retry the planning step.');
|
||||
error.deliveryAdmitted = true;
|
||||
throw error;
|
||||
}
|
||||
const drafts = read();
|
||||
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
||||
delete drafts[target.identity];
|
||||
write(drafts);
|
||||
}
|
||||
return admission;
|
||||
}
|
||||
|
||||
return { load, save, discard:identity => save(identity, ''), post, postBlocker };
|
||||
}
|
||||
|
||||
function createTodayProgressActivity({ fetchJson, createPager, getActions, onActions, surfaceStatus, paint = () => {}, setStatus = () => {} }) {
|
||||
|
|
@ -247,7 +294,7 @@ function mountTodayProgressActivity(qs, fetchJson, actionSource = {}, render) {
|
|||
return activity;
|
||||
}
|
||||
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {} }) {
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null }) {
|
||||
const sheet = qs('#today-progress-sheet');
|
||||
const body = qs('#today-progress-body');
|
||||
const status = qs('#today-progress-status');
|
||||
|
|
@ -310,6 +357,43 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a
|
|||
announce('Progress update saved privately to this Today item.');
|
||||
close();
|
||||
});
|
||||
const blockerButton = qs('#post-today-blocker');
|
||||
blockerButton?.addEventListener('click', async () => {
|
||||
const target = currentTarget();
|
||||
if (!target || target.identity !== openedTarget?.identity) {
|
||||
status.textContent = 'The active Today item changed. Close and open its update again.';
|
||||
return;
|
||||
}
|
||||
const returnInput = qs('#today-blocker-return-at');
|
||||
const wakeAt = new Date(returnInput?.value || '');
|
||||
if (!body.value.trim()) {
|
||||
status.textContent = 'Describe what is blocking this Today item.';
|
||||
body.focus();
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(wakeAt.getTime()) || wakeAt.getTime() <= Date.now()) {
|
||||
status.textContent = 'Choose a valid future return time.';
|
||||
returnInput?.focus?.();
|
||||
return;
|
||||
}
|
||||
blockerButton.disabled = true;
|
||||
status.textContent = 'Posting blocker before moving Today…';
|
||||
try {
|
||||
await photos?.checkpoint?.();
|
||||
const attachments = await photos?.serialize?.() || [];
|
||||
const admission = await progress.postBlocker(
|
||||
target, body.value, attachments, () => photos?.complete?.(), wakeAt.toISOString(), moveOn
|
||||
);
|
||||
onAdmitted(admission);
|
||||
announce('Blocker queued, deferred to Later, and Today moved on.');
|
||||
close();
|
||||
} catch (error) {
|
||||
status.textContent = error.deliveryAdmitted ? error.message : error.message + ' Your blocker remains here; retry.';
|
||||
body.focus();
|
||||
} finally {
|
||||
blockerButton.disabled = false;
|
||||
}
|
||||
});
|
||||
qs('#post-today-progress').addEventListener('click', async () => {
|
||||
const target = currentTarget();
|
||||
if (!target || target.identity !== openedTarget?.identity) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ FEATURE_SOURCES = {
|
|||
"today-timer": (
|
||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
|
||||
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-handoff.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||
"static/today-work.js", "static/today-sync.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user