Files
Investidor-V2/tools/simulator-cashflow-component-browser.mjs
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

229 lines
9.9 KiB
JavaScript

// 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}`);
}