Files
portifolio-evandrodevbr/e2e/keyboard.spec.ts
T
Evandro Fonseca Junior 2cd4a98c55 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.
2026-10-14 19:14:49 -03:00

140 lines
5.4 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { DESKTOP } from "./helpers";
/**
* (c) Teclado, no computador:
* (a) atalho: Tab até "Falar sobre meu projeto" no cabeçalho, Enter, e o campo de nome do contato em até 4 Tabs.
* Só no computador: no celular o botão do hero abre o WhatsApp, não o formulário.
* (b) caminho linear: do topo da home, o Tab chega ao campo de nome do contato em até 36 toques
* (a vitrine de projetos é navegada com as setas: um seletor e o link do case; ver o inventário em LINEAR_MAX_TABS).
* Os testes continuam apertando Tab depois do limite só para dizer, na falha, quantos toques faltavam.
*/
const CTA_LABEL = "Falar sobre meu projeto";
const SHORTCUT_MAX_TABS_TO_CTA = 20;
const SHORTCUT_MAX_TABS_AFTER_ENTER = 4;
/**
* Limite do caminho linear: 36 toques. Inventário das paradas legítimas, do topo da home ao campo de nome (35 hoje):
* 1 link "Pular para o conteúdo";
* cabeçalho: logo, 5 links (Cases, Serviços, Como trabalho, Sobre, IA) e "Falar sobre meu projeto" (7);
* hero: "Falar sobre meu projeto", "Ver projetos entregues", os 4 clientes da faixa, LinkedIn e e-mail (8);
* vitrine de projetos: o seletor, o link do case do projeto aberto e "Ver os cases" (3);
* ofertas: 3 botões e 3 links de case/página, mais "Ver todos os serviços" (7);
* "Ver trajetória completa" (1) e as 5 perguntas da FAQ (5);
* contato: WhatsApp e e-mail (2), e então o campo de nome.
* Tudo isso é conteúdo e navegação que a pessoa quer alcançar; o limite não deve subir sem uma parada nova justificada.
*/
const LINEAR_MAX_TABS = 36;
const LINEAR_GIVE_UP_AFTER = 120;
test.use({ viewport: DESKTOP });
/** Descreve o elemento com foco e diz se é o botão de contato do cabeçalho ou o campo de nome do contato. */
async function focusedElement(page: Page) {
return page.evaluate((ctaLabel) => {
const element = document.activeElement as HTMLElement | null;
if (!element || element === document.body)
return { isCta: false, isName: false, label: "(nada)" };
const label = (
element.getAttribute("aria-label") ||
element.textContent ||
element.getAttribute("name") ||
element.tagName
)
.trim()
.replace(/\s+/g, " ")
.slice(0, 40);
return {
isCta:
Boolean(element.closest("header")) &&
(element.textContent ?? "").trim() === ctaLabel,
isName:
element.getAttribute("name") === "name" &&
Boolean(element.closest("#contact")),
label: `${element.tagName.toLowerCase()} "${label}"`,
};
}, CTA_LABEL);
}
/** Visita nova: foco no documento, topo da página. */
async function openHomeAsNewVisitor(page: Page) {
await page.goto("/", { waitUntil: "load" });
await page.waitForTimeout(800);
await page.evaluate(() => (document.activeElement as HTMLElement)?.blur());
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "instant" }));
}
/** Aperta Tab até o foco satisfazer `stop`, no máximo `giveUpAfter` vezes. `reached` é 0 se nunca satisfez. */
async function tabUntil(
page: Page,
stop: (where: Awaited<ReturnType<typeof focusedElement>>) => boolean,
giveUpAfter: number,
) {
const visited: string[] = [];
for (let tab = 1; tab <= giveUpAfter; tab++) {
await page.keyboard.press("Tab");
const where = await focusedElement(page);
visited.push(`${tab}. ${where.label}`);
if (stop(where)) return { reached: tab, visited };
}
return { reached: 0, visited };
}
test(`atalho: Tab até "${CTA_LABEL}" do cabeçalho, Enter, e o campo de nome em até ${SHORTCUT_MAX_TABS_AFTER_ENTER} Tabs`, async ({
page,
}) => {
await openHomeAsNewVisitor(page);
const toCta = await tabUntil(
page,
({ isCta }) => isCta,
SHORTCUT_MAX_TABS_TO_CTA,
);
expect(
toCta.reached,
`"${CTA_LABEL}" do cabeçalho não recebeu foco em ${SHORTCUT_MAX_TABS_TO_CTA} Tabs; passou por:\n${toCta.visited.join("\n")}`,
).toBeGreaterThan(0);
await page.keyboard.press("Enter");
await page.waitForURL(/#contact$/);
// a rolagem até o contato e o foco da âncora assentam
await page.waitForTimeout(1500);
const afterEnter = await tabUntil(
page,
({ isName }) => isName,
SHORTCUT_MAX_TABS_AFTER_ENTER + 20,
);
expect(
afterEnter.reached,
`o campo de nome não apareceu depois do Enter; passou por:\n${afterEnter.visited.join("\n")}`,
).toBeGreaterThan(0);
expect(
afterEnter.reached,
`depois do Enter, o campo de nome só foi alcançado no Tab ${afterEnter.reached} (limite ${SHORTCUT_MAX_TABS_AFTER_ENTER}); passou por:\n${afterEnter.visited.join("\n")}`,
).toBeLessThanOrEqual(SHORTCUT_MAX_TABS_AFTER_ENTER);
await expect(page.locator("#contact input[name='name']")).toBeFocused();
});
test(`caminho linear: do topo da home, Tab chega ao campo de nome do contato em até ${LINEAR_MAX_TABS} toques`, async ({
page,
}) => {
await openHomeAsNewVisitor(page);
const { reached, visited } = await tabUntil(
page,
({ isName }) => isName,
LINEAR_GIVE_UP_AFTER,
);
expect(
reached,
`o campo de nome não apareceu nem em ${LINEAR_GIVE_UP_AFTER} Tabs`,
).toBeGreaterThan(0);
expect(
reached,
`o campo de nome só foi alcançado no Tab ${reached} (limite ${LINEAR_MAX_TABS}); passou por:\n${visited.join("\n")}`,
).toBeLessThanOrEqual(LINEAR_MAX_TABS);
await expect(page.locator("#contact input[name='name']")).toBeFocused();
});