327 lines
12 KiB
JavaScript
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();
|
|
}
|