stackchain-dashboard/tests/test_mobile_first_task.py
timmy ebb7a78cab
All checks were successful
CI / lint (pull_request) Successful in 2m48s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 4m3s
CI / release-candidate (pull_request) Has been skipped
feat: finish onboarding with active Today (Closes #1144)
2026-08-19 18:51:15 +00:00

164 lines
6.3 KiB
Python

import json
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-first-task.js"
def run_node(body: str) -> dict:
script = f"""
const createFirstTask = require({json.dumps(str(CONTROLLER))});
class Element {{
constructor() {{ this.listeners = {{}}; this.disabled = false; this.hidden = false; this.open = false; this.textContent = ''; this.focused = false; }}
addEventListener(name, callback) {{ this.listeners[name] = callback; }}
click() {{ this.listeners.click?.({{preventDefault() {{}}}}); }}
showModal() {{ this.open = true; }}
close() {{ this.open = false; this.listeners.close?.(); }}
focus() {{ this.focused = true; }}
}}
{body}
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_first_task_activation_is_account_bound_and_completes_only_when_today_is_active():
result = run_node(
"""
const values = new Map();
const storage = {getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value)};
const sheet = new Element();
let login = 'Timmy';
let hasWork = false;
let todayActive = false;
const controller = createFirstTask({
storage, getLogin:() => login, hasWork:() => hasWork, isTodayActive:() => todayActive,
isOnline:() => true, mediaQuery:{matches:true}, sheet, title:new Element(),
findButton:new Element(), createButton:new Element(), setupButton:new Element(),
closeButton:new Element(), status:new Element(), onFind() {}, onCreate() {}, onSetup() {},
});
const before = [controller.required(), controller.open(), sheet.open];
hasWork = true;
const awaitingStart = controller.refresh();
const stillRequired = controller.required();
todayActive = true;
const completed = controller.refresh();
const timmyRequired = controller.required();
login = 'alexander'; hasWork = false; todayActive = false;
const alexanderRequired = controller.required();
process.stdout.write(JSON.stringify({before, awaitingStart, stillRequired, completed, sheetOpen:sheet.open, timmyRequired, alexanderRequired, values:[...values]}));
"""
)
assert result == {
"before": [True, True, True],
"awaitingStart": "awaiting-start",
"stillRequired": True,
"completed": "completed",
"sheetOpen": False,
"timmyRequired": False,
"alexanderRequired": True,
"values": [["stackchain.first-task.v1:timmy", "complete"]],
}
def test_first_task_activation_resumes_with_finish_starting_guidance():
result = run_node(
"""
const sheet = new Element();
const title = new Element();
const findButton = new Element();
const createButton = new Element();
const status = new Element();
const controller = createFirstTask({
storage:{getItem:() => null, setItem() {}}, getLogin:() => 'timmy', hasWork:() => true,
isTodayActive:() => false, isOnline:() => true, mediaQuery:{matches:true}, sheet, title,
findButton, createButton, setupButton:new Element(), closeButton:new Element(), status,
onFind() {}, onCreate() {}, onSetup() {},
});
controller.open();
process.stdout.write(JSON.stringify({
required:controller.required(), title:title.textContent, status:status.textContent,
find:findButton.textContent, create:createButton.textContent,
}));
"""
)
assert result == {
"required": True,
"title": "Finish starting your first task",
"status": "Your task is ready. Start it from Find or create and start another task.",
"find": "Find & start",
"create": "Create & start",
}
def test_first_task_activation_routes_existing_flows_and_keeps_create_available_offline():
result = run_node(
"""
const calls = [];
const sheet = new Element();
const findButton = new Element();
const createButton = new Element();
const setupButton = new Element();
const closeButton = new Element();
const status = new Element();
let online = false;
const controller = createFirstTask({
storage:{getItem:() => null, setItem() {}}, getLogin:() => 'timmy', hasWork:() => false,
isOnline:() => online, mediaQuery:{matches:true}, sheet, findButton, createButton,
setupButton, closeButton, status,
onFind:() => calls.push('find'), onCreate:() => calls.push('create'), onSetup:() => calls.push('setup'),
});
controller.start();
controller.open();
const offline = {findDisabled:findButton.disabled, createDisabled:createButton.disabled, status:status.textContent, createFocused:createButton.focused};
findButton.click();
createButton.click();
controller.open();
setupButton.click();
controller.open();
online = true;
controller.render();
findButton.click();
process.stdout.write(JSON.stringify({offline, calls, sheetOpen:sheet.open}));
"""
)
assert result == {
"offline": {
"findDisabled": True,
"createDisabled": False,
"status": "You are offline. Create a task now and it will stay in Drafts until you reconnect.",
"createFocused": True,
},
"calls": ["create", "setup", "find"],
"sheetOpen": False,
}
@pytest.mark.anyio
async def test_dashboard_renders_and_wires_phone_safe_first_task_activation():
html = await dashboard()
assert '<dialog class="mobile-first-task" id="mobile-first-task" aria-labelledby="mobile-first-task-title">' in html
assert '<h2 id="mobile-first-task-title">Start your first task</h2>' in html
assert 'id="mobile-first-task-find" type="button">Find &amp; start</button>' in html
assert 'id="mobile-first-task-create" type="button">Create &amp; start</button>' in html
assert 'id="mobile-first-task-setup" type="button">Make this phone work-ready</button>' in html
assert '<script src="static/mobile-first-task.js"></script>' in html
assert "const mobileFirstTask = createMobileFirstTask({" in html
assert "isTodayActive: () => workSession.checkpointed()" in html
assert "shouldActivate: () => mobileFirstTask.required()" in html
assert "openActivation: () => mobileFirstTask.open()" in html
assert "mobileFirstTask.refresh()" in html
assert '.mobile-first-task { box-sizing:border-box; width:100%;' in html
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
assert '.mobile-first-task-actions button { min-height:48px;' in html