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.
168 lines
6.2 KiB
TypeScript
168 lines
6.2 KiB
TypeScript
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)");
|
|
});
|
|
});
|