Make mobile Home task-first with a Back-safe Insights view #995
|
|
@ -26,6 +26,7 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
|
|||
.app-menu { margin-left:auto; }
|
||||
.app-menu > summary { display:none; }
|
||||
.app-menu-panel { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||||
#open-insights { display:none; }
|
||||
button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #2a496e; color:#e5e7eb; padding:8px 12px; border-radius:10px; cursor:pointer; }
|
||||
.notification-undo { position:fixed; z-index:110; left:50%; bottom:calc(88px + env(safe-area-inset-bottom)); transform:translateX(-50%); box-sizing:border-box; width:min(520px,calc(100vw - 24px)); display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 12px; border:1px solid #60a5fa; border-radius:12px; background:#10233d; box-shadow:0 12px 36px rgba(0,0,0,.5); overflow-wrap:anywhere; }
|
||||
.notification-undo[hidden] { display:none; }
|
||||
|
|
@ -91,6 +92,8 @@ button:hover { filter: brightness(1.15); }
|
|||
.panel[open] > summary { margin-bottom: 8px; }
|
||||
aside, section { position: relative; z-index: 1; }
|
||||
main { position: relative; z-index: 1; display: grid; grid-template-columns: 300px 1fr 340px; gap: 14px; padding: 14px; }
|
||||
.insights-shell { display:contents; }
|
||||
.insights-header { display:none; }
|
||||
@media (max-width: 1100px) { main { grid-template-columns: 1fr; } }
|
||||
.sidebar { display:flex; flex-direction:column; gap: 12px; }
|
||||
.stack { display:flex; flex-direction:column; gap:10px; }
|
||||
|
|
@ -932,6 +935,12 @@ textarea { resize: vertical; min-height: 120px; }
|
|||
.app-menu:not([open]) > .app-menu-panel { display:none; }
|
||||
.app-menu-panel { position:absolute; top:calc(100% + 6px); right:0; width:min(280px,calc(100vw - 20px)); display:grid; gap:8px; padding:10px; border:1px solid #2a496e; border-radius:12px; background:#0b1526; box-shadow:0 18px 45px rgba(0,0,0,.5); }
|
||||
.app-menu-panel button { min-height:44px; width:100%; }
|
||||
#open-insights { display:block; }
|
||||
.insights-shell { display:none; }
|
||||
.insights-shell[data-mobile-open="true"] { position:fixed; inset:0; z-index:90; display:grid; align-content:start; gap:12px; overflow:auto; overflow-x:hidden; padding:max(12px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) max(12px,env(safe-area-inset-bottom)) max(12px,env(safe-area-inset-left)); background:#050c15; }
|
||||
.insights-header { position:sticky; top:0; z-index:2; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:10px; border:1px solid #2a496e; border-radius:12px; background:#0b1526; }
|
||||
.insights-header h2, .insights-header p { margin:0 0 4px; }
|
||||
.insights-header button { min-height:44px; flex:none; }
|
||||
.device-setup-panel { width:100%; padding:14px; padding-bottom:calc(14px + env(safe-area-inset-bottom)); }
|
||||
.device-setup-step { grid-template-columns:1fr; }
|
||||
.device-setup-action { width:100%; }
|
||||
|
|
|
|||
|
|
@ -204,6 +204,20 @@
|
|||
},
|
||||
});
|
||||
mobileTaskDock.start();
|
||||
const mobileInsights = createMobileInsights({
|
||||
root: qs('#insights-sheet'),
|
||||
launcher: qs('#open-insights'),
|
||||
closeButton: qs('#close-insights'),
|
||||
dock: qs('#mobile-task-dock'),
|
||||
hud: qs('[data-mobile-today-hud]'),
|
||||
backgrounds: [qs('header'), qs('#my-work')],
|
||||
menu: qs('.app-menu'),
|
||||
history: window.history,
|
||||
location: window.location,
|
||||
eventTarget: window,
|
||||
mediaQuery: window.matchMedia('(max-width: 600px)'),
|
||||
});
|
||||
mobileInsights.start();
|
||||
qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click());
|
||||
qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click());
|
||||
let liveMode = true;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<details class="app-menu">
|
||||
<summary id="app-menu-toggle" aria-label="Open dashboard menu">Menu</summary>
|
||||
<div class="app-menu-panel">
|
||||
<button id="open-insights" type="button" aria-controls="insights-sheet">Insights</button>
|
||||
<button id="refresh">Refresh</button>
|
||||
<button id="open-palette">Command</button>
|
||||
<button id="open-device-setup" type="button">Device setup</button>
|
||||
|
|
@ -273,6 +274,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="insights-shell" id="insights-sheet" aria-labelledby="insights-heading">
|
||||
<div class="insights-header">
|
||||
<div><h2 id="insights-heading">Insights</h2><p class="small muted">Context, activity, live signals, and workspace tools.</p></div>
|
||||
<button id="close-insights" type="button">Close</button>
|
||||
</div>
|
||||
<aside class="sidebar">
|
||||
<details class="panel stack" data-panel-key="context" open>
|
||||
<summary><h2>Context & view</h2></summary>
|
||||
|
|
@ -334,6 +340,7 @@
|
|||
<div id="md-preview" style="margin-top:8px;"></div>
|
||||
</details>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="plan-today-sheet" id="plan-today-sheet" role="dialog" aria-modal="true" aria-labelledby="plan-today-title" hidden>
|
||||
|
|
@ -1628,6 +1635,7 @@
|
|||
<script src="static/update-decision-transaction.js"></script>
|
||||
<script src="static/agenda-session-launcher.js"></script>
|
||||
<script src="static/mobile-launch.js"></script>
|
||||
<script src="static/mobile-insights.js"></script>
|
||||
<script src="static/mobile-app-shortcuts.js"></script>
|
||||
<script src="static/install-app.js"></script>
|
||||
<script src="static/mobile-device-setup.js"></script>
|
||||
|
|
|
|||
74
frontend/mobile-insights.js
Normal file
74
frontend/mobile-insights.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||
else root.createMobileInsights = factory;
|
||||
})(typeof self !== 'undefined' ? self : this, function (o) {
|
||||
const route = '#/insights';
|
||||
let active = false;
|
||||
let previous = '#/my-work';
|
||||
|
||||
function attr(element, name, value) {
|
||||
if (value === null) element.removeAttribute(name);
|
||||
else element.setAttribute(name, value);
|
||||
}
|
||||
|
||||
function render(open, focus = false) {
|
||||
active = Boolean(open && o.mediaQuery.matches);
|
||||
attr(o.root, 'data-mobile-open', active ? 'true' : null);
|
||||
attr(o.root, 'role', active ? 'dialog' : null);
|
||||
attr(o.root, 'aria-modal', active ? 'true' : null);
|
||||
attr(o.root, 'aria-hidden', active || !o.mediaQuery.matches ? null : 'true');
|
||||
o.root.inert = !active && o.mediaQuery.matches;
|
||||
o.backgrounds.forEach(element => { element.inert = active; });
|
||||
o.dock.hidden = active;
|
||||
attr(o.hud, 'data-overlay-hidden', active ? 'true' : null);
|
||||
if (active) o.closeButton.focus();
|
||||
else if (focus) o.launcher.focus();
|
||||
}
|
||||
|
||||
function url(hash) {
|
||||
return (o.location.pathname || '') + (o.location.search || '') + hash;
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!o.mediaQuery.matches || active) return false;
|
||||
previous = o.location.hash && o.location.hash !== route ? o.location.hash : '#/my-work';
|
||||
if (o.menu) o.menu.open = false;
|
||||
o.history.pushState({ ...(o.history.state || {}), mobileInsights:true, previousHash:previous }, '', url(route));
|
||||
render(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function sync() {
|
||||
render(o.location.hash === route, active && o.location.hash !== route);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!active) return false;
|
||||
if (o.history.state?.mobileInsights) o.history.back();
|
||||
else {
|
||||
const state = { ...(o.history.state || {}) };
|
||||
delete state.mobileInsights;
|
||||
delete state.previousHash;
|
||||
o.history.replaceState(state, '', url(previous));
|
||||
render(false, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function start() {
|
||||
o.launcher.addEventListener('click', open);
|
||||
o.closeButton.addEventListener('click', close);
|
||||
o.eventTarget.addEventListener('popstate', sync);
|
||||
o.eventTarget.addEventListener('hashchange', sync);
|
||||
o.eventTarget.addEventListener('keydown', event => {
|
||||
if (active && event.key === 'Escape') {
|
||||
event.preventDefault?.();
|
||||
close();
|
||||
}
|
||||
});
|
||||
o.mediaQuery.addEventListener?.('change', sync);
|
||||
sync();
|
||||
}
|
||||
|
||||
return { start, open, close, current:() => active };
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v115';
|
||||
const CACHE = 'stackchain-dashboard-shell-v116';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
|
@ -114,6 +114,7 @@ const SHELL = [
|
|||
BASE + 'static/update-decision-transaction.js',
|
||||
BASE + 'static/agenda-session-launcher.js',
|
||||
BASE + 'static/mobile-launch.js',
|
||||
BASE + 'static/mobile-insights.js',
|
||||
BASE + 'static/mobile-app-shortcuts.js',
|
||||
BASE + 'static/install-app.js',
|
||||
BASE + 'static/mobile-device-setup.js',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ FEATURE_SOURCES = {
|
|||
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
||||
"security-center": ("static/security-center.js",),
|
||||
"today-timer": (
|
||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js",
|
||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js",
|
||||
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-handoff.js",
|
||||
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
|
||||
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
|
||||
|
|
|
|||
|
|
@ -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-v115" in worker
|
||||
assert "stackchain-dashboard-shell-v116" in worker
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v115" in worker
|
||||
assert "stackchain-dashboard-shell-v116" 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]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v115" in worker
|
||||
assert "stackchain-dashboard-shell-v116" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
|
|
|||
|
|
@ -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-v115" in worker
|
||||
assert "stackchain-dashboard-shell-v116" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
|
|
|||
178
tests/test_mobile_insights.py
Normal file
178
tests/test_mobile_insights.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-insights.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_mobile_insights_opens_as_addressable_modal_and_hides_home_chrome():
|
||||
script = f"""
|
||||
const createInsights = require({json.dumps(str(CONTROLLER))});
|
||||
const listeners = {{}};
|
||||
const element = () => ({{
|
||||
hidden:false, inert:false, attributes:{{}}, focuses:0, listeners:{{}},
|
||||
setAttribute(name,value) {{ this.attributes[name] = String(value); }},
|
||||
removeAttribute(name) {{ delete this.attributes[name]; }},
|
||||
addEventListener(name,fn) {{ this.listeners[name] = fn; }},
|
||||
focus() {{ this.focuses += 1; }},
|
||||
}});
|
||||
const root = element(); const launcher = element(); const closeButton = element();
|
||||
const dock = element(); const hud = element(); const home = element(); const header = element();
|
||||
const menu = {{open:true}};
|
||||
const location = {{pathname:'/dashboard/', search:'', hash:'#/my-work/today'}};
|
||||
const history = {{state:null, pushes:[], pushState(state,unused,url) {{
|
||||
this.state=state; this.pushes.push(url); location.hash=url.slice(url.indexOf('#'));
|
||||
}}, replaceState() {{}}, back() {{}}}};
|
||||
const controller = createInsights({{
|
||||
root, launcher, closeButton, dock, hud, backgrounds:[header,home], menu, history, location,
|
||||
eventTarget:{{addEventListener(name,fn) {{ listeners[name]=fn; }}}},
|
||||
mediaQuery:{{matches:true, addEventListener() {{}}}},
|
||||
}});
|
||||
controller.start();
|
||||
launcher.listeners.click();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
hash:location.hash, pushes:history.pushes, open:root.attributes['data-mobile-open'],
|
||||
role:root.attributes.role, modal:root.attributes['aria-modal'], rootInert:root.inert,
|
||||
homeInert:home.inert, headerInert:header.inert, dockHidden:dock.hidden,
|
||||
hudHidden:hud.attributes['data-overlay-hidden'], closeFocuses:closeButton.focuses,
|
||||
menuOpen:menu.open,
|
||||
}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"hash": "#/insights",
|
||||
"pushes": ["/dashboard/#/insights"],
|
||||
"open": "true",
|
||||
"role": "dialog",
|
||||
"modal": "true",
|
||||
"rootInert": False,
|
||||
"homeInert": True,
|
||||
"headerInert": True,
|
||||
"dockHidden": True,
|
||||
"hudHidden": "true",
|
||||
"closeFocuses": 1,
|
||||
"menuOpen": False,
|
||||
}
|
||||
|
||||
|
||||
def test_direct_insights_route_closes_locally_and_restores_launcher_focus():
|
||||
script = f"""
|
||||
const createInsights = require({json.dumps(str(CONTROLLER))});
|
||||
const listeners = {{}};
|
||||
const element = () => ({{
|
||||
hidden:false, inert:false, attributes:{{}}, focuses:0, listeners:{{}},
|
||||
setAttribute(name,value) {{ this.attributes[name] = String(value); }},
|
||||
removeAttribute(name) {{ delete this.attributes[name]; }},
|
||||
addEventListener(name,fn) {{ this.listeners[name] = fn; }},
|
||||
focus() {{ this.focuses += 1; }},
|
||||
}});
|
||||
const root=element(), launcher=element(), closeButton=element(), dock=element(), hud=element(), home=element();
|
||||
const location={{pathname:'/dashboard/', search:'', hash:'#/insights'}};
|
||||
const history={{state:null, backs:0, replacements:[], pushState() {{}}, back() {{ this.backs += 1; }},
|
||||
replaceState(state,unused,url) {{ this.state=state; this.replacements.push(url); location.hash=url.slice(url.indexOf('#')); }}
|
||||
}};
|
||||
const controller=createInsights({{
|
||||
root,launcher,closeButton,dock,hud,backgrounds:[home],history,location,
|
||||
eventTarget:{{addEventListener(name,fn) {{ listeners[name]=fn; }}}},
|
||||
mediaQuery:{{matches:true,addEventListener() {{}}}},
|
||||
}});
|
||||
controller.start();
|
||||
closeButton.listeners.click();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
current:controller.current(), hash:location.hash, backs:history.backs,
|
||||
replacements:history.replacements, rootInert:root.inert, homeInert:home.inert,
|
||||
dockHidden:dock.hidden, hudHidden:hud.attributes['data-overlay-hidden'] || null,
|
||||
launcherFocuses:launcher.focuses,
|
||||
}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"current": False,
|
||||
"hash": "#/my-work",
|
||||
"backs": 0,
|
||||
"replacements": ["/dashboard/#/my-work"],
|
||||
"rootInert": True,
|
||||
"homeInert": False,
|
||||
"dockHidden": False,
|
||||
"hudHidden": None,
|
||||
"launcherFocuses": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_browser_back_closes_insights_without_resetting_the_work_route():
|
||||
script = f"""
|
||||
const createInsights = require({json.dumps(str(CONTROLLER))});
|
||||
const listeners={{}};
|
||||
const element=()=>({{hidden:false,inert:false,attributes:{{}},focuses:0,listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{this.focuses+=1;}}}});
|
||||
const root=element(),launcher=element(),closeButton=element(),dock=element(),hud=element(),home=element();
|
||||
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/update'}};
|
||||
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{}}}};
|
||||
const controller=createInsights({{root,launcher,closeButton,dock,hud,backgrounds:[home],history,location,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
|
||||
controller.start(); launcher.listeners.click();
|
||||
location.hash='#/my-work/update'; history.state=null; listeners.popstate?.({{state:null}});
|
||||
process.stdout.write(JSON.stringify({{current:controller.current(),hash:location.hash,launcherFocuses:launcher.focuses,homeInert:home.inert,dockHidden:dock.hidden}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"current": False,
|
||||
"hash": "#/my-work/update",
|
||||
"launcherFocuses": 1,
|
||||
"homeInert": False,
|
||||
"dockHidden": False,
|
||||
}
|
||||
|
||||
|
||||
def test_escape_requests_back_navigation_only_while_insights_is_open():
|
||||
script = f"""
|
||||
const createInsights=require({json.dumps(str(CONTROLLER))});
|
||||
const listeners={{}};
|
||||
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
|
||||
const root=element(),launcher=element(),closeButton=element(),dock=element(),hud=element();
|
||||
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work'}};
|
||||
const history={{state:null,backs:0,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{this.backs+=1;}}}};
|
||||
const controller=createInsights({{root,launcher,closeButton,dock,hud,backgrounds:[],history,location,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
|
||||
controller.start(); launcher.listeners.click();
|
||||
let prevented=0; listeners.keydown?.({{key:'Escape',preventDefault(){{prevented+=1;}}}}); listeners.keydown?.({{key:'Enter',preventDefault(){{prevented+=1;}}}});
|
||||
process.stdout.write(JSON.stringify({{backs:history.backs,prevented}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {"backs": 1, "prevented": 1}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_home_progressively_discloses_secondary_panels_as_insights():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="open-insights"' in html
|
||||
assert 'id="insights-sheet"' in html
|
||||
assert 'id="close-insights"' in html
|
||||
assert html.index('id="my-work"') < html.index('id="insights-sheet"')
|
||||
assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
|
||||
assert html.index('data-panel-key="markdown"') < html.index('</section>\n</main>')
|
||||
assert '<script src="static/mobile-insights.js"></script>' in html
|
||||
assert "createMobileInsights({" in html
|
||||
assert "mediaQuery: window.matchMedia('(max-width: 600px)')" in html
|
||||
assert ".insights-shell { display:contents; }" in html
|
||||
assert "#open-insights { display:none; }" in html
|
||||
assert '.insights-shell[data-mobile-open="true"]' in html
|
||||
assert "position:fixed; inset:0;" in html
|
||||
assert "overflow-x:hidden" in html
|
||||
assert ".insights-header button { min-height:44px;" in html
|
||||
|
||||
|
||||
def test_mobile_insights_rolls_into_the_offline_shell():
|
||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v116" in worker
|
||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||
|
|
@ -283,4 +283,4 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
|
|||
assert ".mobile-start-day-finish { min-height:44px;" in html
|
||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||
assert "stackchain-dashboard-shell-v115" in service_worker
|
||||
assert "stackchain-dashboard-shell-v116" in service_worker
|
||||
|
|
|
|||
|
|
@ -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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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
|
||||
|
|
|
|||
|
|
@ -165,13 +165,13 @@ async function dispatchPush(payload) {{
|
|||
def test_offline_activation_migration_rolls_the_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
|
||||
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
|
@ -180,7 +180,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
|||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/authored-outbox.js'" in source
|
||||
assert "BASE + 'static/background-issue-sync.js'" in source
|
||||
|
|
@ -189,7 +189,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
|||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||
assert "BASE + 'static/issue-attachment.js'" in source
|
||||
|
||||
|
|
@ -197,14 +197,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
|||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -212,7 +212,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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/issue-sheet.js'" in source
|
||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
|
@ -230,14 +230,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
|||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
|
@ -246,21 +246,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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" 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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
@ -930,7 +930,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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
|
@ -1051,6 +1051,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
|||
"/dashboard/static/update-decision-transaction.js",
|
||||
"/dashboard/static/agenda-session-launcher.js",
|
||||
"/dashboard/static/mobile-launch.js",
|
||||
"/dashboard/static/mobile-insights.js",
|
||||
"/dashboard/static/mobile-app-shortcuts.js",
|
||||
"/dashboard/static/install-app.js",
|
||||
"/dashboard/static/mobile-device-setup.js",
|
||||
|
|
|
|||
|
|
@ -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-v115';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v116';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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-v115" in source
|
||||
assert "stackchain-dashboard-shell-v116" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user