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.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:25 -03:00
1 parent e80f73d075
commit 10f170abf4
56 files changed
+3099 -906

No files matched your search

@@ -0,0 +1,128 @@
import { act, render, screen } from "@testing-library/react";
import { usePathname } from "next/navigation";
import type { WhatsAppContexts } from "@/lib/whatsapp";
import FloatingWhatsApp from "./FloatingWhatsApp";
/**
* O botão vive em app/layout.tsx e vale em todo o site: a mensagem acompanha a página aberta, e a observação
* do hero e do contato (que só existem na home) recomeça a cada troca de página.
*/
jest.mock("next/navigation", () => ({ usePathname: jest.fn() }));
const contexts: WhatsAppContexts = {
services: { "criacao-de-sites": "Sites profissionais" },
cases: { "sev-modaintima": "SEV Moda Íntima" },
};
type Entry = Pick<IntersectionObserverEntry, "target" | "isIntersecting">;
const instances: FakeObserver[] = [];
const originalObserver = global.IntersectionObserver;
class FakeObserver {
disconnect = jest.fn();
constructor(private callback: IntersectionObserverCallback) {
instances.push(this);
}
observe = jest.fn();
unobserve = jest.fn();
takeRecords = () => [];
report(entries: Entry[]) {
act(() =>
this.callback(entries as IntersectionObserverEntry[], this as never),
);
}
}
const floatingLink = () =>
screen.queryByRole("link", { name: "Conversar no WhatsApp" });
const messageOf = (link: HTMLElement | null) =>
new URL(link?.getAttribute("href") ?? "").searchParams.get("text");
beforeEach(() => {
instances.length = 0;
global.IntersectionObserver =
FakeObserver as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = originalObserver;
document.getElementById("hero-contact-cta")?.remove();
document.getElementById("contact")?.remove();
});
describe("message by route", () => {
it.each([
[
"/servicos/criacao-de-sites",
"Quero conversar sobre Sites profissionais.",
],
[
"/cases/sev-modaintima",
"Quero conversar sobre um projeto parecido com o da SEV Moda Íntima.",
],
["/ia", "Quero conversar sobre a revisão de um aplicativo feito com IA."],
["/", "Quero conversar sobre um projeto."],
["/servicos", "Quero conversar sobre um projeto."],
["/servicos/inexistente", "Quero conversar sobre um projeto."],
])("on %s", (pathname, sentence) => {
jest.mocked(usePathname).mockReturnValue(pathname);
// sem hero nem contato na página, o botão aparece de imediato
render(<FloatingWhatsApp contexts={contexts} />);
expect(messageOf(floatingLink())).toContain(sentence);
});
it("falls back to a generic message without contexts", () => {
jest.mocked(usePathname).mockReturnValue("/servicos/criacao-de-sites");
render(<FloatingWhatsApp />);
expect(messageOf(floatingLink())).toContain(
"Quero conversar sobre um projeto.",
);
});
});
describe("page changes", () => {
it("observes the hero and the contact again on every page, hiding the button until the new report", () => {
jest.mocked(usePathname).mockReturnValue("/cases/sev-modaintima");
const { rerender } = render(<FloatingWhatsApp contexts={contexts} />);
// páginas de case não têm hero nem contato: o botão está à vista
expect(floatingLink()).toBeInTheDocument();
expect(instances).toHaveLength(0);
// volta à home, que tem os dois convites
document.body.insertAdjacentHTML(
"beforeend",
'<a id="hero-contact-cta" href="/#contact">Conversar</a><section id="contact"></section>',
);
jest.mocked(usePathname).mockReturnValue("/");
rerender(<FloatingWhatsApp contexts={contexts} />);
// sem piscar: some até o observador dizer que o hero saiu da tela
expect(floatingLink()).toBeNull();
expect(instances).toHaveLength(1);
const hero = document.getElementById("hero-contact-cta") as Element;
const contact = document.getElementById("contact") as Element;
instances[0].report([
{ target: hero, isIntersecting: false },
{ target: contact, isIntersecting: false },
]);
expect(floatingLink()).toBeInTheDocument();
expect(messageOf(floatingLink())).toContain(
"Quero conversar sobre um projeto.",
);
// sai da home para um serviço: o observador antigo é desligado e o botão passa a falar do serviço
document.getElementById("hero-contact-cta")?.remove();
document.getElementById("contact")?.remove();
jest.mocked(usePathname).mockReturnValue("/servicos/criacao-de-sites");
rerender(<FloatingWhatsApp contexts={contexts} />);
expect(instances[0].disconnect).toHaveBeenCalled();
expect(messageOf(floatingLink())).toContain(
"Quero conversar sobre Sites profissionais.",
);
});
});