import { expect, type Page, test } from "@playwright/test"; import { DESKTOP_USE, PHONE_USE } from "./helpers"; /** * (f) A home não tem trecho vazio, no computador (1440x900) e no celular (390x844): amostrada a cada ¼ de tela de * rolagem, do topo ao fim, TODA amostra tem texto visível. Tolerância zero: nem uma amostra isolada sem texto passa * (uma troca de cena que deixe a tela vazia por um instante, no ponto exato de uma amostra, reprova e conta como * defeito da vitrine, não do teste). * Uma amostra é "vazia" quando não há nenhum elemento de texto visível na janela. Cabeçalho, avisos, leitores de * tela e itens ocultos não contam (o cabeçalho fixo estaria em todas as amostras e esconderia uma tela vazia). */ /** Passo da amostragem, em fração da altura da janela. */ const STEP = 1 / 4; const VIEWPORTS = [ { name: "computador 1440x900", use: DESKTOP_USE }, { name: "celular 390x844 (isMobile, com toque)", use: PHONE_USE }, ] as const; interface Sample { top: number; count: number; } /** Quantos trechos de texto visíveis há na janela agora. */ function countVisibleText(page: Page) { return page.evaluate(() => { const ignored = "header, footer [aria-hidden], [aria-hidden='true'], [role='dialog'], .sr-only, .skip-link, script, style, noscript"; const effectiveOpacity = (element: Element | null) => { let opacity = 1; for (let node = element; node; node = node.parentElement) opacity *= Number.parseFloat(getComputedStyle(node).opacity); return opacity; }; const isShown = (element: Element) => { const style = getComputedStyle(element); return ( style.visibility === "visible" && style.display !== "none" && effectiveOpacity(element) >= 0.2 ); }; const isInsideWindow = (rect: DOMRect) => rect.width > 1 && rect.height > 1 && rect.bottom > 0 && rect.top < innerHeight && rect.right > 0 && rect.left < innerWidth; const isVisibleText = (node: Node) => { const parent = node.parentElement; if ((node.textContent?.trim().length ?? 0) < 3 || !parent) return false; if (parent.closest(ignored) || !isShown(parent)) return false; const range = document.createRange(); range.selectNodeContents(node); return isInsideWindow(range.getBoundingClientRect()); }; let visible = 0; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, ); for (let node = walker.nextNode(); node; node = walker.nextNode()) if (isVisibleText(node)) visible += 1; return visible; }); } /** Rola a home de ¼ em ¼ de tela, do topo ao fim, e conta o texto visível em cada parada. */ async function sampleHome(page: Page) { const { maxScroll, viewportHeight } = await page.evaluate(() => ({ maxScroll: document.documentElement.scrollHeight - innerHeight, viewportHeight: innerHeight, })); const step = Math.round(viewportHeight * STEP); const positions: number[] = []; for (let top = 0; top < maxScroll; top += step) positions.push(top); positions.push(Math.max(maxScroll, 0)); const samples: Sample[] = []; for (const top of positions) { await page.evaluate((target) => { window.scrollTo({ top: target, behavior: "instant" }); }, top); // a rolagem suave, as animações ligadas à rolagem e as imagens preguiçosas assentam await page.waitForTimeout(600); samples.push({ top, count: await countVisibleText(page) }); } return { samples, step, maxScroll, viewportHeight }; } for (const { name, use } of VIEWPORTS) { test.describe(`rolagem da home, ${name}`, () => { test.use(use); test("amostrada a cada ¼ de tela, nenhuma posição fica sem texto visível", async ({ page, }) => { await page.goto("/", { waitUntil: "load" }); await page.waitForTimeout(1500); const { samples, step, maxScroll, viewportHeight } = await sampleHome(page); const empty = samples .filter(({ count }) => count === 0) .map( ({ top }) => `${top}px (${(top / viewportHeight).toFixed(2)} tela)`, ); const log = samples .map(({ top, count }) => `${top}px: ${count} trecho(s) de texto`) .join("\n"); expect( empty, `posições sem texto visível (amostras a cada ${step}px, página de ${maxScroll}px, janela de ${viewportHeight}px):\n${log}`, ).toEqual([]); }); }); }