Files
portifolio-evandrodevbr/e2e/contact.spec.ts
T
2026-10-14 22:54:27 -03:00

231 lines
8.9 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { DESKTOP_USE, PHONE_USE } from "./helpers";
/**
* (h) Contato, no computador (1440x900) e, onde dito, no celular (390x844, toque):
* - o botão de cada oferta da home ("Escolha por onde começar") leva o cartão do formulário (#contact-form) para a
* tela, com o tipo de projeto da oferta já escolhido e o endereço `/#contact-form` (o `?tipo=` sai), 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);
* - o link direto `/?tipo=<tipo>#contact-form` (quem chega de outra página) abre a home com o formulário na tela e o
* tipo marcado;
* - no celular, o toque no botão da oferta deixa o cartão do formulário e o campo do tipo à vista;
* - 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, abaixo do
* cabeçalho fixo e acima da barra de cookies, quando ela está aberta (primeira visita, em qualquer servidor).
*/
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 que traz pedidos pelo WhatsApp",
sistema: "Sistema para tirar a operação da planilha",
revisao: "Revisão de sistema que já existe",
} as const;
test.use(DESKTOP_USE);
/** 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 as partes do contato estão inteiras na janela, abaixo do cabeçalho e acima da barra de cookies (se aberta);
* senão, onde estão. `card`: o destino das ofertas (o cartão do formulário e o campo de tipo); sem ele, o destino do
* botão do cabeçalho (o título do contato e o campo de tipo).
*/
function contactFormOnScreen(page: Page, card = false) {
return page.evaluate((onlyCard) => {
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,
);
// a barra de cookies fixa na base cobre o que estiver abaixo do topo dela
const barTop = document
.getElementById("cookie-bar")
?.getBoundingClientRect().top;
const limit = Math.min(innerHeight, barTop ?? innerHeight);
const parts = onlyCard
? {
"cartão do formulário": box("#contact-form"),
"campo de tipo": box("#contact select[name='subject']"),
}
: {
título: box("#contact h2"),
"campo de tipo": box("#contact select[name='subject']"),
};
const hidden = Object.entries(parts).filter(
([name, rect]) =>
!rect ||
rect.top < headerBottom - 1 ||
// o cartão inteiro pode ser mais alto que a janela: basta o topo dele estar à vista
(name === "cartão do formulário"
? rect.top > limit + 1
: rect.bottom > limit + 1),
);
return hidden.length === 0
? "ok"
: `fora da tela: ${hidden.map(([name]) => name).join(", ")}; ${JSON.stringify({ ...parts, headerBottom, janela: innerHeight, barraDeCookies: barTop ?? null, rolagem: Math.round(scrollY) })}`;
}, card);
}
/** Espera a rolagem (suave ou não) levar o formulário de contato para a tela. */
async function expectContactOnScreen(page: Page, what: string, card = false) {
await expect
.poll(() => contactFormOnScreen(page, card), {
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-form`)
.sort(),
);
});
for (const [kind, subject] of Object.entries(SUBJECT_BY_KIND)) {
test(`oferta "${kind}": o botão leva o formulário à tela com "${subject}" marcado, e o botão do cabeçalho rola de novo até o contato`, async ({
page,
}) => {
await open(page, "/");
const offer = page.locator(
`#modalities a[href='/?tipo=${kind}#contact-form']`,
);
await offer.scrollIntoViewIfNeeded();
await offer.click();
await expectContactOnScreen(page, `depois da oferta "${kind}"`, true);
await expect(page.locator("#contact select[name='subject']")).toHaveValue(
subject,
);
// o endereço fica na home, no cartão do formulário, sem o ?tipo= (o tipo já está no formulário)
await expect(page).toHaveURL(/\/#contact-form$/);
// 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,
);
});
}
for (const [kind, subject] of Object.entries(SUBJECT_BY_KIND)) {
test(`link direto /?tipo=${kind}#contact-form: abre a home com o formulário à vista e "${subject}" marcado`, async ({
page,
}) => {
await page.goto(`/?tipo=${kind}#contact-form`, { waitUntil: "load" });
await page.waitForTimeout(1500);
await expectContactOnScreen(page, `link direto "${kind}"`, true);
await expect(page.locator("#contact select[name='subject']")).toHaveValue(
subject,
);
});
}
test.describe("no celular (toque)", () => {
test.use(PHONE_USE);
for (const [kind, subject] of Object.entries(SUBJECT_BY_KIND)) {
test(`oferta "${kind}": o toque deixa o cartão do formulário e o campo de tipo à vista, com "${subject}" marcado`, async ({
page,
}) => {
await open(page, "/");
const offer = page.locator(
`#modalities a[href='/?tipo=${kind}#contact-form']`,
);
await offer.scrollIntoViewIfNeeded();
await offer.tap();
await expectContactOnScreen(page, `toque na oferta "${kind}"`, true);
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)");
});
});