Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
138 lines
5.1 KiB
TypeScript
138 lines
5.1 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.
|
|
*/
|
|
|
|
const contexts: WhatsAppContexts = {
|
|
// o whatsappSubject do serviço (lib/site-content.ts): uma frase em minúsculas, que cabe em "Quero conversar sobre {assunto}."
|
|
services: { "criacao-de-sites": "um site que traz pedidos pelo WhatsApp" },
|
|
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: "Chamar 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 um site que traz pedidos pelo WhatsApp.",
|
|
],
|
|
[
|
|
"/cases/sev-modaintima",
|
|
"Quero conversar sobre um projeto parecido com o case SEV Moda Íntima.",
|
|
],
|
|
["/ia", "Quero conversar sobre a revisão de um aplicativo feito com IA."],
|
|
// a mesma frase do botão do hero e do cabeçalho de cada página
|
|
["/", "Quero conversar sobre um site ou sistema para a minha empresa."],
|
|
["/servicos", "Quero conversar sobre sites, sistemas e aplicativos."],
|
|
[
|
|
"/cases",
|
|
"Quero conversar sobre um projeto parecido com os cases do portfólio.",
|
|
],
|
|
[
|
|
"/curriculo",
|
|
"Quero conversar sobre um projeto, depois de ver o seu currículo.",
|
|
],
|
|
["/privacidade", "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 site ou sistema para a minha empresa.",
|
|
);
|
|
|
|
// 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 um site que traz pedidos pelo WhatsApp.",
|
|
);
|
|
});
|
|
});
|