Files
2026-10-23 10:36:34 +02:00

418 lines
16 KiB
JavaScript

// Real portal components in Vite, synthetic public data, no Laravel/database access.
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.HERO_QA_PLAYWRIGHT ||
'C:/Users/jorge/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright',
);
const root = process.cwd();
const base = process.env.HERO_QA_VITE || 'http://127.0.0.1:5173';
assert.equal(new URL(base).hostname, '127.0.0.1');
const dir = path.join(root, 'outputs/portal-hero-no-controls-20261023');
const before = process.argv.includes('--before');
fs.mkdirSync(dir, { recursive: true });
if (before) assert.ok(!fs.existsSync(path.join(dir, 'before-results.json')));
const fixtureProps = {
auth: { user: null, canManagePortal: false },
essentialResources: [],
countries: [],
};
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>Portal QA</title></head>
<body><div id="app"></div><script type="module" src="/outputs/portal-hero-no-controls-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 PortalNavigation from '../../resources/js/layouts/portal-navigation';
import '../../resources/css/app.css';
await createInertiaApp({page:{component:'PortalInvestidor',props:${JSON.stringify(fixtureProps)},url:'/',version:null},
resolve:()=>Portal,layout:()=>PortalNavigation,setup({el,App,props}){createRoot(el).render(<App {...props}/>);}});
`,
);
const browser = await chromium.launch({
headless: true,
...(process.env.HERO_QA_CHROMIUM
? { executablePath: process.env.HERO_QA_CHROMIUM }
: {}),
});
const results = [];
const errors = [];
const requests = [];
let activePage;
try {
const cases = before
? [
[1440, 'reduce'],
[390, 'reduce'],
]
: [
[320, 'reduce'],
[390, 'reduce'],
[768, 'reduce'],
[1440, 'reduce'],
[1920, 'reduce'],
[390, 'no-preference'],
[1440, 'no-preference'],
];
for (const [width, reducedMotion] of cases) {
const context = await browser.newContext({
viewport: { width, height: 1000 },
reducedMotion,
});
await context.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (url.origin !== base) return route.abort();
if (url.pathname.startsWith('/images/')) {
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: [
{
slug: 'energia',
name: 'Energia',
shortDescription: 'Energia de demonstração',
opportunitiesCount: 6,
},
],
stats: {
opportunities: 6,
sectors: 1,
provinces: 3,
},
featuredOpportunities: [],
},
},
});
if (url.pathname.startsWith('/api/'))
return route.fulfill({
json: { data: [], total: 0, current_page: 1, last_page: 1 },
});
if (route.request().headers()['x-inertia']) {
requests.push(url.pathname + url.search);
return route.fulfill({
headers: { 'X-Inertia': 'true' },
json: {
component: 'PortalInvestidor',
props: {
...fixtureProps,
initialView: 'opportunities',
initialQuery: url.searchParams.get('q') || '',
},
url: url.pathname + url.search,
version: null,
},
});
}
return route.continue();
});
const page = await context.newPage();
activePage = page;
page.on('pageerror', (error) => errors.push(error.message));
await page.goto(
`${base}/outputs/portal-hero-no-controls-20261023/fixture.html`,
{
waitUntil: 'networkidle',
timeout: 60000,
},
);
const search = page.locator('#portal-opportunity-search');
await search.waitFor();
await page.getByText('6', { exact: true }).first().waitFor();
assert.equal(await page.locator('h1').count(), 1);
assert.equal(await page.locator('.tempo-header').count(), 1);
await page
.locator('.portal-hero img')
.evaluateAll((images) =>
Promise.all(images.map((image) => image.decode())),
);
await page.screenshot({
path: path.join(
dir,
`${before ? 'before' : 'after'}-${width}-${reducedMotion}.png`,
),
fullPage: false,
});
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
),
false,
`overflow at ${width}`,
);
assert.ok(
await page
.getByRole('link', {
name: /^Documentos e Formulários/,
})
.count(),
);
if (!before) {
const hero = page.getByRole('region', {
name: 'INVISTA EM MOÇAMBIQUE',
});
assert.equal(
await hero
.getByRole('group', { name: 'Imagens de apresentação' })
.count(),
0,
);
assert.equal(
await hero
.getByRole('button', {
name: /Mostrar imagem|Pausar apresentação|Retomar apresentação/,
})
.count(),
0,
);
assert.equal(
await hero.getByRole('link', { name: 'Termos de uso' }).count(),
0,
);
const images = hero.locator('img');
assert.equal(await images.count(), 2);
assert.ok(
await images.evaluateAll((nodes) =>
nodes.every(
(image) => image.complete && image.naturalWidth > 0,
),
),
);
const fullTitle = 'INVISTA EM MOÇAMBIQUE';
const fullSubtitle =
'Transforme informação em decisões e ideias em negócios reais.';
const title = hero.locator('h1 [data-hero-typed]');
const subtitle = hero.locator('p [data-hero-typed]');
if (reducedMotion === 'no-preference') {
await search.focus();
const positionBefore = await search.boundingBox();
await page.waitForFunction(
({ heading, tagline }) =>
document.querySelector('h1 [data-hero-typed]')
.textContent === heading &&
document.querySelector('p [data-hero-typed]')
.textContent === tagline,
{ heading: fullTitle, tagline: fullSubtitle },
);
const positionAfter = await search.boundingBox();
assert.ok(
Math.abs(positionAfter.y - positionBefore.y) < 0.5,
'Search moved during typing',
);
await page.screenshot({
path: path.join(dir, 'typed-' + width + '.png'),
});
const active = await images.evaluateAll((nodes) =>
nodes.findIndex((node) =>
node.classList.contains('opacity-100'),
),
);
await page.waitForTimeout(8300);
assert.equal(
await images.evaluateAll((nodes) =>
nodes.findIndex((node) =>
node.classList.contains('opacity-100'),
),
),
active,
);
await search.evaluate((element) => element.blur());
await page.mouse.move(0, 0);
await page.waitForFunction(
(previous) =>
[
...document.querySelectorAll('.portal-hero img'),
].findIndex((image) =>
image.classList.contains('opacity-100'),
) !== previous,
active,
{ timeout: 12000 },
);
assert.ok(
(await title.textContent()).length < fullTitle.length,
'Automatic slide did not replay typing',
);
await page.waitForFunction(
({ heading, tagline }) =>
document.querySelector('h1 [data-hero-typed]')
.textContent === heading &&
document.querySelector('p [data-hero-typed]')
.textContent === tagline,
{ heading: fullTitle, tagline: fullSubtitle },
);
await page.screenshot({
path: path.join(dir, 'second-image-' + width + '.png'),
});
} else {
assert.equal(await title.textContent(), fullTitle);
assert.equal(await subtitle.textContent(), fullSubtitle);
assert.equal(
await hero.locator('.portal-hero-caret').count(),
0,
);
}
}
// Exercise the real parent handler and Inertia request with accents and punctuation.
await search.fill(' energia & Nampula / Moçambique ');
if (!before && width === 390) {
await search.focus();
await page.keyboard.press('Tab');
assert.equal(
await page
.getByRole('button', { name: 'Pesquisar', exact: true })
.evaluate((el) => el === document.activeElement),
true,
);
await page.keyboard.press('Enter');
} else if (!before && width === 768) {
await page
.getByRole('button', { name: 'Pesquisar', exact: true })
.click();
} else {
await search.press('Enter');
}
await page
.getByRole('heading', {
name: 'Oportunidades de investimento',
exact: true,
})
.waitFor();
const last = new URL(requests.at(-1), base);
assert.equal(last.pathname, '/oportunidades');
assert.equal(
last.searchParams.get('q'),
before
? ' energia & Nampula / Moçambique '
: 'energia & Nampula / Moçambique',
);
if (!before && width === 390) {
const home = `${base}/outputs/portal-hero-no-controls-20261023/fixture.html`;
await page.goto(home, { waitUntil: 'networkidle' });
await page.locator('#portal-opportunity-search').fill(' ');
await page
.getByRole('button', { name: 'Pesquisar', exact: true })
.click();
await page
.getByRole('heading', {
name: 'Oportunidades de investimento',
exact: true,
})
.waitFor();
assert.equal(
new URL(requests.at(-1), base).searchParams.get('q'),
'',
);
await page.goto(home, { waitUntil: 'networkidle' });
await page
.getByRole('button', {
name: 'Consultar oportunidades',
exact: true,
})
.click();
await page
.getByRole('heading', {
name: 'Oportunidades de investimento',
exact: true,
})
.waitFor();
assert.equal(
new URL(requests.at(-1), base).pathname,
'/oportunidades',
);
await context.route('**/api/v1/home', (route) =>
route.fulfill({
status: 500,
json: { message: 'Synthetic failure' },
}),
);
await page.goto(home, { waitUntil: 'networkidle' });
await page.getByRole('alert').waitFor();
assert.deepEqual(
await page
.locator('dl[aria-label="Indicadores do portal"] dd')
.allTextContents(),
['—', '—', '—'],
);
await context.route('**/api/v1/home', (route) =>
route.fulfill({
json: {
success: true,
data: {
sectors: [],
featuredOpportunities: [],
stats: {
opportunities: 0,
sectors: 0,
provinces: 0,
},
},
},
}),
);
await page.goto(home, { waitUntil: 'networkidle' });
assert.deepEqual(
await page
.locator('dl[aria-label="Indicadores do portal"] dd')
.allTextContents(),
['0', '0', '0'],
);
}
results.push({ width, reducedMotion, status: 'passed' });
await context.close();
}
assert.deepEqual(errors, []);
fs.writeFileSync(
path.join(dir, before ? 'before-results.json' : 'qa-results.json'),
JSON.stringify(
{ results, errors, requests, databaseAccess: false },
null,
2,
),
);
console.log(JSON.stringify({ results, errors }));
} catch (error) {
if (activePage && !activePage.isClosed()) {
await activePage.screenshot({ path: path.join(dir, 'failure.png') });
fs.writeFileSync(
path.join(dir, 'failure.html'),
await activePage.content(),
);
console.error(
JSON.stringify(
await activePage.locator('h1').evaluateAll((nodes) =>
nodes.map((node) => ({
text: node.textContent,
html: node.outerHTML,
})),
),
),
);
console.error(JSON.stringify(errors));
}
throw error;
} finally {
await browser.close();
}