test: e2e mais rígida e .gitattributes
Zero telas sem texto no computador e no celular; atalho de teclado em até 4 Tabs e linear em até 36 com o inventário das paradas; ciclo de foco do menu com a barra de cookies; oferta e cabeçalho levando ao formulário; foco vindo de outra página; separador da faixa de clientes; contagem de pôsteres da SEV. .gitattributes com eol=lf.
This commit is contained in:
1 parent
204ebd5fa5
commit
2cd4a98c55
10 files changed
+588
-87
No files matched your search
+111
-39
@@ -1,29 +1,34 @@
|
||||
import { expect, type Page, test } from "@playwright/test";
|
||||
import { PHONE } from "./helpers";
|
||||
import { isDevServer, 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;
|
||||
* - o Tab fica preso no cabeçalho e na barra de cookies com o menu aberto, nos dois sentidos, e a volta passa pelos
|
||||
* três controles da barra: contato -> "Política de privacidade" -> Aceitar -> Recusar -> logo -> "Fechar menu";
|
||||
* - 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";
|
||||
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"];
|
||||
|
||||
/** `run` aparece em `sequence`, em posições seguidas? */
|
||||
function hasRun(sequence: string[], run: string[]) {
|
||||
return sequence.some((_, start) =>
|
||||
run.every((label, offset) => sequence[start + offset] === label),
|
||||
);
|
||||
}
|
||||
|
||||
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"))
|
||||
);
|
||||
});
|
||||
await waitUntilInteractive(page);
|
||||
});
|
||||
|
||||
const openMenuButton = (page: Page) =>
|
||||
@@ -33,6 +38,18 @@ const closeMenuButton = (page: Page) =>
|
||||
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.
|
||||
*/
|
||||
async function waitUntilInteractive(page: Page) {
|
||||
await expect(openMenuButton(page)).toBeVisible();
|
||||
await openMenu(page);
|
||||
await closeMenuButton(page).click();
|
||||
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
|
||||
@@ -48,6 +65,52 @@ 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,
|
||||
}) => {
|
||||
@@ -63,22 +126,7 @@ test("o menu abre pelo botão e mostra os links e o contato", async ({
|
||||
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");
|
||||
});
|
||||
}
|
||||
const bar = await cookieBar(page);
|
||||
await openMenu(page);
|
||||
await expect(bar).toBeVisible();
|
||||
|
||||
@@ -120,9 +168,10 @@ test("o botão de contato do menu não fica sob a barra de cookies", async ({
|
||||
}
|
||||
});
|
||||
|
||||
test("com o menu aberto o Tab e o Shift+Tab ficam presos no cabeçalho", async ({
|
||||
test("com o menu aberto o Tab e o Shift+Tab ficam presos no cabeçalho e na barra de cookies", async ({
|
||||
page,
|
||||
}) => {
|
||||
await cookieBar(page);
|
||||
await openMenu(page);
|
||||
|
||||
const header = page.locator("header");
|
||||
@@ -139,27 +188,50 @@ test("com o menu aberto o Tab e o Shift+Tab ficam presos no cabeçalho", async (
|
||||
.replace(/\s+/g, " ")
|
||||
.slice(0, 40);
|
||||
return {
|
||||
insideHeader: Boolean(element?.closest("header")),
|
||||
label: `${element?.tagName.toLowerCase()} "${label}"`,
|
||||
// o cabeçalho e a barra de cookies (id cookie-bar, COOKIE_BAR_ID) formam o ciclo do menu
|
||||
inside: Boolean(element?.closest("header, #cookie-bar")),
|
||||
label,
|
||||
};
|
||||
});
|
||||
|
||||
// o ciclo, na ordem do Tab: a barra (vem antes do cabeçalho no layout), logo, botão do menu, 5 links, contato
|
||||
const cycle = [
|
||||
...BAR_CONTROLS,
|
||||
LOGO_LABEL,
|
||||
CLOSE_LABEL,
|
||||
"Cases",
|
||||
"Serviços",
|
||||
"Como trabalho",
|
||||
"Sobre",
|
||||
"IA",
|
||||
CTA_LABEL,
|
||||
];
|
||||
// a volta que mais importa: do contato à barra de cookies, ao logo e ao botão do menu
|
||||
const wrap = [CTA_LABEL, ...BAR_CONTROLS, LOGO_LABEL, CLOSE_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++) {
|
||||
const sequence: string[] = [];
|
||||
// mais toques do que controles no ciclo: dá a volta inteira, mais de uma vez
|
||||
for (let press = 1; press <= cycle.length * 2; press++) {
|
||||
await page.keyboard.press(key);
|
||||
const { insideHeader, label } = await whereIsFocus();
|
||||
const { inside, label } = await whereIsFocus();
|
||||
expect(
|
||||
insideHeader,
|
||||
`${key} nº ${press} levou o foco para fora do cabeçalho: ${label}`,
|
||||
inside,
|
||||
`${key} nº ${press} levou o foco para fora do cabeçalho e da barra de cookies: ${label}`,
|
||||
).toBe(true);
|
||||
visited.add(label);
|
||||
sequence.push(label);
|
||||
}
|
||||
const visited = new Set(sequence);
|
||||
const missing = cycle.filter((label) => !visited.has(label));
|
||||
expect(
|
||||
visited.size,
|
||||
`${key} percorreu ${visited.size} controles: ${[...visited].join("; ")}`,
|
||||
).toBeGreaterThanOrEqual(8);
|
||||
missing,
|
||||
`${key} não passou por: ${missing.join("; ")}; passou por: ${[...visited].join("; ")}`,
|
||||
).toEqual([]);
|
||||
const expected = key === "Tab" ? wrap : [...wrap].reverse();
|
||||
expect(
|
||||
hasRun(sequence, expected),
|
||||
`${key} deveria andar ${expected.join(" -> ")}; andou ${sequence.join(" -> ")}`,
|
||||
).toBe(true);
|
||||
}
|
||||
await expect(header.getByRole("link", { name: CTA_LABEL })).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user