stackchain-dashboard/tests/test_task_overlay_history.py
timmy 7a62f7ead7
All checks were successful
CI / lint (pull_request) Successful in 43s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped
feat: preview work before planning (#405)
2026-08-09 14:22:37 +00:00

215 lines
7.4 KiB
Python

import json
import subprocess
from pathlib import Path
from tests.dashboard_bundle import dashboard_bundle_text
OVERLAY_HISTORY = Path(__file__).parents[1] / "frontend" / "task-overlay-history.js"
def test_task_overlay_history_unwinds_preview_search_and_dashboard_without_duplicate_entries():
script = f"""
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
const listeners = {{}};
const location = {{ hash:'', pathname:'/dashboard/' }};
const stack = [{{ page:'dashboard' }}];
let cursor = 0;
const changes = [];
const history = {{
get state() {{ return stack[cursor]; }},
pushState(state) {{ stack.splice(cursor + 1); stack.push(state); cursor += 1; }},
back() {{ if (cursor > 0) cursor -= 1; listeners.popstate({{state:stack[cursor]}}); }},
}};
const controller = createTaskOverlayHistory({{
history, location,
eventTarget: {{ addEventListener(name, callback) {{ listeners[name] = callback; }} }},
onChange(kind, previous) {{ changes.push([kind, previous]); }},
}});
controller.start();
controller.open('search');
controller.open('search');
controller.open('search-preview');
controller.close();
controller.close();
process.stdout.write(JSON.stringify({{changes, stack, cursor, current:controller.current()}}));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"changes": [
["search", None],
["search-preview", "search"],
["search", "search-preview"],
[None, "search"],
],
"stack": [
{"page": "dashboard"},
{"page": "dashboard", "taskOverlay": "search"},
{"page": "dashboard", "taskOverlay": "search-preview"},
],
"cursor": 0,
"current": None,
}
def test_task_overlay_history_rejects_unknown_states_and_preserves_existing_history_state():
script = f"""
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
const pushed = [];
const history = {{
state: {{ workRoute:'#/my-work/issue/stackchain/api/7' }},
pushState(state) {{ this.state = state; pushed.push(state); }}, back() {{}},
}};
const controller = createTaskOverlayHistory({{
history, location:{{}}, eventTarget:{{addEventListener() {{}}}}, onChange() {{}},
}});
const invalid = controller.open('settings');
const valid = controller.open('new');
process.stdout.write(JSON.stringify({{invalid, valid, pushed}}));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"invalid": False,
"valid": True,
"pushed": [{
"workRoute": "#/my-work/issue/stackchain/api/7",
"taskOverlay": "new",
}],
}
def test_task_overlay_history_can_leave_an_overlay_for_a_work_route_transition():
script = f"""
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
const changes = [];
const history = {{
state: {{ page:'dashboard' }},
pushState(state) {{ this.state = state; }},
replaceState(state) {{ this.state = state; }},
back() {{ throw new Error('route transitions must not race history.back'); }},
}};
const controller = createTaskOverlayHistory({{
history, eventTarget:{{addEventListener() {{}}}},
onChange(kind, previous) {{ changes.push([kind, previous]); }},
}});
controller.open('find');
const left = controller.leave();
process.stdout.write(JSON.stringify({{left, state:history.state, current:controller.current(), changes}}));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"left": True,
"state": {"page": "dashboard"},
"current": None,
"changes": [["find", None], [None, "find"]],
}
def test_plan_today_uses_one_history_layer_and_can_leave_after_save():
script = f"""
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
const listeners = {{}};
const changes = [];
const stack = [{{ page:'dashboard' }}];
let cursor = 0;
const history = {{
get state() {{ return stack[cursor]; }},
pushState(state) {{ stack.splice(cursor + 1); stack.push(state); cursor += 1; }},
replaceState(state) {{ stack[cursor] = state; }},
back() {{ cursor -= 1; listeners.popstate({{state:stack[cursor]}}); }},
}};
const controller = createTaskOverlayHistory({{
history,
eventTarget: {{ addEventListener(name, callback) {{ listeners[name] = callback; }} }},
onChange(kind, previous) {{ changes.push([kind, previous]); }},
}});
controller.start();
const opened = controller.open('plan-today');
const repeated = controller.open('plan-today');
const left = controller.leave();
process.stdout.write(JSON.stringify({{
opened, repeated, left, stack, cursor, current:controller.current(), changes,
}}));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"opened": True,
"repeated": True,
"left": True,
"stack": [
{"page": "dashboard"},
{"page": "dashboard"},
],
"cursor": 1,
"current": None,
"changes": [["plan-today", None], [None, "plan-today"]],
}
def test_plan_today_preview_is_a_nested_browser_history_layer():
script = f"""
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
const listeners = {{}};
const changes = [];
const stack = [{{ page:'dashboard' }}];
let cursor = 0;
const history = {{
get state() {{ return stack[cursor]; }},
pushState(state) {{ stack.splice(cursor + 1); stack.push(state); cursor += 1; }},
back() {{ cursor -= 1; listeners.popstate({{state:stack[cursor]}}); }},
}};
const controller = createTaskOverlayHistory({{
history,
eventTarget: {{ addEventListener(name, callback) {{ listeners[name] = callback; }} }},
onChange(kind, previous) {{ changes.push([kind, previous]); }},
}});
controller.start();
controller.open('plan-today');
const previewed = controller.open('plan-today-preview');
controller.close();
process.stdout.write(JSON.stringify({{previewed, cursor, current:controller.current(), changes}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"previewed": True,
"cursor": 1,
"current": "plan-today",
"changes": [
["plan-today", None],
["plan-today-preview", "plan-today"],
["plan-today", "plan-today-preview"],
],
}
def test_dashboard_routes_mobile_task_overlays_through_browser_history():
html = dashboard_bundle_text()
assert '<script src="static/task-overlay-history.js"></script>' in html
assert "createTaskOverlayHistory({" in html
assert "taskOverlayHistory.open('new')" in html
assert "taskOverlayHistory.open('find')" in html
assert "taskOverlayHistory.open('search')" in html
assert "taskOverlayHistory.open('search-preview')" in html
assert "taskOverlayHistory.close()" in html
assert "saveIssueCaptureDraft();" in html
assert "history.replaceState(history.state || {}, '', cleanUrl)" in html