stackchain-dashboard/tests/test_mobile_today_command_bar.py
timmy b77be4f5b0
All checks were successful
CI / lint (pull_request) Successful in 2m46s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m17s
CI / release-candidate (pull_request) Has been skipped
feat: compact active Today controls on mobile (Closes #1074)
2026-08-18 09:36:34 +00:00

159 lines
5.8 KiB
Python

import json
import subprocess
from pathlib import Path
SOURCE = Path(__file__).parents[1] / "frontend" / "mobile-today-command-bar.js"
FRONTEND = SOURCE.parent
def run_node(body: str) -> dict:
script = f"const createCommandBar = require({json.dumps(str(SOURCE))});\n" + body
completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(completed.stdout)
def test_more_sheet_opens_modally_and_restores_focus_after_close():
result = run_node(r"""
function element() {
const listeners = {};
return {
listeners, open:false, hidden:false, attributes:{}, focusCount:0,
addEventListener:(type, listener) => { (listeners[type] ||= []).push(listener); },
dispatch(type, event={}) { (listeners[type] || []).forEach(listener => listener({preventDefault(){}, target:this, ...event})); },
setAttribute(name, value) { this.attributes[name] = value; },
showModal() { this.open = true; },
close() { this.open = false; this.dispatch('close'); },
focus() { this.focusCount += 1; },
};
}
const more = element();
const sheet = element();
const close = element();
const firstAction = element();
createCommandBar({more, sheet, close, firstAction});
more.dispatch('click');
const opened = {open:sheet.open, expanded:more.attributes['aria-expanded'], firstFocused:firstAction.focusCount};
close.dispatch('click');
process.stdout.write(JSON.stringify({opened, closed:!sheet.open, restored:more.focusCount}));
""")
assert result == {
"opened": {"open": True, "expanded": "true", "firstFocused": 1},
"closed": True,
"restored": 1,
}
def test_end_session_action_reuses_existing_session_control_and_closes_sheet():
result = run_node(r"""
function element() {
const listeners = {};
return {
open:false, clicks:0, attributes:{},
addEventListener:(type, listener) => { (listeners[type] ||= []).push(listener); },
dispatch(type, event={}) { (listeners[type] || []).forEach(listener => listener({preventDefault(){}, target:this, ...event})); },
setAttribute(name, value) { this.attributes[name] = value; },
showModal() { this.open = true; },
close() { this.open = false; this.dispatch('close'); },
click() { this.clicks += 1; this.dispatch('click'); },
focus() {},
};
}
const more = element();
const sheet = element();
const endAction = element();
const existingEndControl = element();
createCommandBar({more, sheet, endAction, existingEndControl});
more.dispatch('click');
endAction.dispatch('click');
process.stdout.write(JSON.stringify({open:sheet.open, endClicks:existingEndControl.clicks}));
""")
assert result == {"open": False, "endClicks": 1}
def test_secondary_action_closes_command_sheet_before_existing_handler_runs():
result = run_node(r"""
function element() {
const listeners = {};
return {
open:false, attributes:{}, observedOpen:null,
addEventListener:(type, listener) => { (listeners[type] ||= []).push(listener); },
dispatch(type) { (listeners[type] || []).forEach(listener => listener({preventDefault(){}, target:this})); },
setAttribute(name, value) { this.attributes[name] = value; },
showModal() { this.open = true; }, close() { this.open = false; this.dispatch('close'); }, focus() {},
};
}
const more = element();
const sheet = element();
const update = element();
createCommandBar({more, sheet, actionButtons:[update]});
update.addEventListener('click', () => { update.observedOpen = sheet.open; });
more.dispatch('click');
update.dispatch('click');
process.stdout.write(JSON.stringify({open:sheet.open, handlerSawOpen:update.observedOpen}));
""")
assert result == {"open": False, "handlerSawOpen": False}
def test_rendered_hud_and_dock_height_define_scroll_clearance():
result = run_node(r"""
const listeners = {};
const element = () => ({
open:false, attributes:{},
addEventListener:(type, listener) => { (listeners[type] ||= []).push(listener); },
setAttribute(name, value) { this.attributes[name] = value; },
focus() {}, showModal() { this.open = true; }, close() { this.open = false; },
});
const values = {};
let resize;
const ResizeObserverImpl = class {
constructor(callback) { resize = callback; }
observe() {}
disconnect() {}
};
const more = element();
const sheet = element();
createCommandBar({
more, sheet,
hud:{offsetHeight:96}, dock:{offsetHeight:56},
style:{setProperty:(name, value) => { values[name] = value; }},
ResizeObserverImpl,
});
resize();
process.stdout.write(JSON.stringify(values));
""")
assert result == {"--mobile-today-clearance": "168px"}
def test_mobile_command_bar_keeps_primary_actions_visible_and_secondary_actions_modal():
html = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
hud = html[html.index('<section class="mobile-today-hud"'):html.index('</section>', html.index('<section class="mobile-today-hud"'))]
sheet = html[html.index('<dialog class="mobile-today-actions"'):html.index('</dialog>', html.index('<dialog class="mobile-today-actions"'))]
assert "data-mobile-today-open" in hud
assert "data-mobile-today-complete" in hud
assert "data-mobile-today-toggle" in hud
assert "data-mobile-today-more" in hud
for secondary in (
"data-work-session-previous",
"data-work-session-next",
"data-mobile-today-update",
"data-mobile-today-blocked",
"data-today-break-open",
"data-work-session-adjust-plan",
"data-mobile-today-end",
):
assert secondary not in hud
assert secondary in sheet
assert 'aria-modal="true"' in sheet
assert "body.mobile-today-active { padding-bottom:calc(var(--mobile-today-clearance" in css
assert "createMobileTodayCommandBar({" in dashboard
assert '<script src="static/mobile-today-command-bar.js"></script>' in html