Merge pull request 'Refresh Human Gates atomically on every reopen' (#1434) from timmy/1433-refresh-human-gates-reopen into main
This commit is contained in:
commit
1e9c3444fa
|
|
@ -13,6 +13,7 @@ function createHumanGates(options = {}) {
|
||||||
let loadEpoch = 0;
|
let loadEpoch = 0;
|
||||||
const decisionKeys = new Map();
|
const decisionKeys = new Map();
|
||||||
let decisionFlight = null;
|
let decisionFlight = null;
|
||||||
|
let openFlight = null;
|
||||||
let onChange = options.onChange;
|
let onChange = options.onChange;
|
||||||
|
|
||||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
||||||
|
|
@ -203,7 +204,19 @@ function createHumanGates(options = {}) {
|
||||||
function open() {
|
function open() {
|
||||||
location.hash = '#/my-work/human-gates';
|
location.hash = '#/my-work/human-gates';
|
||||||
if (nodes.panel) nodes.panel.hidden = false;
|
if (nodes.panel) nodes.panel.hidden = false;
|
||||||
return load().then(() => reviewNext());
|
if (openFlight) return openFlight;
|
||||||
|
const operation = (async () => {
|
||||||
|
await load();
|
||||||
|
reviewSnapshot = queue.items.slice();
|
||||||
|
reviewIndex = 0;
|
||||||
|
return reviewNext();
|
||||||
|
})();
|
||||||
|
openFlight = operation;
|
||||||
|
const clearFlight = () => {
|
||||||
|
if (openFlight === operation) openFlight = null;
|
||||||
|
};
|
||||||
|
operation.then(clearFlight, clearFlight);
|
||||||
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,7 @@ function createProgressiveHumanGates(options = {}) {
|
||||||
|
|
||||||
async function start(force = false) {
|
async function start(force = false) {
|
||||||
if (!force && location.hash !== '#/my-work/human-gates') return false;
|
if (!force && location.hash !== '#/my-work/human-gates') return false;
|
||||||
if (started) {
|
if (started) return controller.open().then(() => true);
|
||||||
if (nodes.panel) nodes.panel.hidden = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (startFlight) return startFlight;
|
if (startFlight) return startFlight;
|
||||||
startFlight = (async () => {
|
startFlight = (async () => {
|
||||||
const identity = await getIdentity();
|
const identity = await getIdentity();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/private-data-registry.js');
|
importScripts(BASE + 'static/private-data-registry.js');
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v144';
|
const CACHE = 'stackchain-dashboard-shell-v145';
|
||||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||||
|
|
|
||||||
95
tests/e2e/test_human_gates_reopen_release.py
Normal file
95
tests/e2e/test_human_gates_reopen_release.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
|
||||||
|
pytest.skip("packaged Human Gates journey runs only in its gated CI job", allow_module_level=True)
|
||||||
|
pytest.importorskip("playwright.sync_api")
|
||||||
|
from playwright.sync_api import expect, sync_playwright
|
||||||
|
|
||||||
|
from fake_gitea import FakeGiteaServer
|
||||||
|
from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)])
|
||||||
|
def test_release_artifact_reopens_human_gates_with_one_fresh_mobile_snapshot(
|
||||||
|
tmp_path: Path, width: int, height: int
|
||||||
|
):
|
||||||
|
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
||||||
|
assert len(archives) == 1, "browser job must download exactly one assembled release archive"
|
||||||
|
|
||||||
|
fake = FakeGiteaServer(("127.0.0.1", 0))
|
||||||
|
fake_thread = threading.Thread(target=fake.serve_forever, daemon=True)
|
||||||
|
fake_thread.start()
|
||||||
|
current = {"gate": "g1"}
|
||||||
|
list_requests: list[str] = []
|
||||||
|
browser_errors: list[str] = []
|
||||||
|
|
||||||
|
def gate(gate_id: str) -> dict:
|
||||||
|
return {
|
||||||
|
"id": gate_id,
|
||||||
|
"title": "First candidate" if gate_id == "g1" else "Fresh candidate",
|
||||||
|
"project": "stackchain/stackchain-dashboard",
|
||||||
|
"candidate_hash": "a1" if gate_id == "g1" else "b2",
|
||||||
|
"revision": 1,
|
||||||
|
"priority": 5,
|
||||||
|
"checks": [],
|
||||||
|
"artifacts": [],
|
||||||
|
"links": [],
|
||||||
|
"provenance": {},
|
||||||
|
"history": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with release_server(
|
||||||
|
archives[0], tmp_path, f"http://127.0.0.1:{fake.server_port}"
|
||||||
|
) as origin, sync_playwright() as playwright:
|
||||||
|
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
|
||||||
|
context = browser.new_context(
|
||||||
|
viewport={"width": width, "height": height}, ignore_https_errors=True
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error)))
|
||||||
|
|
||||||
|
def human_gates_route(route):
|
||||||
|
path = route.request.url.split("?", 1)[0]
|
||||||
|
if path.endswith("/api/v1/human-gates"):
|
||||||
|
list_requests.append(current["gate"])
|
||||||
|
payload = {"pending_count": 1, "items": [gate(current["gate"])]}
|
||||||
|
else:
|
||||||
|
payload = gate(path.rsplit("/", 1)[-1])
|
||||||
|
route.fulfill(status=200, content_type="application/json", body=json.dumps(payload))
|
||||||
|
|
||||||
|
page.route("**/api/v1/human-gates**", human_gates_route)
|
||||||
|
page.goto(origin + "/", wait_until="networkidle")
|
||||||
|
page.locator('input[name="device_label"]').fill("Human Gates release phone")
|
||||||
|
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
||||||
|
page.locator("#submit-sign-in").click()
|
||||||
|
page.wait_for_url(origin + "/", wait_until="networkidle")
|
||||||
|
|
||||||
|
page.evaluate("document.querySelector('#open-human-gates').click()")
|
||||||
|
expect(page.locator("#human-gates")).to_be_visible()
|
||||||
|
expect(page.locator("#human-gate-detail")).to_contain_text("First candidate")
|
||||||
|
page.evaluate("document.querySelector('#close-human-gates').click()")
|
||||||
|
|
||||||
|
before_reopen = len(list_requests)
|
||||||
|
current["gate"] = "g2"
|
||||||
|
page.evaluate("document.querySelector('#open-human-gates').click()")
|
||||||
|
expect(page.locator("#human-gate-detail")).to_contain_text("Fresh candidate")
|
||||||
|
expect(page.locator("#human-gates-list")).to_contain_text("Fresh candidate")
|
||||||
|
assert len(list_requests) == before_reopen + 1
|
||||||
|
assert page.evaluate("window.innerWidth") == width
|
||||||
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
bounds = page.locator('[data-human-gate-id="g2"]').bounding_box()
|
||||||
|
assert bounds and bounds["height"] >= 44
|
||||||
|
assert not browser_errors
|
||||||
|
browser.close()
|
||||||
|
finally:
|
||||||
|
fake.shutdown()
|
||||||
|
fake.server_close()
|
||||||
|
fake_thread.join(timeout=5)
|
||||||
|
|
@ -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 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||||
assert '.update-reply-actions button { min-height:44px;' in html
|
assert '.update-reply-actions button { min-height:44px;' in html
|
||||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
assert "stackchain-dashboard-shell-v144" in worker
|
assert "stackchain-dashboard-shell-v145" in worker
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,7 @@ process.stdout.write(JSON.stringify({{
|
||||||
assert ".following-disposition-mode" in css
|
assert ".following-disposition-mode" in css
|
||||||
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
||||||
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
||||||
assert "stackchain-dashboard-shell-v144" in service_worker
|
assert "stackchain-dashboard-shell-v145" in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,92 @@ const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:()
|
||||||
assert [item["id"] for item in output["snapshot"]["items"]] == ["g2"]
|
assert [item["id"] for item in output["snapshot"]["items"]] == ["g2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_concurrent_reopen_calls_share_one_fresh_list_request():
|
||||||
|
output = run_node(r"""
|
||||||
|
let listCalls=0, releaseList;
|
||||||
|
const pending=new Promise(resolve=>releaseList=resolve);
|
||||||
|
const item={id:'g1',title:'Candidate',candidate_hash:'a1',revision:1,checks:[]};
|
||||||
|
const gates=createHumanGates({
|
||||||
|
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,
|
||||||
|
nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''},
|
||||||
|
fetchJson:async path=>{
|
||||||
|
if(path.endsWith('/g1')) return item;
|
||||||
|
listCalls += 1;
|
||||||
|
await pending;
|
||||||
|
return {pending_count:1,items:[item]};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(async()=>{
|
||||||
|
const first=gates.open();
|
||||||
|
const second=gates.open();
|
||||||
|
releaseList();
|
||||||
|
const reviewed=await Promise.all([first,second]);
|
||||||
|
process.stdout.write(JSON.stringify({listCalls,ids:reviewed.map(item=>item.id)}));
|
||||||
|
})();
|
||||||
|
""")
|
||||||
|
assert output == {"listCalls": 1, "ids": ["g1", "g1"]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_failed_reopen_clears_single_flight_and_can_retry():
|
||||||
|
output = run_node(r"""
|
||||||
|
let listCalls=0;
|
||||||
|
const item={id:'g1',title:'Recovered',candidate_hash:'a1',revision:1,checks:[]};
|
||||||
|
const gates=createHumanGates({
|
||||||
|
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,
|
||||||
|
nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''},
|
||||||
|
fetchJson:async path=>{
|
||||||
|
if(path.endsWith('/g1')) return item;
|
||||||
|
listCalls += 1;
|
||||||
|
if(listCalls === 1) throw new Error('offline');
|
||||||
|
return {pending_count:1,items:[item]};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(async()=>{
|
||||||
|
let firstError='';
|
||||||
|
try { await gates.open(); } catch(error) { firstError=error.message; }
|
||||||
|
const recovered=await gates.open();
|
||||||
|
process.stdout.write(JSON.stringify({firstError,listCalls,recovered:recovered.id}));
|
||||||
|
})();
|
||||||
|
""")
|
||||||
|
assert output == {"firstError": "offline", "listCalls": 2, "recovered": "g1"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_reopen_refreshes_queue_and_starts_a_fresh_atomic_review_snapshot():
|
||||||
|
output = run_node(r"""
|
||||||
|
const first={id:'g1',title:'First candidate',project:'p/one',candidate_hash:'a1',revision:1,checks:[]};
|
||||||
|
const second={id:'g2',title:'Second candidate',project:'p/two',candidate_hash:'b2',revision:1,checks:[]};
|
||||||
|
let listCalls=0;
|
||||||
|
const nodes={count:{},list:{innerHTML:''},status:{},panel:{hidden:true},detail:{innerHTML:''}};
|
||||||
|
const gates=createHumanGates({
|
||||||
|
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''},
|
||||||
|
fetchJson:async path=>{
|
||||||
|
if(path.endsWith('/g1')) return first;
|
||||||
|
if(path.endsWith('/g2')) return second;
|
||||||
|
listCalls += 1;
|
||||||
|
return {pending_count:1,items:[listCalls === 1 ? first : second]};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(async()=>{
|
||||||
|
await gates.open();
|
||||||
|
await new Promise(resolve=>setTimeout(resolve,0));
|
||||||
|
const before=gates.current().id;
|
||||||
|
await gates.open();
|
||||||
|
const selected=gates.select('g2');
|
||||||
|
await new Promise(resolve=>setTimeout(resolve,0));
|
||||||
|
process.stdout.write(JSON.stringify({
|
||||||
|
before,listCalls,selected:selected.id,current:gates.current().id,
|
||||||
|
listHtml:nodes.list.innerHTML,detailHtml:nodes.detail.innerHTML,
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
""")
|
||||||
|
assert output["before"] == "g1"
|
||||||
|
assert output["listCalls"] == 2
|
||||||
|
assert output["selected"] == "g2"
|
||||||
|
assert output["current"] == "g2"
|
||||||
|
assert "Second candidate" in output["listHtml"]
|
||||||
|
assert "Second candidate" in output["detailHtml"]
|
||||||
|
|
||||||
|
|
||||||
def test_selecting_a_queue_card_opens_that_exact_gate():
|
def test_selecting_a_queue_card_opens_that_exact_gate():
|
||||||
output = run_node(r"""
|
output = run_node(r"""
|
||||||
const details={
|
const details={
|
||||||
|
|
@ -306,7 +392,7 @@ def test_human_gate_mobile_shell_and_deep_route_are_wired():
|
||||||
assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard
|
assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard
|
||||||
assert "counts.gate = queueCounts.gate" in dashboard
|
assert "counts.gate = queueCounts.gate" in dashboard
|
||||||
assert "gate:preparationItems.gate || []" in dashboard
|
assert "gate:preparationItems.gate || []" in dashboard
|
||||||
assert "stackchain-dashboard-shell-v144" in WORKER.read_text()
|
assert "stackchain-dashboard-shell-v145" in WORKER.read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
||||||
|
|
@ -346,6 +432,55 @@ const app=createProgressiveHumanGates({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_reopen_refreshes_the_review_session_before_hydration():
|
||||||
|
script = f"""
|
||||||
|
const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))});
|
||||||
|
const listeners={{}};
|
||||||
|
const nodes={{
|
||||||
|
'#human-gates-count':{{}}, '#human-gates-list':{{innerHTML:'',addEventListener(){{}}}},
|
||||||
|
'#human-gates-status':{{}}, '#human-gates':{{hidden:true}},
|
||||||
|
'#human-gate-detail':{{innerHTML:'',addEventListener(){{}},querySelectorAll:()=>[]}},
|
||||||
|
'#open-human-gates':{{addEventListener:(name,fn)=>listeners.open=fn}},
|
||||||
|
'#close-human-gates':{{addEventListener:(name,fn)=>listeners.close=fn}},
|
||||||
|
}};
|
||||||
|
let listCalls=0;
|
||||||
|
const first={{id:'g1',title:'First',candidate_hash:'a1',revision:1,checks:[]}};
|
||||||
|
const second={{id:'g2',title:'Second',candidate_hash:'b2',revision:1,checks:[]}};
|
||||||
|
const app=createProgressiveHumanGates({{
|
||||||
|
document:{{querySelector:selector=>nodes[selector]||null}},
|
||||||
|
location:{{hash:'#/my-work/human-gates'}}, history:{{replaceState(){{}}}},
|
||||||
|
storage:{{getItem:()=>null,setItem(){{}}}}, isOnline:()=>true,
|
||||||
|
getIdentity:async()=>({{login:'timmy',accountKey:'7:timmy'}}),
|
||||||
|
fetchJson:async path=>{{
|
||||||
|
if(path.endsWith('/g1')) return first;
|
||||||
|
if(path.endsWith('/g2')) return second;
|
||||||
|
listCalls += 1;
|
||||||
|
return {{pending_count:1,items:[listCalls === 1 ? first : second]}};
|
||||||
|
}},
|
||||||
|
}});
|
||||||
|
(async()=>{{
|
||||||
|
await app.start();
|
||||||
|
listeners.close();
|
||||||
|
await listeners.open();
|
||||||
|
await new Promise(resolve=>setTimeout(resolve,0));
|
||||||
|
const controller=app.handoff().controller;
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
listCalls,current:controller.current().id,hidden:nodes['#human-gates'].hidden,
|
||||||
|
listHtml:nodes['#human-gates-list'].innerHTML,
|
||||||
|
detailHasSecond:nodes['#human-gate-detail'].innerHTML.includes('Second'),
|
||||||
|
}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], check=True, text=True, capture_output=True)
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"listCalls": 2,
|
||||||
|
"current": "g2",
|
||||||
|
"hidden": False,
|
||||||
|
"listHtml": '<button class="human-gate-card" type="button" data-human-gate-id="g2"><strong>Second</strong><code>b2</code><span>Priority 0</span></button>',
|
||||||
|
"detailHasSecond": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_adopts_progressive_human_gates_without_a_second_list_load():
|
def test_dashboard_adopts_progressive_human_gates_without_a_second_list_load():
|
||||||
dashboard = DASHBOARD.read_text()
|
dashboard = DASHBOARD.read_text()
|
||||||
index = INDEX.read_text()
|
index = INDEX.read_text()
|
||||||
|
|
|
||||||
|
|
@ -435,5 +435,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/later-sync.js'" in source
|
assert "BASE + 'static/later-sync.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -256,4 +256,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 { 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||||
assert ".markdown-content a { min-height:44px;" in css
|
assert ".markdown-content a { min-height:44px;" in css
|
||||||
assert "stackchain-dashboard-shell-v144" in worker
|
assert "stackchain-dashboard-shell-v145" 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]))
|
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 local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||||
assert "stackchain-dashboard-shell-v144" in worker
|
assert "stackchain-dashboard-shell-v145" in worker
|
||||||
|
|
||||||
|
|
||||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
assert "controller.recoverPermission('deadline')" in dashboard
|
assert "controller.recoverPermission('deadline')" in dashboard
|
||||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||||
assert "stackchain-dashboard-shell-v144" in worker
|
assert "stackchain-dashboard-shell-v145" in worker
|
||||||
assert ".device-setup-panel" in css
|
assert ".device-setup-panel" in css
|
||||||
assert ".device-readiness-card" in css
|
assert ".device-readiness-card" in css
|
||||||
assert "overflow-x:hidden" in css
|
assert "overflow-x:hidden" in css
|
||||||
|
|
|
||||||
|
|
@ -274,5 +274,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
|
||||||
def test_mobile_insights_rolls_into_the_offline_shell():
|
def test_mobile_insights_rolls_into_the_offline_shell():
|
||||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in worker
|
assert "stackchain-dashboard-shell-v145" in worker
|
||||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
|
||||||
assert ".mobile-start-day-finish { min-height:44px;" in html
|
assert ".mobile-start-day-finish { min-height:44px;" in html
|
||||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||||
assert "stackchain-dashboard-shell-v144" in service_worker
|
assert "stackchain-dashboard-shell-v145" in service_worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner():
|
||||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||||
source = SERVICE_WORKER.read_text()
|
source = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,14 @@ async function dispatchPush(payload) {{
|
||||||
def test_shared_progressive_snapshot_broker_rolls_the_offline_shell():
|
def test_shared_progressive_snapshot_broker_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/progressive-live-snapshot.js'" in source
|
assert "BASE + 'static/progressive-live-snapshot.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_week_unplan_undo_rolls_the_offline_shell():
|
def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
||||||
|
|
@ -204,20 +204,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
|
|
||||||
|
|
||||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/my-work.js'" in source
|
assert "BASE + 'static/my-work.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
@ -226,7 +226,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/authored-outbox.js'" in source
|
assert "BASE + 'static/authored-outbox.js'" in source
|
||||||
assert "BASE + 'static/background-issue-sync.js'" in source
|
assert "BASE + 'static/background-issue-sync.js'" in source
|
||||||
|
|
@ -235,7 +235,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||||
assert "BASE + 'static/issue-attachment.js'" in source
|
assert "BASE + 'static/issue-attachment.js'" in source
|
||||||
|
|
||||||
|
|
@ -243,14 +243,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_offline_review_next_ships_today_completion_atomically():
|
def test_offline_review_next_ships_today_completion_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/today-completion.js'" in source
|
assert "BASE + 'static/today-completion.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -258,7 +258,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/issue-sheet.js'" in source
|
assert "BASE + 'static/issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
@ -276,14 +276,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/later-picker.js'" in source
|
assert "BASE + 'static/later-picker.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/install-app.js'" in source
|
assert "BASE + 'static/install-app.js'" in source
|
||||||
|
|
@ -292,21 +292,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1361,7 +1361,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():
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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():
|
def test_readiness_runtime_is_available_in_offline_shell():
|
||||||
service_worker = SERVICE_WORKER.read_text()
|
service_worker = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "const CACHE = 'stackchain-dashboard-shell-v144';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v145';" in service_worker
|
||||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
|
||||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v144" in source
|
assert "stackchain-dashboard-shell-v145" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user