chore: testes de ponta a ponta no CI e limpeza do repositório

Job e2e separado no CI; testes de teclado, rolagem, sem JS, axe no celular e menu do celular; limites de metadados derivados do sitemap; sai @react-three/fiber e @react-three/drei; pastas de ferramentas (.augment, .kiro, .claude/settings.local.json) fora do Git.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 17:01:37 -03:00
1 parent 6ac87824f3
commit 1a60795107
57 files changed
+625 -12090

No files matched your search

+113 -58
View File
@@ -1,72 +1,127 @@
import { expect, test } from "@playwright/test";
import { expect, type Page, test } from "@playwright/test";
import { DESKTOP } from "./helpers";
/**
* (f) No computador, nenhuma das paradas a cada 10 % da rolagem da home tem tela vazia: sempre há ao menos um
* 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 paradas e esconderia uma tela vazia).
* (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 });
test("nenhuma parada a cada 10 % da home tem tela vazia", async ({ page }) => {
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 empty: string[] = [];
const stops: string[] = [];
for (let step = 0; step <= 10; step++) {
await page.evaluate((fraction) => {
const max = document.documentElement.scrollHeight - innerHeight;
window.scrollTo({ top: Math.round(max * fraction), behavior: "instant" });
}, step / 10);
// a rolagem suave, as animações ligadas à rolagem e as imagens preguiçosas assentam
await page.waitForTimeout(700);
const result = await 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 visible: string[] = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const text = node.textContent?.trim() ?? "";
const parent = node.parentElement;
if (text.length < 3 || !parent || parent.closest(ignored)) continue;
const style = getComputedStyle(parent);
if (style.visibility !== "visible" || style.display === "none")
continue;
if (effectiveOpacity(parent) < 0.2) continue;
const range = document.createRange();
range.selectNodeContents(node);
const rect = range.getBoundingClientRect();
const inside =
rect.width > 1 &&
rect.height > 1 &&
rect.bottom > 0 &&
rect.top < innerHeight &&
rect.right > 0 &&
rect.left < innerWidth;
if (inside) visible.push(text.slice(0, 40));
}
return { count: visible.length, sample: visible.slice(0, 2) };
});
const label = `${step * 10}%`;
stops.push(`${label}: ${result.count} trecho(s) de texto`);
if (result.count === 0) empty.push(label);
}
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(
empty,
`paradas com tela vazia (nenhum texto visível):\n${stops.join("\n")}`,
gaps,
`janelas de ½ tela ou mais sem texto visível (amostras a cada ${step}px, página de ${maxScroll}px):\n${log}`,
).toEqual([]);
});