Files
portifolio-evandrodevbr/app/components/Contact.paths.test.tsx
Evandro Fonseca Junior 10f170abf4 feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site
H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
2026-10-14 14:15:25 -03:00

283 lines
9.9 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Contact from "./Contact";
/**
* Testes de caminhos-base de Contact (complemento de Contact.test.tsx): validate() campo a campo,
* foco no primeiro campo inválido, limpeza independente dos erros e montagem da mensagem.
*/
beforeEach(() => {
jest.replaceProperty(process.env, "NODE_ENV", "production");
localStorage.setItem("evandro.analytics-consent.v1", "granted");
delete window["ga-disable-G-QTT9QT0T3S"];
window.gtag = jest.fn();
// uma aba aberta de verdade: se devolver null, o Contact cai para location.href
jest
.spyOn(window, "open")
.mockImplementation(() => ({ opener: {} }) as unknown as Window);
});
afterEach(() => {
jest.restoreAllMocks();
localStorage.clear();
delete window.gtag;
});
const NAME_ERROR = "Informe seu nome para eu saber com quem falo.";
const EMAIL_ERROR =
"Confira o e-mail: ele precisa ter o formato [email protected].";
const MESSAGE_ERROR = "Conte em poucas palavras o que você precisa resolver.";
const nameField = () => screen.getByRole("textbox", { name: /nome/i });
const emailField = () => screen.getByRole("textbox", { name: /e-mail/i });
const messageField = () => screen.getByRole("textbox", { name: /contexto/i });
const submitButton = () => screen.getByRole("button", { name: /whatsapp/i });
async function fill(
user: ReturnType<typeof userEvent.setup>,
values: { name?: string; email?: string; message?: string },
) {
if (values.name !== undefined) await user.type(nameField(), values.name);
if (values.email !== undefined) await user.type(emailField(), values.email);
if (values.message !== undefined)
await user.type(messageField(), values.message);
}
function sentMessage() {
const url = new URL(String(jest.mocked(window.open).mock.calls[0][0]));
return url.searchParams.get("text") ?? "";
}
describe("Contact: validate()", () => {
it("path: only the name is missing -> only the name is flagged and gets the focus", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, {
email: "[email protected]",
message: "Organizar pedidos",
});
await user.click(submitButton());
expect(nameField()).toHaveAttribute("aria-invalid", "true");
expect(emailField()).not.toHaveAttribute("aria-invalid");
expect(messageField()).not.toHaveAttribute("aria-invalid");
expect(screen.getByText(NAME_ERROR)).toBeVisible();
expect(screen.queryByText(EMAIL_ERROR)).toBeNull();
expect(screen.queryByText(MESSAGE_ERROR)).toBeNull();
expect(nameField()).toHaveFocus();
expect(window.open).not.toHaveBeenCalled();
expect(window.gtag).not.toHaveBeenCalled();
});
it("path: only the e-mail is malformed -> only the e-mail is flagged and gets the focus (not the name)", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, {
name: "Ana",
email: "ana@semponto",
message: "Organizar pedidos",
});
await user.click(submitButton());
expect(emailField()).toHaveAttribute("aria-invalid", "true");
expect(emailField()).toHaveAccessibleDescription(EMAIL_ERROR);
expect(nameField()).not.toHaveAttribute("aria-invalid");
expect(messageField()).not.toHaveAttribute("aria-invalid");
expect(emailField()).toHaveFocus();
expect(window.open).not.toHaveBeenCalled();
});
it("path: only the message is missing -> only the message is flagged and gets the focus", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, { name: "Ana" });
await user.click(submitButton());
expect(messageField()).toHaveAttribute("aria-invalid", "true");
expect(messageField()).toHaveAccessibleDescription(
new RegExp(MESSAGE_ERROR),
);
expect(nameField()).not.toHaveAttribute("aria-invalid");
expect(emailField()).not.toHaveAttribute("aria-invalid");
expect(messageField()).toHaveFocus();
expect(window.open).not.toHaveBeenCalled();
});
it("path: name and message made only of spaces -> both rejected", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, { name: " ", message: " " });
await user.click(submitButton());
expect(screen.getByText(NAME_ERROR)).toBeVisible();
expect(screen.getByText(MESSAGE_ERROR)).toBeVisible();
expect(nameField()).toHaveFocus();
expect(window.open).not.toHaveBeenCalled();
});
it("path: e-mail made only of spaces -> counts as omitted: no error and no e-mail line", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, {
name: "Ana",
email: " ",
message: "Organizar pedidos",
});
await user.click(submitButton());
expect(screen.queryByText(EMAIL_ERROR)).toBeNull();
expect(window.open).toHaveBeenCalledTimes(1);
expect(sentMessage()).not.toContain("E-mail:");
});
it.each(["a@b", "a@c.", "@c.com", "a@@c.com", "a [email protected]", "sem-arroba.com"])(
"path: e-mail %j -> invalid format is blocked",
async (email) => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, { name: "Ana", email, message: "Organizar pedidos" });
await user.click(submitButton());
expect(screen.getByText(EMAIL_ERROR)).toBeVisible();
expect(window.open).not.toHaveBeenCalled();
},
);
it.each(["[email protected]", "[email protected]"])(
"path: e-mail %j -> valid format is accepted and sent",
async (email) => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, { name: "Ana", email, message: "Organizar pedidos" });
await user.click(submitButton());
expect(screen.queryByText(EMAIL_ERROR)).toBeNull();
expect(window.open).toHaveBeenCalledTimes(1);
expect(sentMessage()).toContain(`E-mail: ${email}\n`);
},
);
});
describe("Contact: updateField() and the error state", () => {
it("path: typing in one field clears only that field's error", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await user.click(submitButton());
expect(screen.getByText(NAME_ERROR)).toBeVisible();
expect(screen.getByText(MESSAGE_ERROR)).toBeVisible();
await user.type(messageField(), "Organizar pedidos");
expect(screen.queryByText(MESSAGE_ERROR)).toBeNull();
expect(messageField()).not.toHaveAttribute("aria-invalid");
expect(screen.getByText(NAME_ERROR)).toBeVisible();
expect(nameField()).toHaveAttribute("aria-invalid", "true");
});
it("path: changing the project type keeps the other fields' errors untouched", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await user.click(submitButton());
await user.selectOptions(
screen.getByRole("combobox", { name: /tipo de projeto/i }),
"Aplicativos",
);
expect(screen.getByText(NAME_ERROR)).toBeVisible();
expect(screen.getByText(MESSAGE_ERROR)).toBeVisible();
expect(
screen.getByRole("combobox", { name: /tipo de projeto/i }),
).toHaveValue("Aplicativos");
});
it("path: fixing every error and submitting again -> errors gone, WhatsApp opens once", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await user.click(submitButton());
expect(window.open).not.toHaveBeenCalled();
await fill(user, { name: "Ana", message: "Organizar pedidos" });
await user.click(submitButton());
expect(screen.queryByText(NAME_ERROR)).toBeNull();
expect(screen.queryByText(MESSAGE_ERROR)).toBeNull();
expect(window.open).toHaveBeenCalledTimes(1);
expect(window.gtag).toHaveBeenCalledTimes(1);
});
});
describe("Contact: mensagem montada", () => {
it("path: surrounding spaces are trimmed in name, e-mail and message", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await fill(user, {
name: " Ana ",
email: " [email protected] ",
message: " Organizar pedidos ",
});
await user.click(submitButton());
const message = sentMessage();
expect(message).toContain("Nome: Ana\n");
expect(message).toContain("E-mail: [email protected]\n");
expect(message).toContain(
"Contexto, objetivo e prazo:\nOrganizar pedidos\n\n",
);
});
it("path: the chosen project type and the e-mail line reach the mailto draft, and the e-mail line is omitted when blank", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
await user.selectOptions(
screen.getByRole("combobox", { name: /tipo de projeto/i }),
"Aplicativos",
);
await fill(user, { name: "Ana", message: "Organizar pedidos" });
const mail = screen.getByRole("link", {
name: "Prefere e-mail? Enviar por e-mail",
});
const body = new URL(mail.getAttribute("href") ?? "").searchParams.get(
"body",
);
expect(body).toContain("Tipo de projeto: Aplicativos");
expect(body).not.toContain("E-mail:");
await user.type(emailField(), "[email protected]");
const withEmail = new URL(
screen
.getByRole("link", { name: "Prefere e-mail? Enviar por e-mail" })
.getAttribute("href") ?? "",
).searchParams.get("body");
expect(withEmail).toContain("E-mail: [email protected]\n");
});
it("path: the direct e-mail row reports its own placement", async () => {
const user = userEvent.setup({ delay: null });
render(<Contact />);
const direct = screen.getByRole("link", {
name: /evandrofonsecajunior@gmail\.com/,
});
// o jsdom não navega para mailto:; o clique só precisa chegar ao rastreio
direct.addEventListener("click", (event) => event.preventDefault());
await user.click(direct);
expect(window.gtag).toHaveBeenCalledWith(
"event",
"email_click",
expect.objectContaining({ placement: "contact_direct" }),
);
});
});