// Isolated React component browser test. Requires Vite and Edge CDP on localhost. import fs from 'node:fs'; const baseUrl = process.argv[2] ?? 'http://127.0.0.1:18117/tools/simulator-cashflow-preview.html'; const outputPath = process.argv[3] ?? 'docs/investmoz/simulations/simulator-cashflow-browser-2026-10-03.json'; 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 nextId = 0; const pending = new Map(); const exceptions = []; const network = []; ws.addEventListener('message', (event) => { const message = JSON.parse(event.data); if (message.method === 'Runtime.exceptionThrown') exceptions.push(message.params.exceptionDetails.text); if (message.method === 'Network.responseReceived') network.push({ url: message.params.response.url, status: message.params.response.status, }); if (!message.id) return; const request = pending.get(message.id); pending.delete(message.id); if (!request) return; if (message.error) request.reject(message.error); else request.resolve(message.result); }); const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++nextId; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); }); const evaluate = async (expression) => { const response = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true, }); if (response.exceptionDetails) throw new Error( response.exceptionDetails.exception?.description ?? response.exceptionDetails.text, ); return response.result?.value; }; const waitFor = async (expression, description) => { 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 ${description}`); }; let report; try { await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable'); await send('Page.navigate', { url: baseUrl }); await waitFor( 'document.querySelector(\'#cashflow-root section[aria-labelledby=\\"cashflow-title\\"]\')', 'the rendered cashflow component', ); const fixedMode = await evaluate(`(() => { const fixed = [...document.querySelectorAll('label')].find(label => label.innerText.includes('Valor mensal fixo'))?.querySelector('input[type="radio"]'); const note = [...document.querySelectorAll('[role="note"]')].map(node => node.innerText).find(text => text.includes('saldo total')) ?? null; return { selected: fixed?.checked ?? false, monthlyInputs: [...document.querySelectorAll('label')].filter(label => label.innerText.includes('Imposto assumido por mês')).length, incompleteNote: note }; })()`); const selectedSchedule = await evaluate(`(() => { const label = [...document.querySelectorAll('label')].find(item => item.innerText.includes('Calendário variável por mês')); const radio = label?.querySelector('input[type="radio"]'); radio?.click(); return Boolean(radio); })()`); if (!selectedSchedule) throw new Error('Monthly schedule option is missing.'); await waitFor( "[...document.querySelectorAll('label')].filter(label => label.innerText.includes('Imposto assumido') && /mês \\d+/u.test(label.innerText)).length === 12", '12 labeled monthly assumption inputs', ); const setMonth = async (month, value) => evaluate(`(() => { const label = [...document.querySelectorAll('label')].find(item => item.innerText.includes('Imposto assumido') && item.innerText.includes('mês ${month}')); const input = label?.querySelector('input'); if (!input) return false; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(input, '${value}'); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return true; })()`); for (const [month, value] of [ [1, '250'], [2, '500'], ]) { if (!(await setMonth(month, value))) throw new Error(`Could not update month ${month}.`); } await waitFor( "[...document.querySelectorAll('tbody tr')].some(row => row.querySelector('th')?.innerText.trim() === '1' && row.querySelectorAll('td')[4]?.innerText.trim() === '250.00')", 'monthly assumption to affect the cash projection', ); const partialSchedule = await evaluate(`(() => { const rows = [...document.querySelectorAll('tbody tr')]; const taxFor = month => rows.find(row => row.querySelector('th')?.innerText.trim() === String(month))?.querySelectorAll('td')[4]?.innerText.trim() ?? null; const note = [...document.querySelectorAll('[role="note"]')].map(node => node.innerText).find(text => text.includes('mês vazio')) ?? null; const status = document.body.innerText.includes('Caixa conhecida antes de imposto em falta'); return { firstMonthTax: taxFor(1), secondMonthTax: taxFor(2), thirdMonthTax: taxFor(3), incompleteNote: note, incompleteSummary: status }; })()`); for (let month = 3; month <= 12; month++) { if (!(await setMonth(month, '0'))) throw new Error(`Could not explicitly set month ${month} to zero.`); } await waitFor( "!document.body.innerText.includes('Há pelo menos um mês sem pressuposto de imposto')", 'complete projection after explicit zero assumptions', ); const completeSchedule = await evaluate(`(() => ({ hasCompleteBalance: document.body.innerText.includes('Saldo projectado ao fim de 12 meses (MZN)'), zeroTaxMonths: [...document.querySelectorAll('tbody tr')].slice(2).every(row => row.querySelectorAll('td')[4]?.innerText.trim() === '0.00'), rowCount: document.querySelectorAll('tbody tr').length, }))()`); const responsive = []; for (const width of [320, 360, 390, 768, 1024, 1440]) { await send('Emulation.setDeviceMetricsOverride', { width, height: 900, deviceScaleFactor: 1, mobile: width < 768, }); responsive.push( await evaluate(`(() => ({ requested: ${width}, viewport: innerWidth, documentWidth: document.documentElement.clientWidth, documentScroll: document.documentElement.scrollWidth, bodyScroll: document.body.scrollWidth, hasHorizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1 || document.body.scrollWidth > document.documentElement.clientWidth + 1, inputs: [...document.querySelectorAll('input:not([type="hidden"])')].filter(input => input.getClientRects().length).length, unnamedInputs: [...document.querySelectorAll('input:not([type="hidden"])')].filter(input => input.getClientRects().length && !(input.labels?.length || input.getAttribute('aria-label'))).length, }))()`), ); } await send('Emulation.clearDeviceMetricsOverride'); const ax = await send('Accessibility.getFullAXTree'); const unnamedAxControls = ax.nodes .filter( (node) => !node.ignored && [ 'button', 'textbox', 'radioButton', 'spinbutton', 'link', ].includes(node.role?.value), ) .filter((node) => !node.name?.value?.trim()) .map((node) => node.role.value); report = { checkedAt: new Date().toISOString(), route: baseUrl, browser: 'Microsoft Edge headless via CDP', scope: 'React cashflow component only; no Laravel, API, database, or fiscal rules', fixedMode, partialSchedule, completeSchedule, responsive, unnamedAxControls, javascriptExceptions: exceptions, networkFailures: network.filter((item) => item.status >= 400), passed: fixedMode.selected && fixedMode.monthlyInputs === 1 && Boolean(fixedMode.incompleteNote) && partialSchedule.firstMonthTax === '250.00' && partialSchedule.secondMonthTax === '500.00' && partialSchedule.thirdMonthTax === 'Indeterminado' && Boolean(partialSchedule.incompleteNote) && partialSchedule.incompleteSummary && completeSchedule.hasCompleteBalance && completeSchedule.zeroTaxMonths && completeSchedule.rowCount === 12 && responsive.every( (item) => !item.hasHorizontalOverflow && item.unnamedInputs === 0, ) && unnamedAxControls.length === 0 && exceptions.length === 0 && network.every((item) => item.status < 400), }; fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); console.log(JSON.stringify(report, null, 2)); if (!report.passed) process.exitCode = 1; } catch (error) { report = { checkedAt: new Date().toISOString(), route: baseUrl, browser: 'Microsoft Edge headless via CDP', failure: error instanceof Error ? error.message : String(error), partialReport: report, passed: false, }; fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); console.error(JSON.stringify(report, null, 2)); throw error; } finally { ws.close(); await fetch(`http://127.0.0.1:9224/json/close/${target.id}`); }