// Actual public portal and navigation, synthetic API, local images, no database. 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.CONTEXT_QA_PLAYWRIGHT || 'C:/Users/jorge/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright', ); const root = process.cwd(); const base = ( process.env.CONTEXT_QA_VITE || fs.readFileSync('public/hot', 'utf8') ).trim(); assert.equal(new URL(base).hostname, '127.0.0.1'); const dir = path.join(root, 'outputs/investment-context-compact-20261023'); fs.mkdirSync(dir, { recursive: true }); const fixturePath = '/outputs/investment-context-compact-20261023/fixture.html'; fs.writeFileSync( path.join(dir, 'fixture.html'), `Cartões 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 Navigation from '../../resources/js/layouts/portal-navigation'; import '../../resources/css/app.css'; await createInertiaApp({page:{component:'PortalInvestidor',props:{auth:{user:null,canManagePortal:false},essentialResources:[],countries:[]},url:'/',version:null},resolve:()=>Portal,layout:()=>Navigation,setup({el,App,props}){createRoot(el).render();}}); `, ); const destinations = [ ['/clusters', 'Clusters', 'Cadeias de valor e lacunas produtivas.'], ['/corredores', 'Corredores', 'Territórios, logística e conexões.'], [ '/projectos-ancora', 'Projectos âncora', 'Projectos estruturantes e relações.', ], ['/educacao', 'Educação', 'Investimentos em competências e formação.'], [ '/documentos', 'Documentos e Formulários', 'Legislação, procedimentos, formulários e materiais de apoio.', ], ]; const browser = await chromium.launch({ headless: true, ...(process.env.CONTEXT_QA_CHROMIUM ? { executablePath: process.env.CONTEXT_QA_CHROMIUM } : {}), }); const errors = [], results = [], navigation = []; let currentPage; async function createCase(width, dpr = 1, fail = 'none') { const context = await browser.newContext({ viewport: { width, height: 1000 }, deviceScaleFactor: dpr, reducedMotion: 'reduce', }); // Avoid development HMR reloading the fixture's synthetic homepage URL. await context.routeWebSocket('**/*', () => {}); let releaseImages; const imagesReady = new Promise((resolve) => { releaseImages = resolve; }); await context.route('**/*', async (route) => { const url = new URL(route.request().url()); if (url.origin !== base) return route.abort(); if (url.pathname.startsWith('/images/')) { if (url.pathname.startsWith('/images/investment-context/')) { await imagesReady; if ( fail === 'all' || (fail === 'primary' && !url.pathname.includes('-fallback')) ) return route.fulfill({ status: 404 }); } 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: [], featuredOpportunities: [], stats: { opportunities: 0, sectors: 0, provinces: 0 }, }, }, }); if (url.pathname.startsWith('/api/')) return route.fulfill({ json: { data: [] } }); if ( route.request().isNavigationRequest() && destinations.some(([href]) => url.pathname === href) ) { navigation.push(url.pathname); return route.fulfill({ contentType: 'text/html', body: '

Destino de navegação de teste

', }); } return route.continue(); }); const page = await context.newPage(); currentPage = page; page.on('pageerror', (error) => errors.push(error.message)); await page.goto(base + fixturePath, { waitUntil: 'domcontentloaded', timeout: 60000, }); const section = page.getByRole('region', { name: 'Encontre o contexto do seu investimento', }); await section.waitFor(); return { context, page, section, releaseImages }; } try { for (const [width, dpr] of [ [320, 1], [390, 2], [768, 1], [1024, 1], [1440, 2], [1920, 1], ]) { const { context, page, section, releaseImages } = await createCase( width, dpr, ); const links = section.getByRole('link'); assert.equal(await links.count(), 5); assert.equal(await section.locator('img').count(), 5); const before = await links.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().height), ); releaseImages(); for (const img of await section.locator('img').all()) { await img.scrollIntoViewIfNeeded(); await img.evaluate((image) => image.decode()); } const photos = await section.locator('img').evaluateAll((nodes) => nodes.map((image) => ({ src: image.currentSrc, alt: image.alt, lazy: image.loading, width: image.naturalWidth, height: image.naturalHeight, fit: getComputedStyle(image).objectFit, ratio: image.getBoundingClientRect().width / image.getBoundingClientRect().height, })), ); assert.equal(new Set(photos.map((image) => image.src)).size, 5); photos.forEach((image) => { assert.ok(image.alt.length > 20); assert.equal(image.lazy, 'lazy'); // naturalWidth is density corrected when srcset uses width descriptors. assert.ok(image.width > 0 && image.height > 0); assert.equal(image.fit, 'cover'); assert.ok(Math.abs(image.ratio - 16 / 9) < 0.01); }); const after = await links.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().height), ); before.forEach((height, index) => assert.ok( Math.abs(height - after[index]) < 1, 'Image caused card layout shift', ), ); const rows = await links.evaluateAll((nodes) => nodes.map((node) => { const rect = node.getBoundingClientRect(); return { top: rect.top, height: rect.height }; }), ); for (const row of rows) { const aligned = rows.filter( (item) => Math.abs(item.top - row.top) < 1, ); assert.ok( aligned.every((item) => Math.abs(item.height - row.height) < 1), 'Unequal heights within a card row', ); } const columns = await section .locator('nav') .evaluate( (node) => getComputedStyle(node).gridTemplateColumns.split(' ') .length, ); assert.equal(columns, width >= 1024 ? 4 : width >= 640 ? 2 : 1); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ), false, ); for (const [href, title, description] of destinations) { const link = section.getByRole('link', { name: title, exact: true, }); assert.equal(await link.getAttribute('href'), href); assert.equal( (await link.locator('p').textContent()).trim(), description, ); } assert.equal(await page.locator('.tempo-header').count(), 1); assert.equal(await page.locator('.portal-hero').count(), 1); await section.screenshot({ path: path.join(dir, `cards-${width}-dpr${dpr}.png`), }); if (width === 1440) { for (const [href, title] of destinations) { const link = section.getByRole('link', { name: title, exact: true, }); await link.focus(); await page.keyboard.press('Enter'); await page.waitForURL(base + href); assert.equal(navigation.at(-1), href); await page.goto(base + fixturePath, { waitUntil: 'networkidle', }); await section.waitFor(); } } results.push({ width, dpr, columns, photos: photos.map(({ src, width, height }) => ({ src, width, height, })), status: 'passed', }); await context.close(); } for (const fail of ['primary', 'all']) { const { context, page, section, releaseImages } = await createCase( 390, 2, fail, ); releaseImages(); for (const link of await section.getByRole('link').all()) await link.scrollIntoViewIfNeeded(); if (fail === 'primary') { await page.waitForFunction( () => [ ...document.querySelectorAll( '#investment-context-heading + nav img', ), ].length === 5 && [ ...document.querySelectorAll( '#investment-context-heading + nav img', ), ].every( (image) => image.complete && image.naturalWidth > 0 && image.currentSrc.includes('-fallback'), ), ); } else { await section .getByText('Imagem indisponível', { exact: true }) .nth(4) .waitFor(); assert.equal(await section.getByRole('link').count(), 5); assert.equal(await section.locator('[role="img"]').count(), 5); } await section.screenshot({ path: path.join(dir, `fallback-${fail}.png`), }); results.push({ fail, status: 'passed' }); await context.close(); } assert.deepEqual(errors, []); fs.writeFileSync( path.join(dir, 'qa-results.json'), JSON.stringify( { results, errors, navigation, databaseAccess: false }, null, 2, ), ); console.log(JSON.stringify({ cases: results.length, errors, navigation })); } catch (error) { if (currentPage && !currentPage.isClosed()) { await Promise.all([ Promise.resolve( currentPage.screenshot({ path: path.join(dir, 'failure.png') }), ), Promise.resolve(currentPage.content()).then((html) => fs.writeFileSync(path.join(dir, 'failure.html'), html), ), ]); } throw error; } finally { await browser.close(); }