feat: continue through mobile search results (Closes #801)
This commit is contained in:
1 parent
af9699e362
commit
745d2529e5
6 files changed
+241
-28
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user