feat: make mobile work search keyboard-safe (#317)
CI / lint (pull_request) Successful in 34s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-08 16:14:29 +00:00
1 parent 6143568b95
commit b5b6b4d0a9
7 files changed
+145 -4

No files matched your search

+54
View File
@@ -10,6 +10,7 @@ from tests.dashboard_bundle import dashboard_bundle_text
FRONTEND = Path(__file__).parents[1] / "frontend"
COMMANDS = FRONTEND / "commands.js"
SEARCH_PREVIEW = FRONTEND / "search-preview.js"
MOBILE_SEARCH_VIEWPORT = FRONTEND / "mobile-search-viewport.js"
class ScriptSourceParser(HTMLParser):
@@ -240,3 +241,56 @@ def test_assigned_issue_preview_hands_off_to_existing_my_work_sheet():
assert "detail.claimable || (detail.assigned_to_me && detail.kind === 'issue')" in html
assert "claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me'" in html
assert "openPreviewIssueInMyWork" in html
def test_mobile_search_viewport_tracks_keyboard_geometry_without_leaking_listeners():
script = f"""
const createMobileSearchViewport = require({json.dumps(str(MOBILE_SEARCH_VIEWPORT))});
const listeners = new Map();
const viewport = {{
height: 430, offsetTop: 17,
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: (name, fn) => {{ if (listeners.get(name) === fn) listeners.delete(name); }},
}};
const values = new Map();
const palette = {{ style: {{
setProperty: (name, value) => values.set(name, value),
removeProperty: name => values.delete(name),
}} }};
const results = {{ scrollTop: 73 }};
const controller = createMobileSearchViewport({{
palette, results, viewport, mediaQuery: {{ matches: true }},
schedule: fn => fn(),
}});
controller.open();
if (values.get('--search-viewport-top') !== '17px') throw new Error('viewport offset was not applied');
if (values.get('--search-viewport-height') !== '430px') throw new Error('viewport height was not applied');
if (listeners.size !== 2) throw new Error('viewport listeners were not attached once');
controller.rememberScroll();
results.scrollTop = 0;
controller.restoreScroll();
if (results.scrollTop !== 73) throw new Error('result scroll position was not restored');
controller.open();
if (listeners.size !== 2) throw new Error('duplicate viewport listeners were attached');
controller.close();
if (listeners.size !== 0 || values.size !== 0) throw new Error('viewport state leaked after close');
"""
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
def test_mobile_search_is_a_keyboard_safe_workspace_wired_to_preview_history():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
assert 'class="cmd-palette-header"' in html
assert 'id="close-command-palette"' in html
assert 'style="margin-top:8px;max-height:min(65vh,520px);overflow-y:auto;"' not in html
assert 'src="static/mobile-search-viewport.js"' in html
assert "height:var(--search-viewport-height,100dvh)" in css
assert "top:var(--search-viewport-top,0px)" in css
assert "#cmd-results { flex:1; min-height:0; overflow-y:auto;" in css
assert "mobileSearchViewport.rememberScroll()" in html
assert "mobileSearchViewport.restoreScroll()" in html
assert "mobileSearchViewport.open()" in html
assert "mobileSearchViewport.close()" in html
+9 -1
View File
@@ -97,12 +97,19 @@ async function dispatchNotificationClick(route) {{
def test_share_target_sign_in_fix_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v32" in source
assert "stackchain-dashboard-shell-v33" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v33" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag():
result = run_worker_scenario(
"""
@@ -268,6 +275,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/mobile-task-dock.js",
"/dashboard/static/mobile-launch.js",
"/dashboard/static/install-app.js",
"/dashboard/static/mobile-search-viewport.js",
"/dashboard/static/background-issue-sync.js",
}