// 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'), ` Portal QA
`, ); 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();}}); `, ); 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(); }