Files
portifolio-evandrodevbr/app/components/FloatingWhatsApp.paths.test.tsx
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
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.
2026-11-10 03:48:25 -03:00

264 lines
8.3 KiB
TypeScript

import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ComponentProps } from "react";
import { resetNavigation, setNavigation } from "@/__mocks__/next/navigation";
import { HOME_SUBJECT } from "@/lib/whatsapp";
import FloatingWhatsApp from "./FloatingWhatsApp";
import GoogleAnalytics from "./GoogleAnalytics";
/**
* Testes de caminhos-base de FloatingWhatsApp (complemento de FloatingWhatsApp.test.tsx):
* cada convite (botão do hero, seção de contato) sozinho na página, relatórios parciais do observador,
* desconexão ao desmontar e a convivência com a barra de cookies.
* A regra "só abaixo de 768 px" é CSS (md:hidden); o jsdom não avalia media queries, então só a classe é conferida.
*/
jest.mock("next/script", () => ({
__esModule: true,
default: ({
strategy: _strategy,
...props
}: ComponentProps<"script"> & { strategy?: string }) => <script {...props} />,
}));
const CONSENT_KEY = "evandro.analytics-consent.v1";
type Entry = Pick<IntersectionObserverEntry, "target" | "isIntersecting">;
const instances: FakeObserver[] = [];
const originalObserver = global.IntersectionObserver;
class FakeObserver {
observed: Element[] = [];
disconnect = jest.fn();
constructor(private callback: IntersectionObserverCallback) {
instances.push(this);
}
observe = jest.fn((target: Element) => {
this.observed.push(target);
});
unobserve = jest.fn();
takeRecords = () => [];
report(entries: Entry[]) {
act(() =>
this.callback(entries as IntersectionObserverEntry[], this as never),
);
}
}
const hero = () => document.getElementById("hero-contact-cta") as Element;
const contact = () => document.getElementById("contact") as Element;
const floatingLink = () =>
screen.queryByRole("link", { name: "Chamar no WhatsApp" });
beforeEach(() => {
resetNavigation();
instances.length = 0;
global.IntersectionObserver =
FakeObserver as unknown as typeof IntersectionObserver;
jest.replaceProperty(process.env, "NODE_ENV", "production");
localStorage.setItem(CONSENT_KEY, "granted");
window.gtag = jest.fn();
});
afterEach(() => {
global.IntersectionObserver = originalObserver;
jest.restoreAllMocks();
localStorage.clear();
delete window.gtag;
document.documentElement.style.removeProperty("--cookie-bar-height");
document.documentElement.removeAttribute("data-cookie-bar");
});
function renderBoth() {
return render(
<>
<a id="hero-contact-cta" href="/#contact">
Conversar
</a>
<section id="contact" aria-label="Contato" />
<FloatingWhatsApp />
</>,
);
}
describe("FloatingWhatsApp: convites presentes na página", () => {
it("path: only the hero button exists -> observes just it and follows its visibility", () => {
render(
<>
<a id="hero-contact-cta" href="/#contact">
Conversar sobre meu projeto
</a>
<FloatingWhatsApp />
</>,
);
expect(instances).toHaveLength(1);
expect(instances[0].observed).toEqual([hero()]);
expect(floatingLink()).toBeNull();
instances[0].report([{ target: hero(), isIntersecting: true }]);
expect(floatingLink()).toBeNull();
instances[0].report([{ target: hero(), isIntersecting: false }]);
expect(floatingLink()).toBeInTheDocument();
});
it("path: only the contact section exists -> observes just it and follows its visibility", () => {
render(
<>
<section id="contact" aria-label="Contato" />
<FloatingWhatsApp />
</>,
);
expect(instances[0].observed).toEqual([contact()]);
instances[0].report([{ target: contact(), isIntersecting: true }]);
expect(floatingLink()).toBeNull();
instances[0].report([{ target: contact(), isIntersecting: false }]);
expect(floatingLink()).toBeInTheDocument();
});
it("path: neither invite exists -> no observer is created and the button shows at once", () => {
render(<FloatingWhatsApp />);
expect(instances).toHaveLength(0);
expect(floatingLink()).toBeInTheDocument();
});
it("path: both invites exist -> both are observed", () => {
renderBoth();
expect(instances[0].observed).toEqual([hero(), contact()]);
});
});
describe("FloatingWhatsApp: relatórios do observador", () => {
it("path: a later report about one invite keeps what was said about the other", () => {
renderBoth();
const [observer] = instances;
observer.report([
{ target: hero(), isIntersecting: true },
{ target: contact(), isIntersecting: false },
]);
expect(floatingLink()).toBeNull();
// só o contato é reportado de novo; o hero continua à vista
observer.report([{ target: contact(), isIntersecting: false }]);
expect(floatingLink()).toBeNull();
observer.report([{ target: hero(), isIntersecting: false }]);
expect(floatingLink()).toBeInTheDocument();
});
it("path: both invites on screen at once -> hidden until both leave", () => {
renderBoth();
const [observer] = instances;
observer.report([
{ target: hero(), isIntersecting: true },
{ target: contact(), isIntersecting: true },
]);
expect(floatingLink()).toBeNull();
observer.report([{ target: hero(), isIntersecting: false }]);
expect(floatingLink()).toBeNull();
observer.report([{ target: contact(), isIntersecting: false }]);
expect(floatingLink()).toBeInTheDocument();
});
it("path: an empty report -> nothing is visible, so the button shows", () => {
renderBoth();
instances[0].report([]);
expect(floatingLink()).toBeInTheDocument();
});
it("path: unmount -> the observer is disconnected", () => {
const { unmount } = renderBoth();
expect(instances[0].disconnect).not.toHaveBeenCalled();
unmount();
expect(instances[0].disconnect).toHaveBeenCalledTimes(1);
});
});
describe("FloatingWhatsApp: conteúdo e cliques", () => {
it("path: the shortcut is CSS-limited to small screens and sized as a 56px target", () => {
render(<FloatingWhatsApp />);
const link = floatingLink();
expect(link).toHaveClass("h-14", "w-14");
expect(link?.parentElement).toHaveClass("md:hidden", "fixed", "right-sp-4");
});
it("path: click on the home reports the home subject as the service (the central navigation mock opens on '/')", () => {
render(<FloatingWhatsApp />);
fireEvent.click(floatingLink() as HTMLElement, { button: 0 });
expect(window.gtag).toHaveBeenCalledWith(
"event",
"whatsapp_open",
expect.objectContaining({ service: HOME_SUBJECT, placement: "floating" }),
);
});
it("path: click on a page without a subject reports 'Contato' as the service", () => {
setNavigation({ pathname: "/privacidade" });
render(<FloatingWhatsApp />);
fireEvent.click(floatingLink() as HTMLElement, { button: 0 });
expect(window.gtag).toHaveBeenCalledWith(
"event",
"whatsapp_open",
expect.objectContaining({ service: "Contato", placement: "floating" }),
);
});
});
describe("FloatingWhatsApp com a barra de cookies aberta", () => {
it("path: the bar marks <html> while open, which hides the button by CSS, and drops the mark once a choice is made", async () => {
const user = userEvent.setup();
localStorage.clear();
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
configurable: true,
get: () => 72,
});
try {
render(
<>
<GoogleAnalytics />
<FloatingWhatsApp />
</>,
);
const refuse = await screen.findByRole("button", { name: "Recusar" });
// a barra publica a altura (o menu do celular a usa) e marca o <html>; o botão flutuante some por CSS com a marca
expect(
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
).toBe("72px");
expect(document.documentElement).toHaveAttribute(
"data-cookie-bar",
"open",
);
expect(floatingLink()?.parentElement).toHaveClass(
"[[data-cookie-bar=open]_&]:hidden",
);
await user.click(refuse);
expect(
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
).toBe("");
expect(document.documentElement).not.toHaveAttribute("data-cookie-bar");
expect(floatingLink()).toBeInTheDocument();
} finally {
Reflect.deleteProperty(HTMLElement.prototype, "offsetHeight");
}
});
});