feat(home): prova no topo, contato sem atrito e conversão medida
Home mais curta (hero, vitrine de clientes, sobre, serviços, IA resumida, depoimentos e contato), faixa de clientes e telas reais no hero, sem números do GitHub. Barra de cookies fina, WhatsApp fixo no celular, formulário com e-mail opcional e alternativa por e-mail, página /ia, eventos de e-mail, redes e currículo e conversão do Ads opcional por variável. Rodapé em 3 colunas. Remove componentes órfãos (LighthouseMetrics, Skills, Experience, contagem do GitHub).
This commit is contained in:
1 parent
6e1022b58c
commit
8d07b8c6bb
60 files changed
+4392
-2124
No files matched your search
@@ -0,0 +1,242 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { ComponentProps } from "react";
|
||||
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: "Conversar no WhatsApp" });
|
||||
|
||||
beforeEach(() => {
|
||||
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 reports 'Contato' as the service because the floating link has no subject", () => {
|
||||
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 publishes its height while open and removes it 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" });
|
||||
|
||||
// o botão flutuante usa a variável publicada pela barra para subir
|
||||
expect(
|
||||
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
|
||||
).toBe("72px");
|
||||
expect(floatingLink()?.parentElement?.className).toContain(
|
||||
"var(--cookie-bar-height,0px)",
|
||||
);
|
||||
|
||||
await user.click(refuse);
|
||||
|
||||
expect(
|
||||
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
|
||||
).toBe("");
|
||||
expect(floatingLink()).toBeInTheDocument();
|
||||
} finally {
|
||||
Reflect.deleteProperty(HTMLElement.prototype, "offsetHeight");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user