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

+197
View File
@@ -0,0 +1,197 @@
import { expect, type Page, test } from "@playwright/test";
import { PHONE } from "./helpers";
/**
* (g) Menu do celular (390x844), numa visita nova (no build de produção a barra de cookies está aberta, fixa na base da tela):
* - o menu abre;
* - o botão de contato do menu não fica sob a barra de cookies (o elemento no ponto do botão é o próprio botão);
* - o Tab fica preso no cabeçalho com o menu aberto, nos dois sentidos;
* - Esc fecha o menu e o foco volta ao botão do menu;
* - navegar por um link do menu fecha o menu.
*/
const CTA_LABEL = "Falar sobre meu projeto";
test.use({ viewport: PHONE });
test.beforeEach(async ({ page }) => {
await page.goto("/", { waitUntil: "load" });
// o botão do menu só responde ao clique depois da hidratação: o React põe nele as props do componente
await page.waitForFunction(() => {
const button = document.querySelector("header button");
return (
button !== null &&
Object.keys(button).some((key) => key.startsWith("__reactProps"))
);
});
});
const openMenuButton = (page: Page) =>
page.getByRole("button", { name: "Abrir menu" });
const closeMenuButton = (page: Page) =>
page.getByRole("button", { name: "Fechar menu" });
const mobileNav = (page: Page) =>
page.getByRole("navigation", { name: "Navegação mobile" });
async function openMenu(page: Page) {
// repete o clique enquanto o botão ainda for "Abrir menu": uma recarga do servidor de desenvolvimento (HMR) no meio
// do teste, ou um clique antes da hidratação, não deixa o menu aberto
await expect(async () => {
if ((await openMenuButton(page).count()) > 0)
await openMenuButton(page).click({ timeout: 2_000 });
await expect(closeMenuButton(page)).toHaveAttribute(
"aria-expanded",
"true",
{ timeout: 2_000 },
);
}).toPass({ timeout: 20_000 });
await expect(mobileNav(page)).toBeVisible();
}
test("o menu abre pelo botão e mostra os links e o contato", async ({
page,
}) => {
await openMenu(page);
const nav = mobileNav(page);
for (const label of ["Cases", "Serviços", "Como trabalho", "Sobre", "IA"])
await expect(
nav.getByRole("link", { name: label, exact: true }),
).toBeVisible();
await expect(nav.getByRole("link", { name: CTA_LABEL })).toBeVisible();
});
test("o botão de contato do menu não fica sob a barra de cookies", async ({
page,
}) => {
const bar = page.getByRole("dialog", { name: "Cookies e privacidade" });
// A barra só é renderizada no build de produção (GoogleAnalytics.tsx). Num servidor de desenvolvimento entra no
// lugar dela uma barra de mesmo contrato (fixa na base, 96 px, --cookie-bar-height publicada na raiz), para o
// teste conferir o lado do cabeçalho. No CI (build de produção) a ausência da barra real reprova.
if ((await bar.count()) === 0 && !process.env.CI) {
await page.evaluate(() => {
const stand = document.createElement("section");
stand.setAttribute("role", "dialog");
stand.setAttribute("aria-label", "Cookies e privacidade");
stand.style.cssText =
"position:fixed;inset-inline:0;bottom:0;height:96px;z-index:9980;background:#0a0a0a;color:#fff";
stand.textContent = "Usamos métricas de navegação do Google";
document.body.append(stand);
document.documentElement.style.setProperty("--cookie-bar-height", "96px");
});
}
await openMenu(page);
await expect(bar).toBeVisible();
const contact = mobileNav(page).getByRole("link", { name: CTA_LABEL });
await expect(contact).toBeVisible();
const box = await contact.boundingBox();
const barBox = await bar.boundingBox();
if (!box || !barBox) throw new Error("sem caixa para o botão ou a barra");
// o botão termina acima do topo da barra...
expect(
box.y + box.height,
`base do botão (${box.y + box.height}px) x topo da barra de cookies (${barBox.y}px)`,
).toBeLessThanOrEqual(barBox.y);
// ...e, em cada ponto testado, o elemento que o usuário atingiria é o próprio botão
const points = [
{ x: box.x + box.width / 2, y: box.y + box.height / 2 },
{ x: box.x + box.width / 2, y: box.y + box.height - 2 },
{ x: box.x + 4, y: box.y + box.height - 2 },
{ x: box.x + box.width - 4, y: box.y + box.height - 2 },
];
for (const { x, y } of points) {
const hit = await contact.evaluate(
(element, point) => {
const atPoint = document.elementFromPoint(point.x, point.y);
return {
isButton: atPoint === element || element.contains(atPoint),
found: atPoint
? `${atPoint.tagName.toLowerCase()} "${(atPoint.textContent ?? "").trim().slice(0, 30)}"`
: "(nada)",
};
},
{ x, y },
);
expect(
hit.isButton,
`em (${Math.round(x)}, ${Math.round(y)}) está ${hit.found}, não o botão de contato`,
).toBe(true);
}
});
test("com o menu aberto o Tab e o Shift+Tab ficam presos no cabeçalho", async ({
page,
}) => {
await openMenu(page);
const header = page.locator("header");
const whereIsFocus = () =>
page.evaluate(() => {
const element = document.activeElement as HTMLElement | null;
const label = (
element?.getAttribute("aria-label") ||
element?.textContent ||
element?.tagName ||
"(nada)"
)
.trim()
.replace(/\s+/g, " ")
.slice(0, 40);
return {
insideHeader: Boolean(element?.closest("header")),
label: `${element?.tagName.toLowerCase()} "${label}"`,
};
});
for (const key of ["Tab", "Shift+Tab"]) {
const visited = new Set<string>();
// mais toques do que controles no cabeçalho (logo, botão, 5 links, contato): dá a volta inteira, mais de uma vez
for (let press = 1; press <= 20; press++) {
await page.keyboard.press(key);
const { insideHeader, label } = await whereIsFocus();
expect(
insideHeader,
`${key} nº ${press} levou o foco para fora do cabeçalho: ${label}`,
).toBe(true);
visited.add(label);
}
expect(
visited.size,
`${key} percorreu ${visited.size} controles: ${[...visited].join("; ")}`,
).toBeGreaterThanOrEqual(8);
}
await expect(header.getByRole("link", { name: CTA_LABEL })).toBeVisible();
});
test("Esc fecha o menu e devolve o foco ao botão do menu", async ({ page }) => {
await openMenu(page);
// o foco anda para dentro do menu antes do Esc
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Escape");
await expect(mobileNav(page)).toHaveCount(0);
const button = openMenuButton(page);
await expect(button).toHaveAttribute("aria-expanded", "false");
await expect(button).toBeFocused();
// a página volta a responder: nada ficou inerte nem sem rolagem
await expect(page.locator("#main-content")).not.toHaveAttribute("inert");
expect(
await page.evaluate(() => document.documentElement.style.overflow),
"overflow da página depois de fechar",
).not.toBe("hidden");
});
test("navegar por um link do menu fecha o menu", async ({ page }) => {
await openMenu(page);
await mobileNav(page).getByRole("link", { name: "Cases" }).click();
await page.waitForURL("**/cases");
await expect(mobileNav(page)).toHaveCount(0);
await expect(openMenuButton(page)).toHaveAttribute("aria-expanded", "false");
await expect(page.locator("#main-content")).not.toHaveAttribute("inert");
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});