Files
portifolio-evandrodevbr/e2e/scroll.spec.ts
2026-10-14 22:54:27 -03:00

123 lines
4.4 KiB
TypeScript

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([]);
});
});
}