import json import subprocess from html.parser import HTMLParser from pathlib import Path from urllib.parse import urljoin 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): def __init__(self): super().__init__() self.sources = [] def handle_starttag(self, tag, attrs): if tag == "script": source = dict(attrs).get("src") if source: self.sources.append(source) def test_filtered_command_runs_the_matching_action(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); let action = ''; const commands = [ {{ name: 'Open whiteboard', run: () => {{ action = 'whiteboard'; }} }}, {{ name: 'Refresh now', run: () => {{ action = 'refresh'; }} }}, ]; const matches = filterCommands(commands, 'refresh'); if (matches.length !== 1) throw new Error(`expected one match, got ${{matches.length}}`); matches[0].run(); if (action !== 'refresh') throw new Error(`expected refresh, got ${{action}}`); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_command_script_resolves_inside_dashboard_subpath(): parser = ScriptSourceParser() parser.feed(dashboard_bundle_text()) command_source = next(source for source in parser.sources if source.endswith("commands.js")) assert urljoin( "https://forge.alexanderwhitestone.com/dashboard/", command_source ) == "https://forge.alexanderwhitestone.com/dashboard/static/commands.js" def test_mobile_search_renders_touch_sized_type_and_status_scope_controls(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert '
Filter search results' in html assert '' in html assert ' setTimeout(resolve, 0)); controller.setQuery('release'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('release')([{{ title: 'Current result' }}]); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile')([{{ title: 'Stale result' }}]); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready'); if (ready.length !== 1) throw new Error(`expected one ready state, got ${{ready.length}}`); if (ready[0].query !== 'release' || ready[0].items[0].title !== 'Current result') {{ throw new Error(`stale response replaced current state: ${{JSON.stringify(ready)}}`); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True, ) def test_remote_command_search_scope_change_aborts_and_restarts_without_stale_results(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const pending = []; const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page, scope) => new Promise(resolve => pending.push({{query, signal, page, scope, resolve}})), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setScope({{kind:'pull', state:'open'}}); await new Promise(resolve => setTimeout(resolve, 0)); if (!pending[0].signal.aborted) throw new Error('scope change did not abort old request'); if (JSON.stringify(pending[1].scope) !== JSON.stringify({{kind:'pull', state:'open'}})) {{ throw new Error('new request did not receive scope'); }} pending[1].resolve({{items:[{{kind:'pull',repository:'a/b',number:2}}],has_more:false,next_page:2}}); pending[0].resolve({{items:[{{kind:'issue',repository:'a/b',number:1}}],has_more:false,next_page:2}}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready').at(-1); if (ready.items.length !== 1 || ready.items[0].kind !== 'pull') throw new Error('stale scope result entered list'); if (ready.scope.kind !== 'pull' || ready.scope.state !== 'open') throw new Error('scope missing from state'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_aborts_superseded_and_cleared_queries(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const signals = []; const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal) => {{ signals.push(signal); return new Promise((resolve, reject) => signal.addEventListener('abort', () => {{ const error = new Error('aborted'); error.name = 'AbortError'; reject(error); }})); }}, onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery('release'); if (!signals[0].aborted) throw new Error('superseded request was not aborted'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery(''); if (!signals[1].aborted) throw new Error('cleared query request was not aborted'); await new Promise(resolve => setTimeout(resolve, 0)); if (states.some(state => state.status === 'error')) throw new Error('abort rendered an error'); if (states.at(-1).status !== 'idle') throw new Error('clear did not restore idle state'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_preserves_partial_result_status(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: () => Promise.resolve({{ items:[{{ title:'Useful result' }}], partial:true }}), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.find(state => state.status === 'ready'); if (!ready || ready.items.length !== 1 || ready.partial !== true) {{ throw new Error('partial result metadata was lost: ' + JSON.stringify(states)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_appends_deduplicated_pages_and_ignores_stale_load_more(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const pending = new Map(); const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page) => new Promise(resolve => pending.set(query + ':' + page, resolve)), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile:1')({{ items:[{{kind:'issue',repository:'a/b',number:1}}], has_more:true, next_page:2 }}); await new Promise(resolve => setTimeout(resolve, 0)); controller.loadMore(); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery('release'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile:2')({{ items:[{{kind:'issue',repository:'a/b',number:1}},{{kind:'pull',repository:'a/b',number:2}}], has_more:false, next_page:3 }}); pending.get('release:1')({{ items:[{{kind:'issue',repository:'a/b',number:3}}], has_more:false, next_page:2 }}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready').at(-1); if (ready.query !== 'release' || ready.items.length !== 1 || ready.items[0].number !== 3) {{ throw new Error('stale page entered current query: ' + JSON.stringify(states)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_command_selection_wraps_for_arrow_keys(): script = f""" const commands = require({json.dumps(str(COMMANDS))}); if (commands.nextSelection(-1, 'ArrowDown', 3) !== 0) throw new Error('should select first'); if (commands.nextSelection(2, 'ArrowDown', 3) !== 0) throw new Error('should wrap down'); if (commands.nextSelection(0, 'ArrowUp', 3) !== 2) throw new Error('should wrap up'); if (commands.nextSelection(1, 'Enter', 3) !== 1) throw new Error('other keys should not move'); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_palette_exposes_accessible_global_work_search_under_dashboard_subpath(): html = dashboard_bundle_text() assert 'role="combobox"' in html assert 'aria-controls="cmd-results"' in html assert 'role="listbox"' in html assert "fetch('api/v1/search?q='" in html assert "async function searchGlobalWork(query, signal, page = 1, scope" in html assert "signal," in html assert "Some results are temporarily unavailable." in html assert 'id="cmd-load-more"' in html assert urljoin( "https://forge.alexanderwhitestone.com/dashboard/", "api/v1/search?q=mobile", ) == "https://forge.alexanderwhitestone.com/dashboard/api/v1/search?q=mobile" def test_search_preview_ignores_stale_result_details(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const pending = new Map(); const states = []; const preview = createSearchPreview({{ fetchJson: item => new Promise(resolve => pending.set(item.number, resolve)), mutate: () => Promise.resolve(), onState: state => states.push(state), }}); preview.open({{ repository:'stackchain/api', number:1, kind:'issue' }}); preview.open({{ repository:'stackchain/api', number:2, kind:'issue' }}); pending.get(2)({{ number:2, title:'Current' }}); await new Promise(resolve => setTimeout(resolve, 0)); pending.get(1)({{ number:1, title:'Stale' }}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready'); if (ready.length !== 1 || ready[0].detail.number !== 2) {{ throw new Error('stale detail replaced current preview: ' + JSON.stringify(ready)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_claim_is_single_flight_and_reports_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let claims = 0; let resolveClaim; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: (_detail, action) => {{ if (action !== 'claim') throw new Error('wrong action'); claims += 1; return new Promise(resolve => {{ resolveClaim = resolve; }}); }}, onState: state => states.push(state), }}); await preview.open({{ repository:'stackchain/api', number:42, kind:'issue' }}); const detail = {{ repository:'stackchain/api', number:42, claimable:true }}; const first = preview.claim(detail); const second = preview.claim(detail); if (claims !== 1 || first !== second) throw new Error('claim was not single-flight'); resolveClaim({{ assignees:['timmy'] }}); await first; if (!states.some(state => state.status === 'claimed')) throw new Error('claim confirmation missing'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_reopen_is_single_flight_and_reports_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let reopens = 0; let resolveReopen; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: (_detail, action) => {{ if (action !== 'reopen') throw new Error('wrong action'); reopens += 1; return new Promise(resolve => {{ resolveReopen = resolve; }}); }}, onState: state => states.push(state), }}); const detail = {{ repository:'stackchain/api', number:42, reopenable:true }}; await preview.open(detail); const first = preview.reopen(detail); const second = preview.reopen(detail); if (reopens !== 1 || first !== second) throw new Error('reopen was not single-flight'); resolveReopen({{ state:'open', assignees:['timmy'] }}); await first; if (!states.some(state => state.status === 'reopened')) throw new Error('reopen confirmation missing'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_shares_canonical_url_without_closing_the_preview(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const calls = []; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(), share: async url => {{ calls.push(url); return 'shared'; }}, onState: state => states.push(state), }}); const detail = {{ repository:'stackchain/api', number:42, kind:'issue' }}; await preview.open(detail); const result = await preview.share('https://forge.example/dashboard/?search=release&preview=issue%3Astackchain%2Fapi%3A42'); process.stdout.write(JSON.stringify({{ result, calls, states }})); }})().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 payload = json.loads(result.stdout) assert payload["result"] == "shared" assert payload["calls"] == [ "https://forge.example/dashboard/?search=release&preview=issue%3Astackchain%2Fapi%3A42" ] assert payload["states"][-1]["status"] == "shared" assert payload["states"][-1]["detail"]["number"] == 42 def test_search_preview_share_is_single_flight_and_cancellation_restores_ready_context(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let calls = 0; let rejectShare; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(), share: () => {{ calls += 1; return new Promise((_resolve, reject) => rejectShare = reject); }}, onState: state => states.push(state), }}); await preview.open({{ repository:'stackchain/api', number:42, kind:'issue' }}); const first = preview.share('https://forge.example/dashboard/?search=x'); const second = preview.share('https://forge.example/dashboard/?search=x'); const canceled = new Error('canceled'); canceled.name = 'AbortError'; rejectShare(canceled); await Promise.allSettled([first, second]); const state = states.at(-1); process.stdout.write(JSON.stringify({{ calls, same:first === second, status:state.status, item:state.item, detail:state.detail, errorName:state.error?.name }})); }})().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) == { "calls": 1, "same": True, "status": "share-canceled", "item": {"repository": "stackchain/api", "number": 42, "kind": "issue"}, "detail": {"repository": "stackchain/api", "number": 42, "kind": "issue"}, "errorName": "AbortError", } def test_remote_search_selection_opens_native_preview_without_navigation(): html = dashboard_bundle_text() run_item = html.split("function runCommandItem(item)", 1)[1].split( "function renderCommands", 1 )[0] assert "searchPreview.open(item.result)" in run_item assert "window.location.assign" not in run_item remote_branch = run_item.split("} else {", 1)[1] assert "qs('#cmd-input').value = ''" not in remote_branch def test_search_preview_share_action_uses_canonical_stackchain_url_and_keeps_context(): html = dashboard_bundle_text() assert "share: url => createWorkRoute.share(url, navigator, navigator.clipboard)" in html handler = html.split("qs('#share-search-result').addEventListener('click'", 1)[1].split( "qs('#claim-search-result')", 1 )[0] assert "canonicalSearchPreviewUrl()" in handler assert "searchPreview.share" in handler assert "closeSearchPreview" not in handler assert "taskOverlayHistory.close" not in handler def test_assigned_issue_preview_hands_off_to_existing_my_work_sheet(): html = dashboard_bundle_text() assert "claimButton.hidden = !(detail.claimable || detail.assigned_to_me)" in html assert "claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me'" in html assert "openPreviewWorkInMyWork" in html def test_assigned_pull_preview_hands_off_to_existing_my_work_sheet_without_claiming(): html = dashboard_bundle_text() assert "claimButton.hidden = !(detail.claimable || detail.assigned_to_me)" in html assert "async function openPreviewWorkInMyWork(detail)" in html handoff = html.split("async function openPreviewWorkInMyWork(detail)", 1)[1].split( "function runCommandItem", 1 )[0] assert "candidate.kind === detail.kind" in handoff assert "candidate.key === detail.repository + '#' + detail.number" in handoff click_handler = html.split( "qs('#claim-search-result').addEventListener('click'", 1 )[1].split("qs('#start-search-result')", 1)[0] assert "if (claimed.claimable) await searchPreview.claim(claimed)" in click_handler assert "await openPreviewWorkInMyWork(claimed)" in click_handler def test_requested_pull_preview_opens_native_review_without_assignment_mutation(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert "startButton.hidden = !(detail.reviewable" in html assert "detail.reviewable ? 'Review now'" in html handler = html.split( "qs('#start-search-result').addEventListener('click'", 1 )[1].split("qs('#close-whiteboard')", 1)[0] assert "detail.kind = 'review'" in handler assert "openRoutedWork" in handler assert "{ replace:true }" in handler assert "searchPreview.claim" not in handler assert "fetchReviewJson" not in handler assert ".search-preview-actions button, .search-preview-actions a { min-height:44px;" in css def test_search_preview_offers_assign_and_start_for_eligible_issues(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert 'id="start-search-result"' in html assert "(detail.assigned_to_me ? 'Start in Today' : 'Assign & start')" in html assert "const searchAssignAndStart = createSearchStart(detail => searchPreview.claim(detail))" in html assert "searchAssignAndStart.run(detail, { alreadyOwned: detail.assigned_to_me })" in html assert ".search-preview-primary-actions" in css assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard(): html = dashboard_bundle_text() assert "detail.reopenable ? 'Reopen & resume'" in html assert "mutate: (detail, action) => fetchReviewJson(" in html handler = html.split("qs('#start-search-result').addEventListener('click'", 1)[1].split( "qs('#close-whiteboard')", 1 )[0] assert "searchReopenAndStart.run(detail)" in handler orchestrator = html.split("function createSearchStart(claim)", 1)[1].split( "const searchAssignAndStart", 1 )[0] assert "available: createAndStart.available" in orchestrator assert "claim," in orchestrator assert "acceptClaimedIssue(confirmed)" in orchestrator assert "const searchReopenAndStart = createSearchStart(detail => searchPreview.reopen(detail))" 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