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.
264 lines
8.3 KiB
TypeScript
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");
|
|
}
|
|
});
|
|
});
|