feat: continue through mobile search results (Closes #801)
CI / lint (pull_request) Successful in 1m39s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 05:08:56 +00:00
1 parent af9699e362
commit 745d2529e5
6 files changed
+241 -28

No files matched your search

+126 -2
View File
@@ -290,6 +290,129 @@ def test_palette_exposes_accessible_global_work_search_under_dashboard_subpath()
) == "https://forge.alexanderwhitestone.com/dashboard/api/v1/search?q=mobile"
def test_search_preview_navigates_a_loaded_result_session_and_reports_position():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
const items = [1, 2, 3].map(number => ({{repository:'stackchain/api', number, kind:'issue'}}));
const states = [];
const navigated = [];
const preview = createSearchPreview({{
fetchJson: item => Promise.resolve({{...item, title:'Issue ' + item.number}}),
mutate: () => Promise.resolve(),
getSession: () => ({{items, more:false}}),
loadMore: () => Promise.resolve(),
onNavigate: item => navigated.push(item.number),
onState: state => states.push(state),
}});
await preview.open(items[1]);
await preview.previous();
await preview.next();
const ready = states.filter(state => state.status === 'ready');
process.stdout.write(JSON.stringify({{
navigated,
positions:ready.map(state => state.navigation && [state.navigation.position, state.navigation.total,
state.navigation.hasPrevious, state.navigation.hasNext]),
current:ready.at(-1).detail.number,
}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"navigated": [1, 2],
"positions": [[2, 3, True, True], [1, 3, False, True], [2, 3, True, True]],
"current": 2,
}
def test_search_preview_next_loads_one_page_at_boundary_then_announces_completion():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
let session = {{items:[1, 2].map(number => ({{repository:'stackchain/api', number, kind:'issue'}})), more:true}};
let loads = 0;
const states = [];
const preview = createSearchPreview({{
fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(),
getSession: () => session,
loadMore: async () => {{
loads += 1;
session = {{items:[...session.items, {{repository:'stackchain/api', number:3, kind:'issue'}}], more:false}};
}},
onNavigate() {{}}, onState: state => states.push(state),
}});
await preview.open(session.items[1]);
await preview.next();
await preview.next();
const final = states.at(-1);
process.stdout.write(JSON.stringify({{loads, current:final.detail.number, complete:final.complete,
navigation:final.navigation}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"loads": 1,
"current": 3,
"complete": True,
"navigation": {"position": 3, "total": 3, "hasPrevious": True, "hasNext": False},
}
def test_remote_search_load_more_is_awaitable_for_preview_boundary_navigation():
script = f"""
const commands = require({json.dumps(str(COMMANDS))});
(async () => {{
const states = [];
const controller = commands.createGlobalSearchController({{
delay:0,
search: async (_query, _signal, page) => page === 1
? {{items:[{{kind:'issue',repository:'a/b',number:1}}], has_more:true, next_page:2}}
: {{items:[{{kind:'issue',repository:'a/b',number:2}}], has_more:false, next_page:3}},
onState: state => states.push(state),
}});
controller.setQuery('mobile');
await new Promise(resolve => setTimeout(resolve, 5));
const loaded = await controller.loadMore();
process.stdout.write(JSON.stringify({{numbers:loaded.items.map(item => item.number), more:loaded.more}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {"numbers": [1, 2], "more": False}
def test_mobile_search_preview_renders_session_navigation_and_advances_after_planning():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
assert 'id="previous-search-result"' in html
assert 'id="search-preview-position"' in html
assert 'id="next-search-result"' in html
assert 'aria-label="Search result navigation"' in html
assert ".search-preview-navigation" in css
assert ".search-preview-header button, .search-preview-actions button" in css
assert "session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})]" in html
preview_source = SEARCH_PREVIEW.read_text()
assert "api.previous()" in preview_source
assert "api.next()" in preview_source
defer_flow = html.split("if (context === 'search')", 1)[1].split(
"if (context === 'detail')", 1
)[0]
queue_flow = html.split(
"qs('#queue-search-result').addEventListener('click'", 1
)[1].split("qs('#start-search-result')", 1)[0]
assert "next()" in defer_flow
assert "next()" in queue_flow
assert "closeSearchPreview()" not in defer_flow
assert "if (outcome === 'queued') await next()" in queue_flow
def test_search_preview_ignores_stale_result_details():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
@@ -514,7 +637,7 @@ def test_search_preview_offers_assign_and_start_for_eligible_issues():
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
def test_search_preview_queues_eligible_issue_and_returns_to_preserved_search():
def test_search_preview_queues_eligible_issue_and_continues_preserved_search():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
@@ -530,7 +653,8 @@ def test_search_preview_queues_eligible_issue_and_returns_to_preserved_search():
)[0]
assert "destination: 'queue'" in handler
assert "alreadyOwned: detail.assigned_to_me" in handler
assert "closeSearchPreview()" in handler
assert "next()" in handler
assert "closeSearchPreview()" not in handler
assert "mobileSearchViewport.restoreScroll()" in html
assert "@media (max-width:420px)" in css
assert ".search-preview-primary-actions { grid-template-columns:1fr; }" in css