stackchain-dashboard/tests/test_workspace_bootstrap.py
timmy 8909bedd78
Some checks failed
CI / lint (pull_request) Successful in 4m2s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Failing after 7m47s
CI / release-candidate (pull_request) Has been skipped
test: align packaged journeys with demand hydration
2026-08-27 12:17:20 +00:00

243 lines
9.7 KiB
Python

import json
import subprocess
from pathlib import Path
BOOTSTRAP = Path(__file__).parents[1] / "frontend" / "workspace-bootstrap.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
def run_bootstrap(scenario: str) -> dict:
harness = f"""
const loadWorkspace = require({json.dumps(str(BOOTSTRAP))});
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
"""
completed = subprocess.run(
["node", "-e", harness], check=True, capture_output=True, text=True
)
return json.loads(completed.stdout)
def test_workspace_bootstrap_demand_loads_optional_features_after_work_core():
result = run_bootstrap("""
const status={textContent:''};
const document={
querySelector(selector) {
if (selector === 'meta[name="stackchain-feature-work-core"]') return {content:'feature-work-core-123.js'};
if (selector === 'meta[name="stackchain-feature-today-timer"]') return {content:'feature-workspace-abc.js'};
if (selector === 'meta[name="stackchain-feature-planning"]') return {content:'feature-planning-def.js'};
if (selector === '#my-work-action-status') return status;
return null;
}
};
const requested=[];
const createLoader=options=>({load:async name=>{requested.push(name + ':' + options.urls[name]);}});
const lifecycle=await loadWorkspace({document,createLoader});
const before=requested.slice();
await lifecycle.hydrateWorkspace?.();
console.log(JSON.stringify({before,after:requested,status:status.textContent}));
""")
assert result == {
"before": ["work-core:feature-work-core-123.js"],
"after": [
"work-core:feature-work-core-123.js",
"today-timer:feature-workspace-abc.js",
"planning:feature-planning-def.js",
],
"status": "",
}
def test_workspace_bootstrap_hydrates_once_and_replays_dependent_mobile_action():
result = run_bootstrap("""
const listeners={}; const requested=[];
const document={
querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/);
return match ? {content:'feature-' + match[1] + '.js'} : null;
},
addEventListener(name,callback,capture){listeners[name]={callback,capture};},
removeEventListener(name,callback,capture){
if(listeners[name]?.callback===callback && listeners[name]?.capture===capture) delete listeners[name];
},
};
const createLoader=()=>({load:async name=>{
requested.push(name);
if(name==='today-timer') setImmediate(()=>{
actionReady=true;
lifecycle.markWorkspaceReady?.();
});
}});
let lifecycle=await loadWorkspace({document,createLoader});
let prevented=0,stopped=0,replayed=0,actionReady=false,replayedReady=false;
const target={
closest(selector){return selector.includes(':not([data-mobile-task="work"])') ? null : this;},
click(){replayed++; replayedReady=actionReady;},
};
await listeners.click?.callback({target,preventDefault(){prevented++;},stopImmediatePropagation(){stopped++;}});
console.log(JSON.stringify({requested,prevented,stopped,replayed,replayedReady,listening:Boolean(listeners.click)}));
""")
assert result == {
"requested": ["work-core", "today-timer", "planning"],
"prevented": 1,
"stopped": 1,
"replayed": 1,
"replayedReady": True,
"listening": False,
}
def test_workspace_bootstrap_hydrates_immediately_for_deep_link():
result = run_bootstrap("""
const requested=[];
const document={
querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/);
return match ? {content:'feature-' + match[1] + '.js'} : null;
},
addEventListener(){},
};
const window={location:{hash:'#/my-work/today'},addEventListener(){},removeEventListener(){}};
const createLoader=()=>({load:async name=>{requested.push(name);}});
const lifecycle=await loadWorkspace({document,window,createLoader});
await lifecycle.deepLinkReady;
console.log(JSON.stringify({requested}));
""")
assert result == {"requested": ["work-core", "today-timer", "planning"]}
def test_workspace_bootstrap_returns_after_work_core_before_optional_features_hydrate():
result = run_bootstrap("""
const document={querySelector(selector) {
const match=selector.match(/stackchain-feature-([^\"]+)/);
return match ? {content:'feature-' + match[1] + '.js'} : null;
}};
const requested=[]; const releases={};
const createLoader=()=>({load:name=>{
requested.push(name);
if (name === 'work-core') return Promise.resolve();
return new Promise(resolve=>{releases[name]=resolve;});
}});
const lifecycle=await loadWorkspace({document,createLoader});
const returned=requested.slice();
const hydration=lifecycle.hydrateWorkspace();
releases['today-timer'](); releases.planning();
await hydration;
console.log(JSON.stringify({returned,settled:requested}));
""")
assert result == {
"returned": ["work-core"],
"settled": ["work-core", "today-timer", "planning"],
}
def test_workspace_bootstrap_recovers_one_transient_failure_in_place():
result = run_bootstrap("""
const status={textContent:''}; const retry={hidden:true,disabled:false}; let attempts=0;
const document={querySelector(selector) {
if (selector.startsWith('meta[')) return {content:'feature-workspace-abc.js'};
if (selector === '#my-work-action-status') return status;
if (selector === '#retry-workspace') return retry;
return null;
}};
const createLoader=()=>({load:async()=>{attempts++; if (attempts === 1) throw new Error('brief outage');}});
const schedule=callback=>{callback();};
const lifecycle=await loadWorkspace({document,createLoader,schedule});
await lifecycle.hydrateWorkspace();
console.log(JSON.stringify({attempts,status:status.textContent,retryHidden:retry.hidden}));
""")
assert result == {"attempts": 4, "status": "", "retryHidden": True}
def test_workspace_bootstrap_offers_single_flight_manual_retry_without_reloading():
result = run_bootstrap("""
const status={textContent:''}; const listeners={}; let reloads=0; let attempts=0;
const retry={hidden:true,disabled:false,addEventListener(name,callback){listeners[name]=callback;}};
const document={querySelector(selector) {
if (selector.startsWith('meta[')) return {content:'feature-workspace-abc.js'};
if (selector === '#my-work-action-status') return status;
if (selector === '#retry-workspace') return retry;
return null;
}};
const window={location:{reload(){reloads++;}},addEventListener(){},removeEventListener(){}};
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 3) throw new Error('offline');}});
const loading=loadWorkspace({document,window,createLoader,schedule:callback=>callback()});
await new Promise(resolve=>setImmediate(resolve));
const offered={hidden:retry.hidden,disabled:retry.disabled,status:status.textContent};
const first=listeners.click(); const second=listeners.click();
await Promise.all([first,second,loading]);
console.log(JSON.stringify({attempts,reloads,offered,status:status.textContent,retryHidden:retry.hidden}));
""")
assert result == {
"attempts": 3,
"reloads": 0,
"offered": {
"hidden": False,
"disabled": False,
"status": "Workspace unavailable. Reconnect or retry.",
},
"status": "",
"retryHidden": True,
}
def test_workspace_retry_control_is_accessible_and_touch_safe():
index = INDEX.read_text()
css = CSS.read_text()
assert '<button class="retry-work-route" id="retry-workspace" type="button" hidden>Retry workspace</button>' in index
assert ".retry-work-route { min-height:44px;" in css
def test_workspace_bootstrap_reconnect_retries_in_place_without_reloading():
result = run_bootstrap("""
const status={textContent:''}; const listeners={}; let reloads=0; let attempts=0;
const retry={hidden:true,disabled:false,addEventListener(){},removeEventListener(){}};
const document={querySelector(selector) {
if (selector.startsWith('meta[')) return {content:'feature-workspace-abc.js'};
if (selector === '#my-work-action-status') return status;
if (selector === '#retry-workspace') return retry;
return null;
}};
const window={
location:{reload(){reloads++;}},
addEventListener(name,callback) { listeners[name]=callback; },
removeEventListener(name,callback) { if (listeners[name] === callback) delete listeners[name]; },
};
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 3) throw new Error('offline');}});
const loading=loadWorkspace({document,window,createLoader,schedule:callback=>callback()});
await new Promise(resolve=>setImmediate(resolve));
const waiting=Boolean(listeners.online);
if (listeners.online) listeners.online();
await loading;
console.log(JSON.stringify({attempts,waiting,reloads,status:status.textContent}));
""")
assert result == {
"attempts": 3,
"waiting": True,
"reloads": 0,
"status": "",
}
def test_workspace_bootstrap_replays_online_event_after_dashboard_registers_handlers():
result = run_bootstrap("""
const status={textContent:''}; const listeners={};
const document={querySelector(selector) {
if (selector.startsWith('meta[')) return {content:'feature-workspace-abc.js'};
if (selector === '#my-work-action-status') return status;
return null;
}};
const window={
addEventListener(name,callback) { listeners[name]=callback; },
removeEventListener(name,callback) { if (listeners[name] === callback) delete listeners[name]; },
};
const createLoader=()=>({load:async()=>{listeners.online();}});
const lifecycle=await loadWorkspace({document,window,createLoader});
let reconnects=0;
lifecycle.replayOnline(()=>{reconnects++;});
lifecycle.replayOnline(()=>{reconnects++;});
console.log(JSON.stringify({reconnects,listening:Boolean(listeners.online)}));
""")
assert result == {"reconnects": 1, "listening": False}