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é 36 toques, mais os controles da * barra de cookies enquanto ela estiver aberta (primeira visita: 36 + 3 = 39; com o consentimento já salvo a barra * não abre e o limite é 36). 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; /** 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>) => 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(); });