test(e2e): fluxos de contato, navegação, teclado e menu do celular mais rígidos
CI / verify (push) Failing after 7m13s
CI / e2e (push) Failing after 7m1s

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 22:54:27 -03:00
1 parent 8903f3c57e
commit 02298779df
8 files changed
+321 -115

No files matched your search

+15 -57
View File
@@ -1,8 +1,9 @@
import { expect, type Page, test } from "@playwright/test";
import { isDevServer, PHONE } from "./helpers";
import { COOKIE_BAR_CONTROLS, cookieBar, PHONE_USE } 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):
* (g) Menu do celular (390x844, com toque: `isMobile` e `hasTouch`, os toques são `tap()`), numa visita nova (a barra de
* cookies está aberta, fixa na base da tela, em qualquer servidor):
* - 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 e na barra de cookies com o menu aberto, nos dois sentidos, e a volta passa pelos
@@ -15,7 +16,7 @@ const CTA_LABEL = "Falar sobre meu projeto";
const LOGO_LABEL = "Evandro.dev.br — início";
const CLOSE_LABEL = "Fechar menu";
/** Os três controles da barra de cookies, na ordem do Tab. */
const BAR_CONTROLS = ["Política de privacidade", "Aceitar", "Recusar"];
const BAR_CONTROLS = COOKIE_BAR_CONTROLS;
/** `run` aparece em `sequence`, em posições seguidas? */
function hasRun(sequence: string[], run: string[]) {
@@ -24,7 +25,7 @@ function hasRun(sequence: string[], run: string[]) {
);
}
test.use({ viewport: PHONE });
test.use(PHONE_USE);
test.beforeEach(async ({ page }) => {
await page.goto("/", { waitUntil: "load" });
@@ -39,23 +40,23 @@ const mobileNav = (page: Page) =>
page.getByRole("navigation", { name: "Navegação mobile" });
/**
* Espera o cabeçalho responder ao clique (a hidratação terminou), só pelo que o usuário vê: abre o menu pelo botão,
* repetindo o clique enquanto ele não responde, e fecha de novo pelo mesmo botão. Deixa o menu fechado.
* Espera o cabeçalho responder ao toque (a hidratação terminou), só pelo que o usuário vê: abre o menu pelo botão,
* repetindo o toque enquanto ele não responde, e fecha de novo pelo mesmo botão. Deixa o menu fechado.
*/
async function waitUntilInteractive(page: Page) {
await expect(openMenuButton(page)).toBeVisible();
await openMenu(page);
await closeMenuButton(page).click();
await closeMenuButton(page).tap();
await expect(openMenuButton(page)).toHaveAttribute("aria-expanded", "false");
await expect(mobileNav(page)).toHaveCount(0);
}
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
// repete o toque enquanto o botão ainda for "Abrir menu": uma recarga do servidor de desenvolvimento (HMR) no meio
// do teste, ou um toque 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 openMenuButton(page).tap({ timeout: 2_000 });
await expect(closeMenuButton(page)).toHaveAttribute(
"aria-expanded",
"true",
@@ -65,52 +66,6 @@ async function openMenu(page: Page) {
await expect(mobileNav(page)).toBeVisible();
}
/**
* A barra de cookies da página (id `cookie-bar`, `COOKIE_BAR_ID` em lib/analytics.ts). Ela só é renderizada no build
* de produção (GoogleAnalytics.tsx). Num servidor de desenvolvimento entra no lugar dela uma barra de mesmo contrato
* (id, fixa na base, 96 px, `--cookie-bar-height` publicada na raiz, antes do cabeçalho, com o link da política,
* Aceitar e Recusar), para os testes conferirem o lado do cabeçalho. Num build de produção a ausência da barra real
* reprova.
*/
async function cookieBar(page: Page) {
const bar = page.getByRole("dialog", { name: "Cookies e privacidade" });
const isReal = await bar.waitFor({ state: "visible", timeout: 3_000 }).then(
() => true,
() => false,
);
if (isReal) return bar;
expect(
await isDevServer(page.request),
"a barra de cookies não apareceu num build de produção",
).toBe(true);
await page.evaluate(() => {
const stand = document.createElement("section");
stand.id = "cookie-bar";
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";
const text = document.createElement("p");
text.append("Usamos métricas de navegação do Google só se você permitir. ");
const policy = document.createElement("a");
policy.href = "/privacidade";
policy.textContent = "Política de privacidade";
text.append(policy);
const accept = document.createElement("button");
accept.type = "button";
accept.textContent = "Aceitar";
const decline = document.createElement("button");
decline.type = "button";
decline.textContent = "Recusar";
stand.append(text, accept, decline);
document.querySelector("header")?.before(stand);
document.documentElement.style.setProperty("--cookie-bar-height", "96px");
});
await expect(bar).toBeVisible();
return bar;
}
test("o menu abre pelo botão e mostra os links e o contato", async ({
page,
}) => {
@@ -136,6 +91,9 @@ test("o botão de contato do menu não fica sob a barra de cookies", async ({
const barBox = await bar.boundingBox();
if (!box || !barBox) throw new Error("sem caixa para o botão ou a barra");
// um toque no botão chegaria a ele (a checagem de "recebe o toque" do Playwright, sem navegar)
await contact.tap({ trial: true });
// o botão termina acima do topo da barra...
expect(
box.y + box.height,
@@ -259,7 +217,7 @@ test("Esc fecha o menu e devolve o foco ao botão do menu", async ({ page }) =>
test("navegar por um link do menu fecha o menu", async ({ page }) => {
await openMenu(page);
await mobileNav(page).getByRole("link", { name: "Cases" }).click();
await mobileNav(page).getByRole("link", { name: "Cases" }).tap();
await page.waitForURL("**/cases");
await expect(mobileNav(page)).toHaveCount(0);