feat: pause Today during mobile Security Center (Closes #1086)
This commit is contained in:
1 parent
aba4b55ca0
commit
473f592615
11 files changed
+174
-14
No files matched your search
@@ -446,6 +446,37 @@ def test_release_artifact_pauses_today_across_mobile_work_and_insights_detours(t
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
|
||||
}""")
|
||||
|
||||
page.locator("#close-issue-sheet").click()
|
||||
page.locator("#app-menu-toggle").click()
|
||||
page.locator("#active-devices").click()
|
||||
security_pause = page.locator("#security-center-today-detour")
|
||||
expect(security_pause).to_be_visible()
|
||||
expect(security_pause).to_contain_text("Today paused · Ship mobile capture")
|
||||
bounds = page.locator("#return-from-security-center").bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
assert page.evaluate("""() => {
|
||||
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === false && timer.detour_interruption?.reason === 'security-center';
|
||||
}""")
|
||||
page.locator("#security-section-devices").click()
|
||||
page.go_back()
|
||||
expect(security_pause).to_be_visible()
|
||||
page.go_back()
|
||||
expect(page.locator("#active-devices-sheet")).to_be_hidden()
|
||||
assert page.evaluate("""() => {
|
||||
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
|
||||
}""")
|
||||
|
||||
page.locator("#active-devices").click()
|
||||
expect(security_pause).to_be_visible()
|
||||
page.locator("#return-from-security-center").click()
|
||||
expect(page.locator("#active-devices-sheet")).to_be_hidden()
|
||||
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
|
||||
browser.close()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
|
||||
@@ -12,6 +12,19 @@ SESSION_JS = ROOT / "frontend" / "session.js"
|
||||
SECURITY_CENTER_JS = ROOT / "frontend" / "security-center.js"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_security_center_wires_mobile_today_pause_before_lazy_loading_and_visible_return():
|
||||
html = await dashboard()
|
||||
session = SESSION_JS.read_text()
|
||||
dashboard_js = (ROOT / "frontend" / "dashboard.js").read_text()
|
||||
|
||||
assert 'id="security-center-today-detour" data-today-detour' in html
|
||||
assert 'id="return-from-security-center" data-return-from-detour type="button">Return to Today</button>' in html
|
||||
assert session.index("beginDetour?.('security-center')") < session.index("devicesSheet.hidden = false;")
|
||||
assert "onClose: () => root.stackchainTodayTimerView?.finishDetour?.('security-center')" in session
|
||||
assert "window.stackchainTodayTimerView = timerView;" in dashboard_js
|
||||
|
||||
|
||||
def run_session_scenario(scenario: str) -> dict:
|
||||
harness = f"""
|
||||
const createSessionBoundary = require({json.dumps(str(SESSION_JS))});
|
||||
|
||||
@@ -78,8 +78,9 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
||||
assert b"function createDetailDefer" in first.feature_bundles["today-timer"].runtime_bytes
|
||||
assert b"gitea_time_logged" not in first.runtime_bytes
|
||||
assert b"gitea_time_logged" in security_center.runtime_bytes
|
||||
# Core mobile workflows stay below 99 KiB, including synced Search views and Update decisions.
|
||||
assert len(first.runtime_gzip_bytes) <= 99 * 1024
|
||||
# Core mobile workflows stay below 100 KiB, including synced Search, Update decisions,
|
||||
# and interruption-safe Security Center navigation.
|
||||
assert len(first.runtime_gzip_bytes) <= 100 * 1024
|
||||
assert f'name="stackchain-feature-issue-capture" content="{capture.runtime_name}"' in first.dashboard_html
|
||||
assert f'name="stackchain-feature-pull-workflow" content="{pull_workflow.runtime_name}"' in first.dashboard_html
|
||||
assert f"BASE + '{capture.runtime_name}'" in first.service_worker_source
|
||||
|
||||
@@ -254,8 +254,8 @@ process.stdout.write(JSON.stringify({{calls, open:queues.open}}));
|
||||
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
|
||||
html = await dashboard()
|
||||
|
||||
assert html.count('data-today-detour role="status"') == 5
|
||||
assert html.count('data-return-from-detour type="button"') == 5
|
||||
assert html.count('data-today-detour role="status"') == 6
|
||||
assert html.count('data-return-from-detour type="button"') == 6
|
||||
assert "detour:() => timerView" in html
|
||||
timer_source = TIMER.read_text()
|
||||
assert "createTodayDetourInterruption" in timer_source
|
||||
|
||||
@@ -230,6 +230,49 @@ const boundary={{
|
||||
assert output["pushed"] == ["/dashboard/#security/activity", "/dashboard/#security/devices"]
|
||||
|
||||
|
||||
def test_security_center_finishes_its_today_detour_only_when_the_sheet_closes():
|
||||
harness = f"""
|
||||
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
|
||||
const listeners={{}};
|
||||
const element=()=>({{
|
||||
hidden:true,textContent:'',children:[],attributes:{{}},
|
||||
addEventListener(){{}},focus(){{}},replaceChildren(){{this.children=[];}},append(...values){{this.children.push(...values);}},
|
||||
setAttribute(name,value){{this.attributes[name]=value;}},removeAttribute(name){{delete this.attributes[name];}},scrollIntoView(){{}},
|
||||
}});
|
||||
const names=['active-devices','active-devices-sheet','active-devices-list','active-devices-status',
|
||||
'enrolled-passkeys-list','enrolled-passkeys-status','enroll-passkey','security-activity-list',
|
||||
'security-activity-status','load-more-security-activity','close-active-devices',
|
||||
'security-section-activity','security-section-devices','security-section-passkeys',
|
||||
'security-activity-section','security-devices-section','security-passkeys-section'];
|
||||
const ids=Object.fromEntries(names.map(id=>[id,element()]));
|
||||
const root={{
|
||||
document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}},
|
||||
history:{{state:null,pushState(state){{this.state=state;}}}},
|
||||
location:{{pathname:'/dashboard/',search:''}},
|
||||
addEventListener:(type,listener)=>listeners[type]=listener,
|
||||
}};
|
||||
const boundary={{listActiveDevices:async()=>[],listPasskeys:async()=>[],listSecurityEvents:async()=>({{events:[],authentication_alerts:[],next_cursor:null}})}};
|
||||
let closes=0;
|
||||
(async()=>{{
|
||||
const controller=attachSecurityCenter({{root,boundary,onClose:()=>closes++}});
|
||||
await controller.open();
|
||||
listeners.popstate({{state:{{stackchainSecuritySection:'devices'}}}});
|
||||
const afterSectionBack=closes;
|
||||
listeners.popstate({{state:null}});
|
||||
listeners.popstate({{state:null}});
|
||||
console.log(JSON.stringify({{afterSectionBack,closes,hidden:ids['active-devices-sheet'].hidden}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(completed.stdout) == {
|
||||
"afterSectionBack": 0,
|
||||
"closes": 1,
|
||||
"hidden": True,
|
||||
}
|
||||
|
||||
|
||||
def test_close_uses_history_back_to_remove_security_center_route():
|
||||
harness = f"""
|
||||
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
|
||||
|
||||
@@ -313,6 +313,26 @@ process.stdout.write(JSON.stringify({pausedDetour, runningDetour, replacedReturn
|
||||
}
|
||||
|
||||
|
||||
def test_security_center_close_cannot_finish_an_unrelated_detour():
|
||||
script = SOURCE.read_text() + r"""
|
||||
const values = new Map();
|
||||
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
|
||||
const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>1000});
|
||||
timer.activate('issue:r:1:');
|
||||
timer.beginDetour('insights');
|
||||
const returned = timer.returnFromDetour('security-center');
|
||||
process.stdout.write(JSON.stringify({returned, pending:timer.detourInterruption(), snapshot:timer.snapshot()}));
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(completed.stdout) == {
|
||||
"returned": None,
|
||||
"pending": {"identity": "issue:r:1:", "resume": True, "reason": "insights"},
|
||||
"snapshot": {"identity": "issue:r:1:", "elapsed_ms": 0, "running": False},
|
||||
}
|
||||
|
||||
|
||||
def test_device_setup_detour_excludes_permission_time_and_survives_reload():
|
||||
script = SOURCE.read_text() + r"""
|
||||
const values = new Map();
|
||||
@@ -342,6 +362,35 @@ process.stdout.write(JSON.stringify({interruption, restored, paused, returned, r
|
||||
}
|
||||
|
||||
|
||||
def test_security_center_detour_excludes_investigation_time_and_resumes_once():
|
||||
script = SOURCE.read_text() + r"""
|
||||
const values = new Map();
|
||||
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
|
||||
let now = 1000;
|
||||
const timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now});
|
||||
timer.activate('issue:r:42:');
|
||||
now = 6000;
|
||||
const interruption = timer.beginDetour('security-center');
|
||||
now = 26000;
|
||||
const paused = timer.snapshot();
|
||||
const returned = timer.returnFromDetour();
|
||||
now = 28000;
|
||||
const resumed = timer.snapshot();
|
||||
const repeated = timer.returnFromDetour();
|
||||
process.stdout.write(JSON.stringify({interruption, paused, returned, resumed, repeated}));
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(completed.stdout) == {
|
||||
"interruption": {"identity": "issue:r:42:", "resume": True, "reason": "security-center"},
|
||||
"paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False},
|
||||
"returned": {"identity": "issue:r:42:", "resumed": True, "reason": "security-center"},
|
||||
"resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True},
|
||||
"repeated": None,
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_detour_view_names_work_and_returns_to_today():
|
||||
script = SOURCE.read_text() + r"""
|
||||
const values = new Map();
|
||||
|
||||
Reference in new issue
Block a user