feat: add mobile checklist steps inline (Closes #915)
This commit is contained in:
1 parent
e7a4eb3331
commit
3f559a50ea
19 files changed
+272
-21
No files matched your search
@@ -47,3 +47,38 @@ process.stdout.write(JSON.stringify({{missing,duplicate}}));
|
||||
assert output["missing"]["conflicts"] == [{"label": "Ship", "reason": "missing"}]
|
||||
assert output["duplicate"]["body"] is None
|
||||
assert output["duplicate"]["conflicts"] == [{"label": "Ship", "reason": "ambiguous"}]
|
||||
|
||||
|
||||
def test_merge_appends_a_locally_added_step_to_remote_edits_without_reverting_them():
|
||||
script = f"""
|
||||
const mergeChecklistConflict = require({json.dumps(str(MERGER))});
|
||||
const result = mergeChecklistConflict({{
|
||||
baseBody:'Intro\\n- [ ] Build',
|
||||
localBody:'Intro\\n- [ ] Build\\n- [ ] Verify rollback',
|
||||
remoteBody:'Updated intro\\n- [x] Build\\n- [ ] Notify support',
|
||||
}});
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == {
|
||||
"body": "Updated intro\n- [x] Build\n- [ ] Notify support\n- [ ] Verify rollback",
|
||||
"changes": [{"label": "Verify rollback", "checked": False, "added": True}],
|
||||
"conflicts": [],
|
||||
}
|
||||
|
||||
|
||||
def test_merge_rejects_an_ambiguous_locally_added_duplicate():
|
||||
script = f"""
|
||||
const mergeChecklistConflict = require({json.dumps(str(MERGER))});
|
||||
const result = mergeChecklistConflict({{
|
||||
baseBody:'- [ ] Build',
|
||||
localBody:'- [ ] Build\\n- [ ] Verify rollback\\n- [ ] verify rollback ',
|
||||
remoteBody:'- [x] Build',
|
||||
}});
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["body"] is None
|
||||
assert output["conflicts"] == [{"label": "Verify rollback", "reason": "ambiguous"}]
|
||||
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
|
||||
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||
assert '.update-reply-actions button { min-height:44px;' in html
|
||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
assert "stackchain-dashboard-shell-v107" in worker
|
||||
assert "stackchain-dashboard-shell-v108" in worker
|
||||
@@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
@@ -182,4 +182,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
||||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v107" in worker
|
||||
assert "stackchain-dashboard-shell-v108" in worker
|
||||
@@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
||||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v107" in worker
|
||||
assert "stackchain-dashboard-shell-v108" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
||||
@@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||
assert "promptStorage:localStorage" in dashboard
|
||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v107" in worker
|
||||
assert "stackchain-dashboard-shell-v108" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
@@ -283,4 +283,4 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
|
||||
assert ".mobile-start-day-finish { min-height:44px;" in html
|
||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||
assert "stackchain-dashboard-shell-v107" in service_worker
|
||||
assert "stackchain-dashboard-shell-v108" in service_worker
|
||||
@@ -2225,6 +2225,26 @@ async def test_mobile_issue_detail_toggles_checklist_with_touch_safe_recovery():
|
||||
assert '.checklist-pending .task-list-toggle { opacity:.65;' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_issue_detail_adds_a_checklist_step_inline_with_accessible_touch_controls():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="open-add-checklist-step"' in html
|
||||
assert 'id="add-checklist-step-form"' in html
|
||||
assert 'id="add-checklist-step" type="text"' in html
|
||||
assert 'id="save-checklist-step" type="submit"' in html
|
||||
assert 'id="cancel-checklist-step"' in html
|
||||
assert 'id="add-checklist-step-status" class="small" aria-live="assertive"' in html
|
||||
assert "(selectedIssueOffline ? issueController.queueAddedTask : issueController.addTask).call(" in html
|
||||
assert "applyIssueContent(state.item, state.detail, state.offline ? result.detail : result)" in html
|
||||
assert "Checklist step queued. Pending sync." in html
|
||||
assert "qs('#add-checklist-step').focus()" in html
|
||||
assert "qs('#open-add-checklist-step').focus()" in html
|
||||
assert '.checklist-add button, .checklist-add input { min-height:44px;' in html
|
||||
assert '#issue-sheet.read-only .checklist-add { display:none;' in html
|
||||
assert 'grid-template-columns:minmax(0,1fr) auto auto' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_drafts_reviews_and_retries_an_unambiguous_checklist_conflict():
|
||||
html = await dashboard()
|
||||
@@ -2447,6 +2467,77 @@ Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.string
|
||||
assert output["confirmed"]["updated_at"] == "2026-08-15T10:01:00Z"
|
||||
|
||||
|
||||
def test_issue_sheet_adds_a_validated_unchecked_step_without_replacing_existing_content():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
const calls = [];
|
||||
const controller = createIssueSheet({{
|
||||
storage:null,
|
||||
fetchJson:(url, options) => {{
|
||||
calls.push({{url, body:JSON.parse(options.body)}});
|
||||
return Promise.resolve({{number:17,title:'Release',body:'Intro\\n\\n- [x] Build\\n- [ ] Verify rollback',updated_at:'new'}});
|
||||
}},
|
||||
}});
|
||||
const item = {{repository:'stackchain/api',number:17}};
|
||||
const detail = {{title:'Release',body:'Intro\\n\\n- [x] Build',updated_at:'old'}};
|
||||
Promise.all([
|
||||
controller.addTask(item, detail, ' Verify rollback '),
|
||||
controller.addTask(item, detail, ' build ').catch(error => ({{error:error.message}})),
|
||||
controller.addTask(item, detail, ' ').catch(error => ({{error:error.message}})),
|
||||
]).then(results => process.stdout.write(JSON.stringify({{calls,results}})));
|
||||
"""
|
||||
output = json.loads(subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
).stdout)
|
||||
|
||||
assert output["calls"] == [{
|
||||
"url": "api/v1/repos/stackchain/api/issues/17/content",
|
||||
"body": {
|
||||
"title": "Release",
|
||||
"body": "Intro\n\n- [x] Build\n- [ ] Verify rollback",
|
||||
"expected_updated_at": "old",
|
||||
},
|
||||
}]
|
||||
assert output["results"][0]["body"].endswith("- [ ] Verify rollback")
|
||||
assert output["results"][1] == {"error": "That checklist step already exists."}
|
||||
assert output["results"][2] == {"error": "Enter a checklist step."}
|
||||
|
||||
|
||||
def test_offline_issue_sheet_adds_a_step_only_after_durable_admission():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
let admit; const queued = [];
|
||||
const controller = createIssueSheet({{
|
||||
storage:null,
|
||||
enqueueDurably:message => {{ queued.push(message); return new Promise(resolve => admit=resolve); }},
|
||||
}});
|
||||
const item = {{repository:'stackchain/api',number:17}};
|
||||
const detail = {{title:'Release',body:'Intro',updated_at:'old'}};
|
||||
let settled = false;
|
||||
const pending = controller.queueAddedTask(item, detail, 'Verify rollback').then(result => {{settled=true;return result;}});
|
||||
const before = settled;
|
||||
admit({{item:{{id:'add-step'}}}});
|
||||
pending.then(result => process.stdout.write(JSON.stringify({{before,queued,result}})));
|
||||
"""
|
||||
output = json.loads(subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
).stdout)
|
||||
|
||||
assert output["before"] is False
|
||||
assert output["queued"] == [{
|
||||
"kind": "issue-content", "repository": "stackchain/api", "number": 17,
|
||||
"title": "Release", "baseBody": "Intro", "body": "Intro\n- [ ] Verify rollback",
|
||||
"expectedUpdatedAt": "old",
|
||||
}]
|
||||
assert output["result"] == {
|
||||
"queued": True,
|
||||
"detail": {
|
||||
"title": "Release", "body": "Intro\n- [ ] Verify rollback",
|
||||
"updated_at": "old", "checklist_pending": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_issue_content_edit_is_single_flight_and_keeps_scoped_draft_until_confirmed():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
|
||||
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
@@ -155,7 +155,7 @@ async function dispatchPush(payload) {{
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
@@ -164,14 +164,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_offline_review_next_ships_today_completion_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -179,22 +179,32 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/issue-sheet.js'" in source
|
||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
||||
|
||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/later-picker.js'" in source
|
||||
|
||||
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -203,21 +213,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||
|
||||
|
||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
@@ -881,7 +891,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
||||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
@@ -961,6 +971,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/issue-attachment.js",
|
||||
"/dashboard/static/issue-filing-review.js",
|
||||
"/dashboard/static/issue-sheet.js",
|
||||
"/dashboard/static/checklist-conflict.js",
|
||||
"/dashboard/static/create-issue-sheet.js",
|
||||
"/dashboard/static/create-and-start.js",
|
||||
"/dashboard/static/assign-and-start.js",
|
||||
|
||||
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
|
||||
def test_readiness_runtime_is_available_in_offline_shell():
|
||||
service_worker = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v107';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v108';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v107" in source
|
||||
assert "stackchain-dashboard-shell-v108" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user