feat: guide mobile delivery recovery (Closes #1014)
This commit is contained in:
1 parent
c0a02a05d8
commit
1e4f101219
9 files changed
+353
-12
No files matched your search
@@ -379,8 +379,8 @@ async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_w
|
||||
assert '.delivery-center { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;' in html
|
||||
assert '.delivery-center button { min-height:44px;' in html
|
||||
assert "counts.delivery = draftInbox.partition(lastDrafts).actionable;" in html
|
||||
assert "openDelivery: openDeliveryRecovery" in html
|
||||
assert "deliveryCenterTitle.focus({preventScroll:true});" in html
|
||||
assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
|
||||
assert "getItems: () => draftInbox.partition(lastDrafts).deliveries" in html
|
||||
assert "authoritativePhases:['delivery']" in html
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RECOVERY = ROOT / "frontend" / "mobile-delivery-recovery.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
result = subprocess.run(
|
||||
["node", "-e", script],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_recovery_orders_deliveries_by_safest_required_intervention():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
const recovery = createRecovery({{
|
||||
getItems: () => [
|
||||
{{outbox_id:'waiting', delivery_state:'waiting', title:'Waiting'}},
|
||||
{{outbox_id:'uncertain', delivery_state:'uncertain', title:'Uncertain'}},
|
||||
{{outbox_id:'attention', status:'attention', title:'Attention'}},
|
||||
{{outbox_id:'authorization', status:'authorization', title:'Authorization'}},
|
||||
],
|
||||
}});
|
||||
process.stdout.write(JSON.stringify(recovery.snapshot()));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"count": 4,
|
||||
"current": {
|
||||
"id": "authorization",
|
||||
"state": "authorization",
|
||||
"primary": "Authorize & send",
|
||||
"position": 1,
|
||||
"total": 4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_recovery_advances_only_after_the_current_delivery_is_resolved():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
(async () => {{
|
||||
let items = [
|
||||
{{outbox_id:'authorize', status:'authorization', title:'Approve review'}},
|
||||
{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}},
|
||||
];
|
||||
let resolves = false;
|
||||
const recovery = createRecovery({{
|
||||
getItems: () => items,
|
||||
activate: async item => {{
|
||||
if (resolves) items = items.filter(candidate => candidate.outbox_id !== item.outbox_id);
|
||||
return resolves;
|
||||
}},
|
||||
}});
|
||||
recovery.open();
|
||||
const retained = await recovery.activate();
|
||||
resolves = true;
|
||||
const advanced = await recovery.activate();
|
||||
process.stdout.write(JSON.stringify({{retained, advanced}}));
|
||||
}})();
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"retained": {
|
||||
"count": 2,
|
||||
"current": {
|
||||
"id": "authorize",
|
||||
"state": "authorization",
|
||||
"primary": "Authorize & send",
|
||||
"position": 1,
|
||||
"total": 2,
|
||||
},
|
||||
},
|
||||
"advanced": {
|
||||
"count": 1,
|
||||
"current": {
|
||||
"id": "retry",
|
||||
"state": "waiting",
|
||||
"primary": "Retry now",
|
||||
"position": 1,
|
||||
"total": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_recovery_renders_delivery_context_and_hands_off_when_cleared():
|
||||
script = f"""
|
||||
const createRecovery = require({json.dumps(str(RECOVERY))});
|
||||
(async () => {{
|
||||
let items = [{{
|
||||
outbox_id:'close-7', status:'authorization', outbox_kind:'issue-close',
|
||||
title:'Close resolved incident', repository:'stackchain/ops',
|
||||
details:'#7', last_attempt_error:'Fresh authorization expired',
|
||||
}}];
|
||||
let completed = 0;
|
||||
const element = () => ({{textContent:'', hidden:false}});
|
||||
const elements = {{
|
||||
dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}},
|
||||
title: element(), destination: element(), reason: element(), progress: element(),
|
||||
action: element(), status: element(),
|
||||
}};
|
||||
const recovery = createRecovery({{
|
||||
getItems: () => items,
|
||||
activate: async () => {{items = []; return true}},
|
||||
onComplete: () => {{completed += 1}},
|
||||
elements,
|
||||
}});
|
||||
const opened = recovery.open();
|
||||
const rendered = {{
|
||||
opened, dialogOpen:elements.dialog.open, title:elements.title.textContent,
|
||||
destination:elements.destination.textContent, reason:elements.reason.textContent,
|
||||
progress:elements.progress.textContent, action:elements.action.textContent,
|
||||
}};
|
||||
const cleared = await recovery.activate();
|
||||
process.stdout.write(JSON.stringify({{rendered, cleared, completed, dialogOpen:elements.dialog.open}}));
|
||||
}})();
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"rendered": {
|
||||
"opened": "opened",
|
||||
"dialogOpen": True,
|
||||
"title": "Close resolved incident",
|
||||
"destination": "stackchain/ops · #7",
|
||||
"reason": "Fresh authorization expired",
|
||||
"progress": "Delivery 1 of 1 · Authorization required",
|
||||
"action": "Authorize & close",
|
||||
},
|
||||
"cleared": {"count": 0, "current": None},
|
||||
"completed": 1,
|
||||
"dialogOpen": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_packages_phone_safe_guided_delivery_recovery():
|
||||
html = await dashboard()
|
||||
service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
|
||||
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
|
||||
|
||||
assert '<dialog id="mobile-delivery-recovery"' in html
|
||||
assert 'aria-labelledby="mobile-delivery-recovery-title"' in html
|
||||
assert 'id="mobile-delivery-recovery-action"' in html
|
||||
assert '<script src="static/mobile-delivery-recovery.js"></script>' in html
|
||||
assert "const mobileDeliveryRecovery = createMobileDeliveryRecovery({" in html
|
||||
assert "openDelivery: () => mobileDeliveryRecovery.open()" in html
|
||||
assert "mobileStartDay.completePhase('delivery')" in html
|
||||
assert "BASE + 'static/mobile-delivery-recovery.js'" in service_worker
|
||||
assert '"static/mobile-delivery-recovery.js"' in bundle
|
||||
assert ".mobile-delivery-recovery-panel" in html
|
||||
assert "min-height:48px" in html
|
||||
assert "env(safe-area-inset-bottom)" in html
|
||||
@@ -1043,6 +1043,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/mobile-task-dock.js",
|
||||
"/dashboard/static/mobile-work-entry.js",
|
||||
"/dashboard/static/mobile-queue-launcher.js",
|
||||
"/dashboard/static/mobile-delivery-recovery.js",
|
||||
"/dashboard/static/mobile-start-day.js",
|
||||
"/dashboard/static/update-triage-session.js",
|
||||
"/dashboard/static/update-review-handoff.js",
|
||||
|
||||
Reference in new issue
Block a user