feat(home): prova no topo, contato sem atrito e conversão medida
Home mais curta (hero, vitrine de clientes, sobre, serviços, IA resumida, depoimentos e contato), faixa de clientes e telas reais no hero, sem números do GitHub. Barra de cookies fina, WhatsApp fixo no celular, formulário com e-mail opcional e alternativa por e-mail, página /ia, eventos de e-mail, redes e currículo e conversão do Ads opcional por variável. Rodapé em 3 colunas. Remove componentes órfãos (LighthouseMetrics, Skills, Experience, contagem do GitHub).
This commit is contained in:
1 parent
6e1022b58c
commit
8d07b8c6bb
60 files changed
+4392
-2124
No files matched your search
@@ -0,0 +1,279 @@
|
||||
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();
|
||||
jest.spyOn(window, "open").mockImplementation(() => null);
|
||||
});
|
||||
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user