Files
Investidor-V2/tools/portal-context-cards-qa.mjs
T

327 lines
12 KiB
JavaScript

// Actual public portal and navigation, synthetic API, local images, no database.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { chromium } = require(
process.env.CONTEXT_QA_PLAYWRIGHT ||
'C:/Users/jorge/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright',
);
const root = process.cwd();
const base = (
process.env.CONTEXT_QA_VITE || fs.readFileSync('public/hot', 'utf8')
).trim();
assert.equal(new URL(base).hostname, '127.0.0.1');
const dir = path.join(root, 'outputs/investment-context-compact-20261023');
fs.mkdirSync(dir, { recursive: true });
const fixturePath = '/outputs/investment-context-compact-20261023/fixture.html';
fs.writeFileSync(
path.join(dir, 'fixture.html'),
`<!doctype html><html lang="pt"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Cartões QA</title></head><body><div id="app"></div><script type="module" src="/outputs/investment-context-compact-20261023/fixture.tsx"></script></body></html>`,
);
fs.writeFileSync(
path.join(dir, 'fixture.tsx'),
`
import React from 'react';
import {createRoot} from 'react-dom/client';
import {createInertiaApp} from '@inertiajs/react';
import Portal from '../../resources/js/pages/PortalInvestidor';
import Navigation from '../../resources/js/layouts/portal-navigation';
import '../../resources/css/app.css';
await createInertiaApp({page:{component:'PortalInvestidor',props:{auth:{user:null,canManagePortal:false},essentialResources:[],countries:[]},url:'/',version:null},resolve:()=>Portal,layout:()=>Navigation,setup({el,App,props}){createRoot(el).render(<App {...props}/>);}});
`,
);
const destinations = [
['/clusters', 'Clusters', 'Cadeias de valor e lacunas produtivas.'],
['/corredores', 'Corredores', 'Territórios, logística e conexões.'],
[
'/projectos-ancora',
'Projectos âncora',
'Projectos estruturantes e relações.',
],
['/educacao', 'Educação', 'Investimentos em competências e formação.'],
[
'/documentos',
'Documentos e Formulários',
'Legislação, procedimentos, formulários e materiais de apoio.',
],
];
const browser = await chromium.launch({
headless: true,
...(process.env.CONTEXT_QA_CHROMIUM
? { executablePath: process.env.CONTEXT_QA_CHROMIUM }
: {}),
});
const errors = [],
results = [],
navigation = [];
let currentPage;
async function createCase(width, dpr = 1, fail = 'none') {
const context = await browser.newContext({
viewport: { width, height: 1000 },
deviceScaleFactor: dpr,
reducedMotion: 'reduce',
});
// Avoid development HMR reloading the fixture's synthetic homepage URL.
await context.routeWebSocket('**/*', () => {});
let releaseImages;
const imagesReady = new Promise((resolve) => {
releaseImages = resolve;
});
await context.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (url.origin !== base) return route.abort();
if (url.pathname.startsWith('/images/')) {
if (url.pathname.startsWith('/images/investment-context/')) {
await imagesReady;
if (
fail === 'all' ||
(fail === 'primary' && !url.pathname.includes('-fallback'))
)
return route.fulfill({ status: 404 });
}
const file = path.resolve(root, 'public', '.' + url.pathname);
assert.ok(
file.startsWith(path.join(root, 'public', 'images') + path.sep),
);
return fs.existsSync(file)
? route.fulfill({ path: file })
: route.fulfill({ status: 404 });
}
if (url.pathname === '/api/v1/home')
return route.fulfill({
json: {
success: true,
data: {
sectors: [],
featuredOpportunities: [],
stats: { opportunities: 0, sectors: 0, provinces: 0 },
},
},
});
if (url.pathname.startsWith('/api/'))
return route.fulfill({ json: { data: [] } });
if (
route.request().isNavigationRequest() &&
destinations.some(([href]) => url.pathname === href)
) {
navigation.push(url.pathname);
return route.fulfill({
contentType: 'text/html',
body: '<!doctype html><html><body><h1>Destino de navegação de teste</h1></body></html>',
});
}
return route.continue();
});
const page = await context.newPage();
currentPage = page;
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base + fixturePath, {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
const section = page.getByRole('region', {
name: 'Encontre o contexto do seu investimento',
});
await section.waitFor();
return { context, page, section, releaseImages };
}
try {
for (const [width, dpr] of [
[320, 1],
[390, 2],
[768, 1],
[1024, 1],
[1440, 2],
[1920, 1],
]) {
const { context, page, section, releaseImages } = await createCase(
width,
dpr,
);
const links = section.getByRole('link');
assert.equal(await links.count(), 5);
assert.equal(await section.locator('img').count(), 5);
const before = await links.evaluateAll((nodes) =>
nodes.map((node) => node.getBoundingClientRect().height),
);
releaseImages();
for (const img of await section.locator('img').all()) {
await img.scrollIntoViewIfNeeded();
await img.evaluate((image) => image.decode());
}
const photos = await section.locator('img').evaluateAll((nodes) =>
nodes.map((image) => ({
src: image.currentSrc,
alt: image.alt,
lazy: image.loading,
width: image.naturalWidth,
height: image.naturalHeight,
fit: getComputedStyle(image).objectFit,
ratio:
image.getBoundingClientRect().width /
image.getBoundingClientRect().height,
})),
);
assert.equal(new Set(photos.map((image) => image.src)).size, 5);
photos.forEach((image) => {
assert.ok(image.alt.length > 20);
assert.equal(image.lazy, 'lazy');
// naturalWidth is density corrected when srcset uses width descriptors.
assert.ok(image.width > 0 && image.height > 0);
assert.equal(image.fit, 'cover');
assert.ok(Math.abs(image.ratio - 16 / 9) < 0.01);
});
const after = await links.evaluateAll((nodes) =>
nodes.map((node) => node.getBoundingClientRect().height),
);
before.forEach((height, index) =>
assert.ok(
Math.abs(height - after[index]) < 1,
'Image caused card layout shift',
),
);
const rows = await links.evaluateAll((nodes) =>
nodes.map((node) => {
const rect = node.getBoundingClientRect();
return { top: rect.top, height: rect.height };
}),
);
for (const row of rows) {
const aligned = rows.filter(
(item) => Math.abs(item.top - row.top) < 1,
);
assert.ok(
aligned.every((item) => Math.abs(item.height - row.height) < 1),
'Unequal heights within a card row',
);
}
const columns = await section
.locator('nav')
.evaluate(
(node) =>
getComputedStyle(node).gridTemplateColumns.split(' ')
.length,
);
assert.equal(columns, width >= 1024 ? 4 : width >= 640 ? 2 : 1);
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
),
false,
);
for (const [href, title, description] of destinations) {
const link = section.getByRole('link', {
name: title,
exact: true,
});
assert.equal(await link.getAttribute('href'), href);
assert.equal(
(await link.locator('p').textContent()).trim(),
description,
);
}
assert.equal(await page.locator('.tempo-header').count(), 1);
assert.equal(await page.locator('.portal-hero').count(), 1);
await section.screenshot({
path: path.join(dir, `cards-${width}-dpr${dpr}.png`),
});
if (width === 1440) {
for (const [href, title] of destinations) {
const link = section.getByRole('link', {
name: title,
exact: true,
});
await link.focus();
await page.keyboard.press('Enter');
await page.waitForURL(base + href);
assert.equal(navigation.at(-1), href);
await page.goto(base + fixturePath, {
waitUntil: 'networkidle',
});
await section.waitFor();
}
}
results.push({
width,
dpr,
columns,
photos: photos.map(({ src, width, height }) => ({
src,
width,
height,
})),
status: 'passed',
});
await context.close();
}
for (const fail of ['primary', 'all']) {
const { context, page, section, releaseImages } = await createCase(
390,
2,
fail,
);
releaseImages();
for (const link of await section.getByRole('link').all())
await link.scrollIntoViewIfNeeded();
if (fail === 'primary') {
await page.waitForFunction(
() =>
[
...document.querySelectorAll(
'#investment-context-heading + nav img',
),
].length === 5 &&
[
...document.querySelectorAll(
'#investment-context-heading + nav img',
),
].every(
(image) =>
image.complete &&
image.naturalWidth > 0 &&
image.currentSrc.includes('-fallback'),
),
);
} else {
await section
.getByText('Imagem indisponível', { exact: true })
.nth(4)
.waitFor();
assert.equal(await section.getByRole('link').count(), 5);
assert.equal(await section.locator('[role="img"]').count(), 5);
}
await section.screenshot({
path: path.join(dir, `fallback-${fail}.png`),
});
results.push({ fail, status: 'passed' });
await context.close();
}
assert.deepEqual(errors, []);
fs.writeFileSync(
path.join(dir, 'qa-results.json'),
JSON.stringify(
{ results, errors, navigation, databaseAccess: false },
null,
2,
),
);
console.log(JSON.stringify({ cases: results.length, errors, navigation }));
} catch (error) {
if (currentPage && !currentPage.isClosed()) {
await Promise.all([
Promise.resolve(
currentPage.screenshot({ path: path.join(dir, 'failure.png') }),
),
Promise.resolve(currentPage.content()).then((html) =>
fs.writeFileSync(path.join(dir, 'failure.html'), html),
),
]);
}
throw error;
} finally {
await browser.close();
}