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 '' 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