from __future__ import annotations import json import subprocess from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js" def run_node(script: str) -> dict: result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True, ) return json.loads(result.stdout) def test_issue_capture_modal_inerts_background_focuses_cancel_and_restores_exact_launcher(): result = run_node( f""" const createIssueCapture = require({json.dumps(str(MODULE))}); function element(id) {{ return {{id, inert:false, hidden:false, disabled:false, isConnected:true, focus() {{ document.activeElement = this; }}, getClientRects() {{ return [{{}}]; }} }}; }} const launcher = element('mobile-new'); const cancel = element('cancel-new-issue'); const title = element('create-issue-title'); const panel = {{...element('create-issue-panel'), scrollTop:420}}; const background = [element('header'), element('main'), element('mobile-task-dock')]; const root = {{ querySelector: selector => selector === '#cancel-new-issue' ? cancel : (selector === '.create-issue-panel' ? panel : null), querySelectorAll: () => [cancel, title], addEventListener() {{}}, removeEventListener() {{}}, }}; const document = {{activeElement: launcher}}; const lifecycle = createIssueCapture.createModalLifecycle({{ root, background, document, requestClose() {{}}, }}); lifecycle.open(launcher); const opened = {{active:document.activeElement.id, inert:background.map(item => item.inert)}}; lifecycle.close(); process.stdout.write(JSON.stringify({{ opened, closed:{{active:document.activeElement.id, inert:background.map(item => item.inert), scrollTop:panel.scrollTop}} }})); """ ) assert result == { "opened": {"active": "cancel-new-issue", "inert": [True, True, True]}, "closed": {"active": "mobile-new", "inert": [False, False, False], "scrollTop": 0}, } def test_issue_capture_modal_wraps_visible_focus_and_escape_uses_close_request(): result = run_node( f""" const createIssueCapture = require({json.dumps(str(MODULE))}); function element(id, visible=true) {{ return {{id, inert:false, hidden:!visible, disabled:false, isConnected:true, focus() {{ document.activeElement = this; }}, getClientRects() {{ return visible ? [{{}}] : []; }} }}; }} const launcher = element('empty-work-create'); const cancel = element('cancel-new-issue'); const hidden = element('hidden-control', false); const submit = element('submit-new-issue'); const listeners = {{}}; const root = {{ querySelector: selector => selector === '#cancel-new-issue' ? cancel : null, querySelectorAll: () => [cancel, hidden, submit], addEventListener: (name, callback) => {{ listeners[name] = callback; }}, removeEventListener() {{}}, }}; const document = {{activeElement: launcher}}; let closes = 0; const lifecycle = createIssueCapture.createModalLifecycle({{ root, background:[], document, requestClose() {{ closes += 1; }}, }}); lifecycle.open(launcher); document.activeElement = submit; let prevented = 0; listeners.keydown({{key:'Tab', shiftKey:false, preventDefault() {{ prevented += 1; }}}}); const wrappedForward = document.activeElement.id; document.activeElement = cancel; listeners.keydown({{key:'Tab', shiftKey:true, preventDefault() {{ prevented += 1; }}}}); const wrappedBackward = document.activeElement.id; listeners.keydown({{key:'Escape', shiftKey:false, preventDefault() {{ prevented += 1; }}}}); process.stdout.write(JSON.stringify({{wrappedForward, wrappedBackward, closes, prevented}})); """ ) assert result == { "wrappedForward": "cancel-new-issue", "wrappedBackward": "submit-new-issue", "closes": 1, "prevented": 3, }