import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CONTROLLER = ROOT / "frontend" / "mobile-search-modal.js" def run_node(script: str) -> dict: completed = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) def test_search_preview_journey_keeps_background_inert_and_restores_prior_state(): script = f""" const createModal = require({json.dumps(str(CONTROLLER))}); const listeners = {{}}; const element = (name) => ({{ name, inert:false, isConnected:true, hidden:false, disabled:false, focuses:0, focus() {{ this.focuses += 1; }}, matches() {{ return true; }}, }}); const header=element('header'), main=element('main'), dock=element('dock'); dock.inert=true; const palette=element('palette'), preview=element('preview'); const input=element('input'), close=element('close'), opener=element('opener'); palette.querySelectorAll=()=>[input]; preview.querySelectorAll=()=>[close]; const controller=createModal({{ document:{{addEventListener(name,fn){{listeners[name]=fn;}}}}, backgrounds:[header,main,dock], }}); controller.activate(palette, {{opener, initialFocus:input}}); const opened=[header.inert,main.inert,dock.inert,input.focuses]; controller.transition(preview, {{initialFocus:close}}); const previewed=[header.inert,main.inert,dock.inert,close.focuses,controller.current()===preview]; controller.transition(palette, {{initialFocus:input}}); const returned=[header.inert,main.inert,dock.inert,input.focuses]; controller.deactivate(); process.stdout.write(JSON.stringify({{opened,previewed,returned,restored:[header.inert,main.inert,dock.inert,opener.focuses]}})); """ assert run_node(script) == { "opened": [True, True, True, 1], "previewed": [True, True, True, 1, True], "returned": [True, True, True, 2], "restored": [False, False, True, 1], } def test_tab_and_shift_tab_wrap_dynamic_visible_controls_on_active_surface(): script = f""" const createModal = require({json.dumps(str(CONTROLLER))}); const listeners={{}}; const element=(name)=>({{name,inert:false,isConnected:true,hidden:false,disabled:false,focuses:0, focus(){{this.focuses+=1;}},matches(selector){{return !this.hidden;}}}}); const surface=element('surface'), first=element('first'), middle=element('middle'), last=element('last'); const hidden=element('hidden'); hidden.hidden=true; const disabled=element('disabled'); disabled.disabled=true; surface.querySelectorAll=()=>[first,hidden,middle,disabled,last]; const document={{activeElement:last,addEventListener(name,fn){{listeners[name]=fn;}}}}; const controller=createModal({{document,backgrounds:[]}}); controller.activate(surface,{{initialFocus:first}}); let prevented=0; document.activeElement=last; listeners.keydown({{key:'Tab',shiftKey:false,preventDefault(){{prevented+=1;}}}}); document.activeElement=first; listeners.keydown({{key:'Tab',shiftKey:true,preventDefault(){{prevented+=1;}}}}); // A newly rendered enabled control becomes the last stop without remounting. const dynamic=element('dynamic'); surface.querySelectorAll=()=>[first,middle,last,dynamic]; document.activeElement=dynamic; listeners.keydown({{key:'Tab',shiftKey:false,preventDefault(){{prevented+=1;}}}}); process.stdout.write(JSON.stringify({{first:first.focuses,last:last.focuses,prevented}})); """ assert run_node(script) == {"first": 3, "last": 1, "prevented": 3} def test_disconnected_opener_is_not_focused_on_final_close(): script = f""" const createModal=require({json.dumps(str(CONTROLLER))}); const surface={{querySelectorAll(){{return[];}}}}; const opener={{isConnected:true,focuses:0,focus(){{this.focuses+=1;}}}}; const document={{addEventListener(){{}}}}; const controller=createModal({{document,backgrounds:[]}}); controller.activate(surface,{{opener}}); opener.isConnected=false; controller.deactivate(); process.stdout.write(JSON.stringify({{focuses:opener.focuses}})); """ assert run_node(script) == {"focuses": 0} def test_controls_inside_collapsed_sections_are_excluded_from_tab_order(): script = f""" const createModal=require({json.dumps(str(CONTROLLER))}); const listeners={{}}; const visible={{disabled:false,hidden:false,isConnected:true,focuses:0,matches(){{return true;}},getClientRects(){{return [{{}}];}},focus(){{this.focuses+=1;}}}}; const collapsed={{...visible,getClientRects(){{return [];}}}}; const surface={{querySelectorAll(){{return [visible,collapsed];}}}}; const document={{activeElement:visible,addEventListener(name,fn){{listeners[name]=fn;}}}}; const controller=createModal({{document,backgrounds:[]}}); controller.activate(surface); let prevented=0; listeners.keydown({{key:'Tab',shiftKey:false,preventDefault(){{prevented+=1;}}}}); process.stdout.write(JSON.stringify({{prevented,focuses:visible.focuses}})); """ assert run_node(script) == {"prevented": 1, "focuses": 2} def test_search_modal_is_packaged_with_accessible_semantics(): html = (ROOT / "frontend" / "index.html").read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() bundle = (ROOT / "src" / "frontend_bundle.py").read_text() assert 'id="cmd-palette" role="dialog" aria-modal="true"' in html assert '' in html assert '"static/mobile-search-modal.js"' in bundle assert "createMobileSearchModal({" in dashboard assert "searchModal.transition(qs('#search-preview')" in dashboard