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 nome@empresa.com.br."; 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, 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(); await fill(user, { email: "ana@example.com", 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(); 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(); 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(); 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(); 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 b@c.com", "sem-arroba.com"])( "path: e-mail %j -> invalid format is blocked", async (email) => { const user = userEvent.setup({ delay: null }); render(); 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(["a@b.co", "ana+projeto@sub.empresa.com.br"])( "path: e-mail %j -> valid format is accepted and sent", async (email) => { const user = userEvent.setup({ delay: null }); render(); 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(); 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(); 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(); 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(); await fill(user, { name: " Ana ", email: " ana@example.com ", message: " Organizar pedidos ", }); await user.click(submitButton()); const message = sentMessage(); expect(message).toContain("Nome: Ana\n"); expect(message).toContain("E-mail: ana@example.com\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(); 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(), "ana@example.com"); 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: ana@example.com\n"); }); it("path: the direct e-mail row reports its own placement", async () => { const user = userEvent.setup({ delay: null }); render(); 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" }), ); }); });