390 lines
15 KiB
JavaScript
390 lines
15 KiB
JavaScript
// Isolated UI fixture: real header/providers, synthetic Inertia props, no database.
|
|
// Requires the local Vite server. No requests to Laravel or external services.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import { portalNavigation } from '../resources/js/lib/portal-navigation.ts';
|
|
|
|
const root = process.cwd();
|
|
const dir = path.join(root, 'outputs/header-navbar-20261021');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const require = createRequire(import.meta.url);
|
|
const { chromium } = require(process.env.HEADER_QA_PLAYWRIGHT || 'playwright');
|
|
const { expect } = require(
|
|
(process.env.HEADER_QA_PLAYWRIGHT || 'playwright') + '/test',
|
|
);
|
|
const base = process.env.HEADER_QA_VITE || 'http://127.0.0.1:5173';
|
|
assert.equal(new URL(base).hostname, '127.0.0.1');
|
|
const before = process.argv.includes('--before');
|
|
if (before && fs.existsSync(path.join(dir, 'before-results.json'))) {
|
|
throw new Error(
|
|
'The original baseline already exists and must be preserved.',
|
|
);
|
|
}
|
|
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>InvesteMoz · revisão do header</title></head><body><div id="app"></div>
|
|
<script type="module" src="/outputs/header-navbar-20261021/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 { PortalGroupedMenu } from '../../resources/js/components/portal-grouped-menu';
|
|
import { TempoUiProvider, useTempoUi } from '../../resources/js/components/tempo-brand';
|
|
import '../../resources/css/app.css';
|
|
const role = new URLSearchParams(location.search).get('role') || 'guest';
|
|
const props = {
|
|
auth: { user: role === 'guest' ? null : { id: 1, name: 'Investidor de demonstração com nome extenso', email: 'qa@example.invalid' }, canManagePortal: role === 'manager' },
|
|
existingBusinessEnabled: true, businessReviewUrl: role === 'manager' ? '/management/negocios' : null,
|
|
educationEnabled: true, aiEnabled: true, legacyContentEnabled: true,
|
|
};
|
|
function Fixture() {
|
|
const ui = useTempoUi();
|
|
return <><PortalGroupedMenu /><main style={{padding:'48px 24px',maxWidth:1200,margin:'auto'}}>
|
|
<p style={{color:'#b91c1c',fontSize:12,fontWeight:600,letterSpacing:2}}>INVESTEMOZ · REVISÃO LOCAL</p>
|
|
<h1 style={{fontSize:36,fontWeight:600,margin:'16px 0'}}>O seu próximo investimento começa aqui</h1>
|
|
<p style={{color:'#64748b'}}>Navegação real do portal com dados fictícios para testar a apresentação.</p>
|
|
<button data-test="increment" onClick={ui.registerSubmittedInterest}>Simular pedido no contador</button>
|
|
</main></>;
|
|
}
|
|
await createInertiaApp({page:{component:'fixture',props,url:'/',version:null},resolve:()=>Fixture,
|
|
setup({el,App,props}) {createRoot(el).render(<TempoUiProvider><App {...props}/></TempoUiProvider>);}});
|
|
`,
|
|
);
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
...(process.env.HEADER_QA_CHROMIUM
|
|
? { executablePath: process.env.HEADER_QA_CHROMIUM }
|
|
: {}),
|
|
});
|
|
const results = [];
|
|
const errors = [];
|
|
try {
|
|
for (const [width, role, language] of before
|
|
? [
|
|
[1440, 'guest', 'pt'],
|
|
[390, 'guest', 'pt'],
|
|
]
|
|
: [
|
|
[1280, 'manager', 'fr'],
|
|
[1280, 'manager', 'pt'],
|
|
[1440, 'manager', 'fr'],
|
|
[320, 'guest', 'pt'],
|
|
[320, 'investor', 'pt'],
|
|
[390, 'guest', 'pt'],
|
|
[390, 'manager', 'pt'],
|
|
[768, 'investor', 'pt'],
|
|
[1024, 'manager', 'pt'],
|
|
[1440, 'guest', 'pt'],
|
|
[1920, 'investor', 'en'],
|
|
]) {
|
|
const context = await browser.newContext({
|
|
viewport: { width, height: 900 },
|
|
});
|
|
await context.addInitScript(
|
|
(lang) => localStorage.setItem('tempo-navigation-language', lang),
|
|
language,
|
|
);
|
|
await context.route('**/*', (route) => {
|
|
const url = new URL(route.request().url());
|
|
if (url.origin === base && url.pathname.startsWith('/images/')) {
|
|
const asset = path.resolve(root, 'public', `.${url.pathname}`);
|
|
if (
|
|
asset.startsWith(
|
|
path.join(root, 'public', 'images') + path.sep,
|
|
) &&
|
|
fs.existsSync(asset)
|
|
) {
|
|
return route.fulfill({ path: asset });
|
|
}
|
|
}
|
|
if (url.origin === base || url.protocol === 'data:')
|
|
return route.continue();
|
|
return route.abort();
|
|
});
|
|
const page = await context.newPage();
|
|
page.on('pageerror', (e) => {
|
|
errors.push(e.message);
|
|
console.error(e.message);
|
|
});
|
|
await page.goto(
|
|
`${base}/outputs/header-navbar-20261021/fixture.html?role=${role}`,
|
|
{ waitUntil: 'domcontentloaded', timeout: 60000 },
|
|
);
|
|
const header = page.locator('.tempo-header');
|
|
await header.waitFor();
|
|
await page.screenshot({
|
|
path: path.join(
|
|
dir,
|
|
`${before ? 'before' : 'after'}-${width}-${role}-${language}.png`,
|
|
),
|
|
});
|
|
if (before) {
|
|
results.push({
|
|
width,
|
|
height: await header.evaluate(
|
|
(el) => el.getBoundingClientRect().height,
|
|
),
|
|
});
|
|
await context.close();
|
|
continue;
|
|
}
|
|
assert.equal(
|
|
await header.locator('nav').count(),
|
|
1,
|
|
'One navigation landmark in header',
|
|
);
|
|
assert.equal(await header.locator('.tempo-section-bar').count(), 0);
|
|
const collapsedHeight = await header.evaluate(
|
|
(el) => el.getBoundingClientRect().height,
|
|
);
|
|
assert.ok(collapsedHeight <= 80, `Header height ${collapsedHeight}`);
|
|
if (width < 1280) {
|
|
const toggle = header.locator('.tempo-header-toggle');
|
|
await toggle.click();
|
|
assert.equal(await toggle.getAttribute('aria-expanded'), 'true');
|
|
await page.screenshot({
|
|
path: path.join(
|
|
dir,
|
|
`expanded-${width}-${role}-${language}.png`,
|
|
),
|
|
});
|
|
await page.keyboard.press('Escape');
|
|
assert.equal(await toggle.getAttribute('aria-expanded'), 'false');
|
|
await expect(toggle).toBeFocused();
|
|
await toggle.click();
|
|
}
|
|
const nav = header.locator('nav');
|
|
if (width >= 1280) {
|
|
const boxes = await Promise.all(
|
|
[
|
|
'.tempo-logo-link',
|
|
'.tempo-primary-navigation',
|
|
'.tempo-header-extras',
|
|
'.tempo-header-actions',
|
|
'.tempo-interest-button',
|
|
].map((selector) => header.locator(selector).boundingBox()),
|
|
);
|
|
for (let i = 1; i < boxes.length; i++)
|
|
assert.ok(
|
|
boxes[i - 1].x + boxes[i - 1].width <= boxes[i].x + 1,
|
|
`Header elements overlap at ${width}px: ${JSON.stringify(boxes)}`,
|
|
);
|
|
}
|
|
assert.deepEqual(
|
|
await nav
|
|
.locator(':scope > ul > li')
|
|
.evaluateAll((nodes) =>
|
|
nodes.map(
|
|
(node) =>
|
|
node
|
|
.querySelector('a,button')
|
|
?.getAttribute('href') ||
|
|
node.textContent.trim(),
|
|
),
|
|
),
|
|
language === 'pt'
|
|
? [
|
|
'/',
|
|
'Descoberta',
|
|
'Realização',
|
|
'Suporte',
|
|
'/sobre',
|
|
'Tempo',
|
|
]
|
|
: language === 'fr'
|
|
? [
|
|
'/',
|
|
'Découvrir',
|
|
'Investir',
|
|
'Assistance',
|
|
'/sobre',
|
|
'Tempo',
|
|
]
|
|
: ['/', 'Discover', 'Invest', 'Support', '/sobre', 'Tempo'],
|
|
);
|
|
assert.equal(await nav.locator('a[href="/sobre"]').count(), 1);
|
|
assert.equal(
|
|
await nav
|
|
.locator('a')
|
|
.first()
|
|
.evaluate((el) => getComputedStyle(el).fontSize),
|
|
width >= 1280 ? '18px' : '16px',
|
|
);
|
|
const destinations = new Set(
|
|
await header
|
|
.locator('a[href]')
|
|
.evaluateAll((nodes) =>
|
|
nodes.map((node) => node.getAttribute('href')),
|
|
),
|
|
);
|
|
async function inspectMenu(menu) {
|
|
await menu.waitFor();
|
|
menu = page.locator(`[id="${await menu.getAttribute('id')}"]`);
|
|
try {
|
|
await expect
|
|
.poll(async () => {
|
|
const box = await menu.boundingBox();
|
|
return (
|
|
!!box &&
|
|
box.x >= 0 &&
|
|
box.x + box.width <= width + 1
|
|
);
|
|
})
|
|
.toBe(true);
|
|
} catch (error) {
|
|
console.error(
|
|
JSON.stringify({
|
|
width,
|
|
role,
|
|
menu: await menu.getAttribute('aria-labelledby'),
|
|
box: await menu.boundingBox(),
|
|
}),
|
|
);
|
|
await page.screenshot({
|
|
path: path.join(dir, 'submenu-failure.png'),
|
|
});
|
|
throw error;
|
|
}
|
|
for (const href of await menu
|
|
.locator('a[href]')
|
|
.evaluateAll((nodes) =>
|
|
nodes.map((node) => node.getAttribute('href')),
|
|
))
|
|
destinations.add(href);
|
|
const subIds = await menu
|
|
.locator('[role="menuitem"][aria-haspopup="menu"]')
|
|
.evaluateAll((nodes) => nodes.map((node) => node.id));
|
|
for (const subId of subIds) {
|
|
const sub = page.locator(`[id="${subId}"]`);
|
|
await sub.focus();
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(sub).toHaveAttribute('aria-expanded', 'true');
|
|
await inspectMenu(page.locator('[role="menu"]').last());
|
|
await page.keyboard.press('ArrowLeft');
|
|
await expect(sub).toBeFocused();
|
|
}
|
|
}
|
|
for (const trigger of await nav
|
|
.locator(':scope > ul > li > button')
|
|
.all()) {
|
|
await trigger.focus();
|
|
await page.keyboard.press('Enter');
|
|
assert.equal(await trigger.getAttribute('aria-expanded'), 'true');
|
|
const menu = page.locator('[role="menu"]').last();
|
|
await inspectMenu(menu);
|
|
await page.keyboard.press('Escape');
|
|
await expect(trigger).toBeFocused();
|
|
}
|
|
await header
|
|
.getByRole('button', { name: 'Tempo', exact: true })
|
|
.click();
|
|
for (const href of [
|
|
'/contacto',
|
|
'https://revista.tempo.co.mz/',
|
|
'https://www.facebook.com/revistatempo.mz/',
|
|
'https://www.instagram.com/revistatempo.mz/',
|
|
'https://www.linkedin.com/company/revistatempo/',
|
|
'https://www.youtube.com/@RevistaTempoMz/',
|
|
]) {
|
|
assert.equal(
|
|
await page.locator(`[role="menu"] a[href="${href}"]`).count(),
|
|
1,
|
|
href,
|
|
);
|
|
}
|
|
await page.keyboard.press('Escape');
|
|
assert.ok(
|
|
await header
|
|
.getByRole('button', { name: 'Idioma da navegação' })
|
|
.isVisible(),
|
|
);
|
|
assert.ok(
|
|
await header
|
|
.getByRole('button', { name: 'Temperatura em Maputo' })
|
|
.isVisible(),
|
|
);
|
|
assert.ok(
|
|
await header.locator('a[href="/manifestar-interesse"]').isVisible(),
|
|
);
|
|
const dashboard = header.locator('a[href="/investidor/manifestacoes"]');
|
|
assert.ok(await dashboard.isVisible());
|
|
await page.locator('[data-test="increment"]').click();
|
|
assert.match(await dashboard.getAttribute('aria-label'), /1 /);
|
|
if (role !== 'guest') {
|
|
await header.locator('.tempo-login-button').click();
|
|
await inspectMenu(page.locator('[role="menu"]').last());
|
|
assert.ok(
|
|
await page
|
|
.locator('[data-test="portal-logout-button"]')
|
|
.isVisible(),
|
|
);
|
|
const hasManagement = await page
|
|
.getByRole('menu')
|
|
.getByText('Gestão', { exact: true })
|
|
.count();
|
|
assert.equal(hasManagement > 0, role === 'manager');
|
|
await page.keyboard.press('Escape');
|
|
} else {
|
|
assert.ok(await header.locator('a[href="/login"]').isVisible());
|
|
}
|
|
const catalog = portalNavigation({
|
|
authenticated: role !== 'guest',
|
|
manager: role === 'manager',
|
|
businesses: true,
|
|
businessReviewUrl:
|
|
role === 'manager' ? '/management/negocios' : null,
|
|
education: true,
|
|
ai: true,
|
|
legacyContent: true,
|
|
});
|
|
function checkDestinations(groups) {
|
|
for (const group of groups) {
|
|
if (group.children) checkDestinations(group.children);
|
|
else if (group.href)
|
|
assert.ok(
|
|
destinations.has(group.href),
|
|
`Preserved destination: ${group.href}`,
|
|
);
|
|
}
|
|
}
|
|
checkDestinations(
|
|
role === 'guest'
|
|
? catalog.filter((group) => group.id !== 'account')
|
|
: catalog,
|
|
);
|
|
const overflow = await header.evaluate((el) =>
|
|
Array.from(el.querySelectorAll('a,button,img'))
|
|
.filter((n) => {
|
|
const r = n.getBoundingClientRect();
|
|
return r.width && (r.x < -1 || r.right > innerWidth + 1);
|
|
})
|
|
.map((n) => n.textContent),
|
|
);
|
|
assert.deepEqual(overflow, []);
|
|
results.push({
|
|
width,
|
|
role,
|
|
language,
|
|
collapsedHeight,
|
|
destinations: destinations.size,
|
|
result: 'passed',
|
|
});
|
|
await context.close();
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
fs.writeFileSync(
|
|
path.join(dir, before ? 'before-results.json' : 'qa-results.json'),
|
|
JSON.stringify({ results, errors }, null, 2),
|
|
);
|
|
console.log(JSON.stringify({ results, errors }));
|
|
} finally {
|
|
await browser.close();
|
|
}
|