feat: make mobile task overlays back-safe (#315)
This commit is contained in:
1 parent
80a1fa60bd
commit
9a152c435e
5 files changed
+262
-17
No files matched your search
@@ -664,7 +664,7 @@ async def test_dashboard_launches_share_capture_with_draft_conflict_choices():
|
||||
assert 'id="resume-issue-draft"' in html
|
||||
assert 'id="shared-content-conflict"' in html
|
||||
assert '.shared-content-actions button { min-height:44px;' in html
|
||||
assert "history.replaceState({}, '', cleanUrl)" in html
|
||||
assert "history.replaceState(history.state || {}, '', cleanUrl)" in html
|
||||
assert "navigator.serviceWorker.register('service-worker.js')" in html
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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_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
|
||||
Reference in new issue
Block a user