418 lines
16 KiB
JavaScript
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();
|
|
}
|