feat: complete delegated mobile filing handoff (Closes #866)
CI / lint (pull_request) Successful in 1m44s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 59s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-15 03:06:32 +00:00
1 parent 5b954a7a53
commit 6633ba5244
18 files changed
+300 -24

No files matched your search

+18
View File
@@ -78,6 +78,24 @@ const fetchJson=async(url,options={{}})=>{{calls.push({{url,body:options.body||'
}
def test_closed_app_issue_receipt_uses_the_confirmed_canonical_gitea_url():
script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'unowned-link',operationId:'unowned-link',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Backlog capture',body:'',labelIds:[],unassigned:true}};
const store={{claimNext:async()=>item,complete:async()=>{{item=null;}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async url=>url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:19,title:'Backlog capture',assignees:[],url:'https://forge.example/git/o/r/issues/19'}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(result.receipts[0]));}})();
"""
receipt = run_node(script)
assert receipt == {
"id": "unowned-link",
"status": "confirmed",
"kind": "issue",
"url": "https://forge.example/git/o/r/issues/19",
}
def test_closed_app_sync_delivers_desired_blocker_state():
script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
+1 -1
View File
@@ -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-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
+89
View File
@@ -0,0 +1,89 @@
import json
import subprocess
from pathlib import Path
RECEIPT = Path(__file__).parents[1] / "frontend" / "issue-filing-receipt.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
def run_node(script: str) -> dict:
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(result.stdout)
def test_confirmed_delegated_issue_opens_an_accessible_shareable_receipt():
script = f"""
const createReceipt = require({json.dumps(str(RECEIPT))});
const element = () => ({{hidden:true,textContent:'',href:'',focused:false,focus(){{this.focused=true;}},addEventListener(name, fn){{this[name]=fn;}}}});
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
const controller = createReceipt({{...elements,navigator:{{share:async payload=>{{elements.shared=payload;}}}}}});
controller.show({{repository:'stackchain/dashboard',number:866,title:'Finish the filing journey',assignees:['alex'],url:'https://forge.example/git/stackchain/dashboard/issues/866'}}, elements.doneButton);
(async()=>{{await elements.shareButton.click();process.stdout.write(JSON.stringify({{
hidden:elements.root.hidden,key:elements.key.textContent,title:elements.title.textContent,
ownership:elements.ownership.textContent,href:elements.openLink.href,focused:elements.heading.focused,
shared:elements.shared,
}}));}})();
"""
output = run_node(script)
assert output == {
"hidden": False,
"key": "stackchain/dashboard#866",
"title": "Finish the filing journey",
"ownership": "Assigned to @alex.",
"href": "https://forge.example/git/stackchain/dashboard/issues/866",
"focused": True,
"shared": {
"title": "stackchain/dashboard#866 · Finish the filing journey",
"text": "Finish the filing journey",
"url": "https://forge.example/git/stackchain/dashboard/issues/866",
},
}
def test_unassigned_receipt_copies_link_and_escape_restores_launch_focus():
script = f"""
const createReceipt = require({json.dumps(str(RECEIPT))});
const element = () => ({{hidden:true,textContent:'',href:'',focusCount:0,focus(){{this.focusCount++;}},addEventListener(name, fn){{this[name]=fn;}}}});
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
const copied=[];const launcher=element();
const controller=createReceipt({{...elements,navigator:{{}},clipboard:{{writeText:async value=>copied.push(value)}}}});
controller.show({{repository:'stackchain/dashboard',number:867,title:'Unowned follow-up',assignees:[],url:'https://forge.example/git/stackchain/dashboard/issues/867'}},launcher);
(async()=>{{await elements.shareButton.click();elements.root.keydown({{key:'Escape',preventDefault(){{elements.prevented=true;}}}});process.stdout.write(JSON.stringify({{
ownership:elements.ownership.textContent,copied,status:elements.status.textContent,hidden:elements.root.hidden,
restored:launcher.focusCount,prevented:elements.prevented,
}}));}})();
"""
output = run_node(script)
assert output == {
"ownership": "Created with no owner.",
"copied": ["https://forge.example/git/stackchain/dashboard/issues/867"],
"status": "Issue link copied.",
"hidden": True,
"restored": 1,
"prevented": True,
}
def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
index = INDEX.read_text()
css = CSS.read_text()
dashboard = DASHBOARD.read_text()
assert 'id="issue-filing-receipt" role="dialog" aria-modal="true"' in index
assert 'id="issue-filing-receipt-heading"' in index
assert 'id="issue-filing-receipt-open"' in index
assert 'id="issue-filing-receipt-share"' in index
assert 'id="issue-filing-receipt-another"' in index
assert 'id="issue-filing-receipt-done"' in index
assert '<script src="static/issue-filing-receipt.js"></script>' in index
assert '.issue-filing-receipt-panel' in css
assert 'padding-bottom:calc(18px + env(safe-area-inset-bottom))' in css
assert '.issue-filing-receipt-actions' in css
assert 'min-height:44px' in css
assert 'createIssueFilingReceipt({' in dashboard
assert "issueFilingReceipt.show(confirmed, qs('#new-issue'))" in dashboard
+1 -1
View File
@@ -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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -137,4 +137,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-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
+1 -1
View File
@@ -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-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -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-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -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-v101" in source
assert "stackchain-dashboard-shell-v102" 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
+43 -12
View File
@@ -120,11 +120,11 @@ async function dispatchMessage(data, ports = []) {{
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
if (pending) await pending;
}}
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null) {{
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null) {{
let pending;
listeners.notificationclick({{
action,
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId}}, close: () => {{ state.notificationClosed = true; }}}},
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId, ...(url ? {{url}} : {{}})}}, close: () => {{ state.notificationClosed = true; }}}},
waitUntil: promise => {{ pending = promise; }},
}});
if (pending) await pending;
@@ -150,7 +150,7 @@ async function dispatchPush(payload) {{
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -159,14 +159,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-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -174,7 +174,7 @@ 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -182,14 +182,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -198,21 +198,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-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -460,6 +460,36 @@ def test_opted_in_background_sync_notifies_privately_and_receipt_tap_focuses_rou
assert result["notificationClosed"] is True
def test_created_issue_receipt_tap_opens_the_confirmed_canonical_gitea_url():
result = run_worker_scenario(
"""
const issueUrl = 'https://forge.example/git/stackchain/api/issues/44';
state.receiptLogin = 'timmy';
state.flushResult = {login:'timmy', receipts:[
{id:'capture-1',status:'confirmed',kind:'issue',url:issueUrl},
]};
state.clientList = [{url:'https://forge.example/dashboard/', navigate:async function(url){ this.url=url; }, focus:async function(){ state.focused.push(this.url); }}];
await dispatchSync('stackchain-issue-outbox-v1');
await dispatchNotificationClick('', '', null, 'stackchain-delivery-capture-1', issueUrl);
process.stdout.write(JSON.stringify(state));
"""
)
assert result["notifications"] == [{
"title": "Queued issue created",
"options": {
"body": "Tap to open it in Stackchain.",
"tag": "stackchain-delivery-capture-1",
"data": {"url": "https://forge.example/git/stackchain/api/issues/44"},
},
}]
assert result["focused"] == [
"https://forge.example/git/stackchain/api/issues/44"
]
assert result["opened"] == []
assert result["notificationClosed"] is True
def test_inbound_push_renders_generic_update_and_tap_opens_existing_workflow():
result = run_worker_scenario(
"""
@@ -764,7 +794,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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/queue-today.js'" in source
@@ -873,6 +903,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/mobile-composer-viewport.js",
"/dashboard/static/mention-composer.js",
"/dashboard/static/push-notifications.js",
"/dashboard/static/issue-filing-receipt.js",
"/dashboard/static/background-issue-sync.js",
}
+1 -1
View File
@@ -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-v101';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v102';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/today-sync.js'" in source