import { expect, type Page, test } from "@playwright/test"; import { DESKTOP } from "./helpers"; /** * (f) No computador, a home não tem trecho vazio: amostrada a cada ¼ de tela de rolagem, nenhuma sequência de * amostras sem texto cobre ½ tela ou mais de rolagem (3 amostras seguidas vazias ou mais). Uma amostra vazia * isolada, ou duas seguidas (menos de ½ tela), é o instante de uma troca de cena e não reprova. * 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; /** Trecho vazio (em fração da altura da janela) a partir do qual o teste reprova. */ const MAX_EMPTY_SPAN = 1 / 2; test.use({ viewport: DESKTOP }); 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 }; } /** Sequências de amostras vazias seguidas cuja rolagem (da primeira à última da sequência) chega a `minSpan`. */ function emptyRuns(samples: Sample[], minSpan: number) { const runs: string[] = []; let start = -1; for (const [index, { count }] of samples.entries()) { if (count === 0 && start < 0) start = index; const endsRun = count > 0 || index === samples.length - 1; if (start >= 0 && endsRun) { const last = count === 0 ? index : index - 1; const span = samples[last].top - samples[start].top; if (span >= minSpan) runs.push(`${samples[start].top}px a ${samples[last].top}px`); start = -1; } } return runs; } test("a home, amostrada a cada ¼ de tela, não tem janela de ½ tela ou mais sem texto visível", async ({ page, }) => { await page.goto("/", { waitUntil: "load" }); await page.waitForTimeout(1500); const { samples, step, maxScroll, viewportHeight } = await sampleHome(page); const gaps = emptyRuns(samples, viewportHeight * MAX_EMPTY_SPAN); const log = samples .map(({ top, count }) => `${top}px: ${count} trecho(s) de texto`) .join("\n"); expect( gaps, `janelas de ½ tela ou mais sem texto visível (amostras a cada ${step}px, página de ${maxScroll}px):\n${log}`, ).toEqual([]); });