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:
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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user