test(e2e): fluxos de contato, navegação, teclado e menu do celular mais rígidos
This commit is contained in:
1 parent
8903f3c57e
commit
02298779df
8 files changed
+321
-115
No files matched your search
+91
-28
@@ -1,15 +1,19 @@
|
||||
import { expect, type Page, test } from "@playwright/test";
|
||||
import { DESKTOP } from "./helpers";
|
||||
import { DESKTOP_USE, PHONE_USE } 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);
|
||||
* (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 e abaixo do
|
||||
* cabeçalho fixo.
|
||||
* "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";
|
||||
@@ -19,12 +23,12 @@ const CTA_LABEL = "Falar sobre meu projeto";
|
||||
* (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",
|
||||
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({ viewport: DESKTOP });
|
||||
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) {
|
||||
@@ -38,9 +42,13 @@ const headerContact = (page: Page) =>
|
||||
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(() => {
|
||||
/**
|
||||
* "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
|
||||
@@ -50,24 +58,39 @@ function contactFormOnScreen(page: Page) {
|
||||
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']"),
|
||||
};
|
||||
// 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(
|
||||
([, rect]) =>
|
||||
!rect || rect.top < headerBottom - 1 || rect.bottom > innerHeight + 1,
|
||||
([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, rolagem: Math.round(scrollY) })}`;
|
||||
});
|
||||
: `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) {
|
||||
async function expectContactOnScreen(page: Page, what: string, card = false) {
|
||||
await expect
|
||||
.poll(() => contactFormOnScreen(page), {
|
||||
.poll(() => contactFormOnScreen(page, card), {
|
||||
message: `${what}: o formulário de contato na tela`,
|
||||
timeout: 8_000,
|
||||
})
|
||||
@@ -83,24 +106,28 @@ test("a home tem as três ofertas e cada uma tem o seu tipo de projeto", async (
|
||||
.evaluateAll((links) => links.map((link) => link.getAttribute("href")));
|
||||
expect(hrefs.sort()).toEqual(
|
||||
Object.keys(SUBJECT_BY_KIND)
|
||||
.map((kind) => `/?tipo=${kind}#contact`)
|
||||
.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 contato à tela com "${subject}" marcado, e o botão do cabeçalho rola de novo até ele`, async ({
|
||||
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']`);
|
||||
const offer = page.locator(
|
||||
`#modalities a[href='/?tipo=${kind}#contact-form']`,
|
||||
);
|
||||
await offer.scrollIntoViewIfNeeded();
|
||||
await offer.click();
|
||||
|
||||
await expectContactOnScreen(page, `depois da oferta "${kind}"`);
|
||||
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);
|
||||
@@ -114,6 +141,42 @@ for (const [kind, subject] of Object.entries(SUBJECT_BY_KIND)) {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
}) => {
|
||||
|
||||
Reference in new issue
Block a user