187 lines
7.3 KiB
TypeScript
187 lines
7.3 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test";
|
|
import {
|
|
COOKIE_BAR_CONTROLS,
|
|
cookieBar,
|
|
DESKTOP_USE,
|
|
saveConsent,
|
|
} 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é 37 toques, mais os controles da
|
|
* barra de cookies enquanto ela estiver aberta (primeira visita: 37 + 3 = 40; com o consentimento já salvo a barra
|
|
* não abre e o limite é 37). 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: 37 toques. Inventário das paradas legítimas, do topo da home ao campo de nome (37 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 5 clientes da faixa, LinkedIn e e-mail (9);
|
|
* vitrine de projetos: o seletor, o link do case do projeto aberto e "Ver os cases" (3);
|
|
* ofertas: 3 botões e 4 links de case/página (a revisão aparece no card e no fim), mais "Ver todos os serviços" (8);
|
|
* "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 = 37;
|
|
/** Primeira visita: a barra de cookies abre antes do cabeçalho e soma os controles dela (link da política, Aceitar, Recusar). */
|
|
const LINEAR_MAX_TABS_FIRST_VISIT =
|
|
LINEAR_MAX_TABS + COOKIE_BAR_CONTROLS.length;
|
|
const LINEAR_GIVE_UP_AFTER = 120;
|
|
|
|
test.use(DESKTOP_USE);
|
|
|
|
/** 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. Com `consent`, o consentimento já estava salvo no navegador (a barra
|
|
* de cookies não abre); sem ele é a primeira visita, e a barra de cookies tem de estar aberta.
|
|
*/
|
|
async function openHomeAsNewVisitor(
|
|
page: Page,
|
|
consent?: "granted" | "denied",
|
|
) {
|
|
if (consent) await saveConsent(page, consent);
|
|
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" }));
|
|
const bar = page.getByRole("dialog", { name: "Cookies e privacidade" });
|
|
if (consent) await expect(bar).toHaveCount(0);
|
|
else await cookieBar(page);
|
|
}
|
|
|
|
/** 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, primeira visita (barra de cookies aberta): do topo da home, Tab chega ao campo de nome do contato em até ${LINEAR_MAX_TABS_FIRST_VISIT} toques (${LINEAR_MAX_TABS} + os ${COOKIE_BAR_CONTROLS.length} controles da barra)`, async ({
|
|
page,
|
|
}) => {
|
|
await openHomeAsNewVisitor(page);
|
|
const bar = await cookieBar(page);
|
|
expect(
|
|
await bar.locator("a[href], button").count(),
|
|
"controles focáveis da barra de cookies",
|
|
).toBe(COOKIE_BAR_CONTROLS.length);
|
|
|
|
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_FIRST_VISIT}); passou por:\n${visited.join("\n")}`,
|
|
).toBeLessThanOrEqual(LINEAR_MAX_TABS_FIRST_VISIT);
|
|
await expect(page.locator("#contact input[name='name']")).toBeFocused();
|
|
});
|
|
|
|
test(`caminho linear, com o consentimento já salvo (sem a barra de cookies): Tab chega ao campo de nome do contato em até ${LINEAR_MAX_TABS} toques`, async ({
|
|
page,
|
|
}) => {
|
|
await openHomeAsNewVisitor(page, "denied");
|
|
|
|
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();
|
|
});
|