// 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'), ` InvesteMoz · revisão do header
`, ); 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 <>

INVESTEMOZ · REVISÃO LOCAL

O seu próximo investimento começa aqui

Navegação real do portal com dados fictícios para testar a apresentação.

; } await createInertiaApp({page:{component:'fixture',props,url:'/',version:null},resolve:()=>Fixture, setup({el,App,props}) {createRoot(el).render();}}); `, ); 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(); }