// Read-only responsive/accessibility smoke test against an isolated local Laravel page and Edge CDP. import fs from 'node:fs'; const baseUrl = process.argv[2] ?? 'http://127.0.0.1:18082/simulador-fiscal'; const outputPath = process.argv[3] ?? 'docs/investmoz/simulations/simulator-browser-audit-2026-10-03.json'; const expectedQaTarget = process.argv[4] ?? null; const target = await ( await fetch('http://127.0.0.1:9224/json/new?about:blank', { method: 'PUT' }) ).json(); const ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise((resolve, reject) => { ws.addEventListener('open', resolve, { once: true }); ws.addEventListener('error', reject, { once: true }); }); let id = 0; const pending = new Map(); const errors = []; const apiResponses = []; ws.addEventListener('message', (event) => { const message = JSON.parse(event.data); if (message.method === 'Runtime.exceptionThrown') { errors.push(message.params.exceptionDetails.text); } if (message.method === 'Network.responseReceived') { const response = message.params.response; const path = new URL(response.url).pathname; if (path.startsWith('/api/v1/')) { const targetHeader = Object.entries(response.headers ?? {}).find( ([name]) => name.toLowerCase() === 'x-investmoz-qa-target', )?.[1]; apiResponses.push({ path, status: response.status, qaTarget: typeof targetHeader === 'string' ? targetHeader : null, }); } } if (!message.id) return; const request = pending.get(message.id); pending.delete(message.id); if (message.error) request.reject( new Error( `CDP ${request.method}: ${message.error.message ?? 'request failed'}`, ), ); else request.resolve(message.result); }); const send = (method, params = {}) => new Promise((resolve, reject) => { const requestId = ++id; pending.set(requestId, { resolve, reject, method }); ws.send(JSON.stringify({ id: requestId, method, params })); }); const evaluate = async (expression) => { let response; try { response = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true, }); } catch (error) { throw new Error( `Runtime.evaluate [${expression.slice(0, 120)}]: ${error.message}`, ); } if (response.exceptionDetails) throw new Error('Page evaluation failed'); return response.result?.value; }; const waitFor = async (expression) => { for (let attempt = 0; attempt < 100; attempt++) { if (await evaluate(`Boolean(${expression})`)) return; await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error(`Timed out waiting for ${expression}`); }; const waitForApiResponse = async () => { for (let attempt = 0; attempt < 100; attempt++) { if (apiResponses.length > 0) return; await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error('Timed out waiting for the public draft API response'); }; try { await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable'); await send('Page.navigate', { url: baseUrl }); await waitFor("document.querySelector('main #simulator-step-title')"); await waitForApiResponse(); const dimensions = []; for (const width of [390, 768, 1024, 1440]) { await send('Emulation.setDeviceMetricsOverride', { width, height: 900, deviceScaleFactor: 1, mobile: width < 768, }); await new Promise((resolve) => setTimeout(resolve, 100)); const result = await evaluate(`(() => { const controls = [...document.querySelectorAll('input:not([type="hidden"]), select, textarea, button')] .filter(e => e.getClientRects().length); const fixed = controls.filter(e => getComputedStyle(e).position === 'fixed'); return { viewport: innerWidth, document: document.documentElement.clientWidth, documentScroll: document.documentElement.scrollWidth, bodyScroll: document.body.scrollWidth, fixedControls: fixed.length, controls: controls.length, }; })()`); dimensions.push(result); } await send('Emulation.setDeviceMetricsOverride', { width: 390, height: 900, deviceScaleFactor: 1, mobile: true, }); await evaluate("document.querySelector('#simulator-step-title').focus()"); const keyboardFocusSequence = []; for (let index = 0; index < 12; index++) { await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9, nativeVirtualKeyCode: 9, }); await send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9, nativeVirtualKeyCode: 9, }); keyboardFocusSequence.push( await evaluate(`(() => { const e = document.activeElement; return { tag: e?.tagName?.toLowerCase() ?? null, name: e?.getAttribute?.('aria-label') || e?.innerText?.trim() || e?.labels?.[0]?.innerText?.trim() || e?.title || null, visible: Boolean(e?.getClientRects?.().length), focusVisible: Boolean(e?.matches?.(':focus-visible')), }; })()`), ); } await send('Emulation.clearDeviceMetricsOverride'); const domAudit = await evaluate(`(() => { const visible = e => e.getClientRects().length > 0 && getComputedStyle(e).visibility !== 'hidden'; const controls = [...document.querySelectorAll('input:not([type="hidden"]), select, textarea')] .filter(visible).map(e => ({ tag: e.tagName.toLowerCase(), type: e.type ?? null, name: e.name || null, label: e.labels?.[0]?.innerText?.trim() || e.getAttribute('aria-label') || (e.getAttribute('aria-labelledby') ? document.getElementById(e.getAttribute('aria-labelledby'))?.innerText?.trim() : null), })); const buttons = [...document.querySelectorAll('button')].filter(visible).map(e => ({ text: e.innerText.trim(), name: e.getAttribute('aria-label') || e.innerText.trim() || e.title || null, })); const links = [...document.querySelectorAll('a')].filter(visible).map(e => ({ text: e.innerText.trim(), name: e.getAttribute('aria-label') || e.innerText.trim() || e.title || null, href: e.getAttribute('href'), })); return { title: document.title, controls, buttons, links }; })()`); const axTree = await send('Accessibility.getFullAXTree'); const controlRoles = new Set([ 'button', 'textbox', 'searchbox', 'combobox', 'checkbox', 'radioButton', 'link', 'slider', 'spinbutton', ]); const unnamedAxControls = axTree.nodes .filter((node) => !node.ignored && controlRoles.has(node.role?.value)) .filter((node) => !node.name?.value?.trim()) .map((node) => node.role.value); const unlabeledControls = domAudit.controls.filter( (control) => !control.label, ); const unnamedButtons = domAudit.buttons.filter((button) => !button.name); const unnamedLinks = domAudit.links.filter((link) => !link.name); const visibleAlerts = await evaluate(`[...document.querySelectorAll('[role="alert"]')] .filter(e => e.getClientRects().length > 0) .map(e => e.innerText.trim()) .filter(Boolean)`); const noOverflow = dimensions.every( (item) => item.documentScroll <= item.document + 1 && item.bodyScroll <= item.document + 1, ); const invalidKeyboardFocus = keyboardFocusSequence.filter( (item) => !item.name || !item.visible, ); const apiFailures = apiResponses.filter( (response) => response.status < 200 || response.status >= 300, ); const qaTargetVerified = expectedQaTarget === null || (apiResponses.length > 0 && apiResponses.every( (response) => response.qaTarget === expectedQaTarget, )); const report = { checkedAt: new Date().toISOString(), route: baseUrl, browser: 'Microsoft Edge headless via CDP', scope: 'UI, foco e GET público de rascunho; sem cálculo, escrita ou validação de conteúdo persistido', title: domAudit.title, dimensions, keyboardFocusSequence, invalidKeyboardFocus, visibleControls: domAudit.controls.length, visibleButtons: domAudit.buttons.length, visibleLinks: domAudit.links.length, unlabeledControls, unnamedButtons, unnamedLinks, unnamedAxControls, visibleAlerts, javascriptExceptions: errors, apiResponses, apiFailures, expectedQaTarget, qaTargetVerified, passed: noOverflow && invalidKeyboardFocus.length === 0 && unlabeledControls.length === 0 && unnamedButtons.length === 0 && unnamedLinks.length === 0 && unnamedAxControls.length === 0 && visibleAlerts.length === 0 && apiResponses.length > 0 && qaTargetVerified && apiFailures.length === 0 && errors.length === 0, }; fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); console.log(JSON.stringify(report, null, 2)); if (!report.passed) process.exitCode = 1; } finally { ws.close(); await fetch(`http://127.0.0.1:9224/json/close/${target.id}`); }