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:
1 parent
6ac87824f3
commit
1a60795107
57 files changed
+625
-12090
No files matched your search
+113
-58
@@ -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([]);
|
||||
});
|
||||
Reference in new issue
Block a user