import { expect, type Page, test } from "@playwright/test"; import { DESKTOP } from "./helpers"; /** * (h) Contato, no computador (1440x900): * - o botão de cada oferta da home ("Escolha por onde começar") leva o formulário (#contact) para a tela, com o tipo * de projeto da oferta já escolhido, e um clique seguinte em "Falar sobre meu projeto" do cabeçalho continua * rolando até #contact (a página rola para outro lugar entre os dois cliques); * - de /privacidade, o botão do cabeçalho leva a /#contact e deixa o foco dentro de #contact; * - com movimento reduzido, dois cliques seguidos no link /#contact do cabeçalho rolam nos dois. * "Formulário na tela": o título do contato e o campo "Tipo de projeto" ficam inteiros dentro da janela e abaixo do * cabeçalho fixo. */ const CTA_LABEL = "Falar sobre meu projeto"; /** * Tipo de cada oferta (o `?tipo=` do href do botão) -> opção que o formulário mostra marcada. Espelha CONTACT_KINDS * (app/components/contact-subject.ts): uma oferta nova sem entrada aqui reprova o primeiro teste. */ const SUBJECT_BY_KIND = { site: "Site para empresa", sistema: "Sistemas web sob medida", revisao: "Revisão ou diagnóstico de sistema", } as const; test.use({ viewport: DESKTOP }); /** Abre a página e espera o tempo de a hidratação assentar, como os demais testes de interação. */ async function open(page: Page, path: string) { await page.goto(path, { waitUntil: "load" }); await page.waitForTimeout(1500); } const headerContact = (page: Page) => page.locator("header").getByRole("link", { name: CTA_LABEL }); const scrollToTop = (page: Page) => page.evaluate(() => window.scrollTo({ top: 0, behavior: "instant" })); /** "ok" se o título e o campo de tipo do contato estão inteiros na janela e abaixo do cabeçalho; senão, onde estão. */ function contactFormOnScreen(page: Page) { return page.evaluate(() => { const box = (selector: string) => { const rect = document.querySelector(selector)?.getBoundingClientRect(); return rect ? { top: Math.round(rect.top), bottom: Math.round(rect.bottom) } : null; }; const headerBottom = Math.round( document.querySelector("header")?.getBoundingClientRect().bottom ?? 0, ); const parts = { título: box("#contact h2"), "campo de tipo": box("#contact select[name='subject']"), }; const hidden = Object.entries(parts).filter( ([, rect]) => !rect || rect.top < headerBottom - 1 || rect.bottom > innerHeight + 1, ); return hidden.length === 0 ? "ok" : `fora da tela: ${hidden.map(([name]) => name).join(", ")}; ${JSON.stringify({ ...parts, headerBottom, janela: innerHeight, rolagem: Math.round(scrollY) })}`; }); } /** Espera a rolagem (suave ou não) levar o formulário de contato para a tela. */ async function expectContactOnScreen(page: Page, what: string) { await expect .poll(() => contactFormOnScreen(page), { message: `${what}: o formulário de contato na tela`, timeout: 8_000, }) .toBe("ok"); } test("a home tem as três ofertas e cada uma tem o seu tipo de projeto", async ({ page, }) => { await open(page, "/"); const hrefs = await page .locator("#modalities a[href*='?tipo=']") .evaluateAll((links) => links.map((link) => link.getAttribute("href"))); expect(hrefs.sort()).toEqual( Object.keys(SUBJECT_BY_KIND) .map((kind) => `/?tipo=${kind}#contact`) .sort(), ); }); for (const [kind, subject] of Object.entries(SUBJECT_BY_KIND)) { test(`oferta "${kind}": o botão leva o contato à tela com "${subject}" marcado, e o botão do cabeçalho rola de novo até ele`, async ({ page, }) => { await open(page, "/"); const offer = page.locator(`#modalities a[href='/?tipo=${kind}#contact']`); await offer.scrollIntoViewIfNeeded(); await offer.click(); await expectContactOnScreen(page, `depois da oferta "${kind}"`); await expect(page.locator("#contact select[name='subject']")).toHaveValue( subject, ); // a pessoa lê outro trecho da página e usa o botão do cabeçalho: ele ainda rola até o contato await scrollToTop(page); await expect.poll(() => contactFormOnScreen(page)).not.toBe("ok"); await headerContact(page).click(); await expectContactOnScreen(page, "depois do clique no cabeçalho"); // e o tipo escolhido pela oferta continua marcado await expect(page.locator("#contact select[name='subject']")).toHaveValue( subject, ); }); } test("de /privacidade, o botão do cabeçalho leva a /#contact e deixa o foco dentro de #contact", async ({ page, }) => { await open(page, "/privacidade"); await headerContact(page).click(); await page.waitForURL(/\/#contact$/); await expect .poll( () => page.evaluate(() => { const contact = document.getElementById("contact"); const active = document.activeElement; return contact !== null && active !== null && contact.contains(active) ? "dentro de #contact" : `fora de #contact: ${active?.tagName.toLowerCase()}${active?.id ? `#${active.id}` : ""}`; }), { message: "foco depois de chegar a /#contact", timeout: 8_000 }, ) .toBe("dentro de #contact"); }); test.describe("com movimento reduzido", () => { test.use({ reducedMotion: "reduce" }); test("na home, dois cliques seguidos no link /#contact do cabeçalho rolam nos dois", async ({ page, }) => { await open(page, "/"); for (const click of ["primeiro", "segundo"]) { await scrollToTop(page); await expect.poll(() => contactFormOnScreen(page)).not.toBe("ok"); await headerContact(page).click(); await expectContactOnScreen(page, `${click} clique`); } }); test("chegando de /privacidade, o clique no cabeçalho e um segundo clique na home rolam nos dois", async ({ page, }) => { await open(page, "/privacidade"); await headerContact(page).click(); await page.waitForURL(/\/#contact$/); await expectContactOnScreen(page, "primeiro clique (de /privacidade)"); await scrollToTop(page); await expect.poll(() => contactFormOnScreen(page)).not.toBe("ok"); await headerContact(page).click(); await expectContactOnScreen(page, "segundo clique (já na home)"); }); });