123 lines
4.4 KiB
TypeScript
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([]);
|
|
});
|
|
});
|
|
}
|