Files
Investidor-V2/tools/portal-home-sectors-qa.mjs
T

418 lines
16 KiB
JavaScript

// Public React/Inertia screens with synthetic sectors and local assets; 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.SECTORS_QA_PLAYWRIGHT ||
'C:/Users/jorge/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright',
);
const root = process.cwd();
const base = (
process.env.SECTORS_QA_VITE || fs.readFileSync('public/hot', 'utf8')
).trim();
assert.equal(new URL(base).hostname, '127.0.0.1');
const dir = path.join(root, 'outputs/home-sectors-20261023');
fs.mkdirSync(dir, { recursive: true });
const sectors = [
[
'agronegocio',
'Agronegócio',
'Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.',
],
[
'turismo',
'Turismo',
'Hotelaria, turismo costeiro, cultural e ecológico.',
],
[
'infraestruturas',
'Infraestruturas',
'Logística, transportes, parques industriais e energia.',
],
[
'economia-azul',
'Economia Azul',
'Pesca, aquacultura, portos e logística marítima.',
],
[
'mercado-financeiro',
'Mercado Financeiro',
'Fintech, capital de risco e instrumentos financeiros.',
],
['mineracao', 'Mineração', null],
['energia-especifica', 'Energia', null],
['imobiliario', 'Imobiliário', null],
['bens-de-consumo-e-franquias', 'Bens de Consumo e Franquias', null],
['tecnologias-de-informacao', 'Tecnologias de Informação', null],
].map(([slug, name, description], index) => ({
id: index + 1,
slug,
name,
shortDescription: description,
short_description: description,
description,
opportunitiesCount: index === 4 ? 4 : 0,
opportunities_count: index === 4 ? 4 : 0,
url: `/sectores/${slug}`,
specializations:
index === 4
? [
{
id: 901,
name: 'Mercado Financeiro — Fintech',
slug: 'fintech',
url: '/sectores/mercado-financeiro/fintech',
},
]
: [],
}));
const fixturePath = '/outputs/home-sectors-20261023/fixture.html';
const html =
'<!doctype html><html lang="pt"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sectores QA</title></head><body><div id="app"></div><script type="module" src="/outputs/home-sectors-20261023/fixture.tsx"></script></body></html>';
fs.writeFileSync(path.join(dir, 'fixture.html'), 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 Services from '../../resources/js/pages/portal/services';
import Navigation from '../../resources/js/layouts/portal-navigation';
import '../../resources/css/app.css';
const sectors=${JSON.stringify(sectors)};
const url=new URL(location.href),catalogue=url.pathname==='/sectores',detail=url.pathname.startsWith('/sectores/');
const selected=sectors.find(sector=>sector.url===url.pathname)||{id:901,name:'Fintech',slug:'fintech',description:null,short_description:null};
const props={auth:{user:null,canManagePortal:false},countries:[],essentialResources:[],...(catalogue?{section:'sectors',sectors:url.searchParams.has('empty')?[]:sectors}:detail?{section:'sector',sector:selected,opportunities:[]}:{} )};
await createInertiaApp({page:{component:catalogue||detail?'portal/services':'PortalInvestidor',props,url:url.pathname+url.search,version:null},resolve:()=>catalogue||detail?Services:Portal,layout:()=>Navigation,setup({el,App,props}){createRoot(el).render(<App {...props}/>);}});
`,
);
const browser = await chromium.launch({
headless: true,
...(process.env.SECTORS_QA_CHROMIUM
? { executablePath: process.env.SECTORS_QA_CHROMIUM }
: {}),
});
// Full navigations need Vite's transformed HTML, including the React preamble.
const fixtureResponse = await fetch(base + fixturePath);
assert.ok(fixtureResponse.ok);
const navigationHtml = await fixtureResponse.text();
const errors = [],
results = [],
navigation = [];
let currentPage;
async function openCase(width, { empty = false, fail = 'none' } = {}) {
const context = await browser.newContext({
viewport: { width, height: 1000 },
reducedMotion: 'reduce',
});
await context.routeWebSocket('**/*', () => {});
await context.route('**/*', (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/sectors/') &&
(fail === 'all' ||
(fail === 'primary' &&
!url.pathname.includes('outros-sectores')))
)
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: empty ? [] : sectors,
featuredOpportunities: [],
stats: {
opportunities: 4,
sectors: empty ? 0 : 10,
provinces: 3,
},
},
},
});
if (url.pathname.startsWith('/api/'))
return route.fulfill({ json: { data: [] } });
if (
route.request().isNavigationRequest() &&
(url.pathname === '/sectores' ||
url.pathname.startsWith('/sectores/') ||
url.pathname === '/')
) {
navigation.push(url.pathname);
return route.fulfill({
contentType: 'text/html',
body: navigationHtml,
});
}
return route.continue();
});
const page = await context.newPage();
currentPage = page;
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(base + fixturePath, {
waitUntil: 'networkidle',
timeout: 60000,
});
return { context, page };
}
async function loadImages(region) {
for (const image of await region.locator('img').all()) {
await image.scrollIntoViewIfNeeded();
await region
.page()
.waitForFunction(
(node) => node.complete && node.naturalWidth > 0,
await image.elementHandle(),
{ timeout: 15000 },
);
}
}
try {
for (const width of [320, 390, 768, 1024, 1440, 1920]) {
const { context, page } = await openCase(width);
const section = page.getByRole('region', {
name: 'Sectores de investimento',
exact: true,
});
await section.waitFor();
await loadImages(section);
assert.equal(await section.getByRole('article').count(), 5);
assert.deepEqual(
await section.locator('h3').allTextContents(),
sectors.slice(0, 5).map((s) => s.name),
);
assert.equal(
await section
.getByRole('link', { name: /^Ler mais sobre / })
.count(),
5,
);
assert.ok(
await section.getByText('0 oportunidades', { exact: true }).count(),
);
assert.equal(
await section.getByText('4 oportunidades', { exact: true }).count(),
1,
);
assert.equal(
await section
.getByRole('link', { name: 'Especialização: Fintech' })
.getAttribute('href'),
'/sectores/mercado-financeiro/fintech',
);
const headings = await page.locator('main h2').evaluateAll((nodes) =>
nodes.map((node) => ({
title: node.textContent.trim(),
size: getComputedStyle(node).fontSize,
weight: getComputedStyle(node).fontWeight,
color: getComputedStyle(node).color,
lineHeight: getComputedStyle(node).lineHeight,
})),
);
assert.equal(headings.length, 6);
headings.forEach((heading) => {
assert.equal(heading.color, 'rgb(237, 28, 36)');
assert.equal(heading.size, '24px');
assert.equal(heading.weight, '700');
assert.equal(heading.lineHeight, '32px');
});
const homeColumns = await section
.locator(':scope > div')
.first()
.locator(':scope > div')
.evaluate(
(node) =>
getComputedStyle(node).gridTemplateColumns.split(' ')
.length,
);
assert.equal(homeColumns, width >= 1024 ? 5 : width >= 640 ? 3 : 2);
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
),
false,
);
await section.screenshot({
path: path.join(dir, `home-sectors-${width}.png`),
});
if (width === 1440)
await page.screenshot({
path: path.join(dir, 'homepage-1440.png'),
fullPage: true,
});
const more = section.getByRole('link', {
name: 'Ver mais sectores',
exact: true,
});
assert.equal(await more.getAttribute('href'), '/sectores');
await more.focus();
await page.keyboard.press('Enter');
await page.waitForURL(base + '/sectores');
await page
.getByRole('heading', {
name: 'Sectores de investimento',
exact: true,
})
.waitFor();
const catalogue = page.getByRole('region', {
name: 'Todos os sectores de investimento',
});
await loadImages(catalogue);
assert.equal(await catalogue.getByRole('article').count(), 10);
assert.deepEqual(
await catalogue.locator('h3').allTextContents(),
sectors.map((s) => s.name),
);
assert.equal(
await catalogue
.getByRole('link', { name: /^Ler mais sobre / })
.count(),
10,
);
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
),
false,
);
await catalogue.screenshot({
path: path.join(dir, `catalogue-${width}.png`),
});
if (width === 1440) {
for (const sector of sectors) {
await catalogue
.getByRole('link', {
name: `Ler mais sobre ${sector.name}`,
exact: true,
})
.click();
await page.waitForURL(base + sector.url);
await page
.getByRole('heading', { name: sector.name, exact: true })
.waitFor();
await page.goto(base + '/sectores', {
waitUntil: 'networkidle',
});
await catalogue.waitFor();
}
await catalogue
.getByRole('link', { name: 'Especialização: Fintech' })
.click();
await page.waitForURL(
base + '/sectores/mercado-financeiro/fintech',
);
await page
.getByRole('heading', { name: 'Fintech', exact: true })
.waitFor();
await page.goto(base + '/sectores', { waitUntil: 'networkidle' });
await catalogue
.getByRole('link', { name: 'Voltar ao início' })
.click();
await page.waitForURL(base + '/');
await page
.getByRole('region', {
name: 'Sectores de investimento',
exact: true,
})
.waitFor();
}
results.push({
width,
homeSectors: 5,
catalogueSectors: 10,
homeColumns,
headings,
status: 'passed',
});
await context.close();
}
for (const fail of ['primary', 'all']) {
const { context, page } = await openCase(390, { fail });
const section = page.getByRole('region', {
name: 'Sectores de investimento',
exact: true,
});
await section.waitFor();
for (const article of await section.getByRole('article').all())
await article.scrollIntoViewIfNeeded();
await page.waitForFunction((mode) => {
const cards = [
...document.querySelectorAll(
'#home-sectors-heading ~ div article',
),
];
return (
cards.length === 5 &&
cards.every((card) =>
mode === 'all'
? !card.querySelector('img')
: card.querySelector('img')?.complete &&
card.querySelector('img')?.naturalWidth > 0 &&
card
.querySelector('img')
?.currentSrc.includes('outros-sectores'),
)
);
}, fail);
assert.equal(
await section
.getByRole('link', { name: /^Ler mais sobre / })
.count(),
5,
);
await section.screenshot({
path: path.join(dir, `fallback-${fail}.png`),
});
results.push({ fail, status: 'passed' });
await context.close();
}
const { context, page } = await openCase(390, { empty: true });
const section = page.getByRole('region', {
name: 'Sectores de investimento',
exact: true,
});
await section.getByText('Não existem sectores publicados.').waitFor();
assert.equal(await section.getByRole('article').count(), 0);
await page.goto(base + '/sectores?empty=1', { waitUntil: 'networkidle' });
await page.getByText('Não existem sectores publicados.').waitFor();
results.push({ emptyCatalogue: true, 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();
}