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.
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
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.",
|
|
);
|
|
});
|
|
});
|