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.
318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import type { ComponentProps } from "react";
|
|
import { openCookiePreferences } from "@/lib/analytics";
|
|
import GoogleAnalytics from "./GoogleAnalytics";
|
|
|
|
jest.mock("next/script", () => ({
|
|
__esModule: true,
|
|
default: ({
|
|
strategy: _strategy,
|
|
...props
|
|
}: ComponentProps<"script"> & { strategy?: string }) => <script {...props} />,
|
|
}));
|
|
|
|
const CONSENT_KEY = "evandro.analytics-consent.v1";
|
|
const GA_DISABLE_KEY = "ga-disable-G-QTT9QT0T3S";
|
|
const GA_SRC = "https://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3S";
|
|
|
|
type AnalyticsWindow = Window & {
|
|
dataLayer?: IArguments[];
|
|
gtag?: (...args: unknown[]) => void;
|
|
[GA_DISABLE_KEY]?: boolean;
|
|
};
|
|
|
|
const analyticsWindow = window as AnalyticsWindow;
|
|
|
|
function consentUpdates(gtag: jest.Mock) {
|
|
return gtag.mock.calls.filter(
|
|
([command, action]) => command === "consent" && action === "update",
|
|
);
|
|
}
|
|
|
|
function setCookie(value: string) {
|
|
// biome-ignore lint/suspicious/noDocumentCookie: JSDOM cookie setup is the behavior boundary exercised by revocation.
|
|
document.cookie = value;
|
|
}
|
|
|
|
describe("GoogleAnalytics consent", () => {
|
|
beforeEach(() => {
|
|
jest.replaceProperty(process.env, "NODE_ENV", "production");
|
|
document.getElementById("google-analytics-script")?.remove();
|
|
localStorage.clear();
|
|
setCookie("_ga=; Max-Age=0; Path=/");
|
|
setCookie("_ga_PORTFOLIO=; Max-Age=0; Path=/");
|
|
setCookie("portfolio_session=; Max-Age=0; Path=/");
|
|
analyticsWindow.dataLayer = undefined;
|
|
analyticsWindow.gtag = undefined;
|
|
analyticsWindow[GA_DISABLE_KEY] = undefined;
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it("waits for a decision before loading analytics", async () => {
|
|
render(<GoogleAnalytics />);
|
|
|
|
expect(
|
|
await screen.findByRole("button", { name: "Aceitar" }),
|
|
).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Recusar" })).toBeVisible();
|
|
expect(screen.getByRole("dialog")).toHaveAccessibleDescription(
|
|
/Métricas de navegação do Google, só se você permitir\./,
|
|
);
|
|
expect(
|
|
screen.queryByTestId("google-analytics-script"),
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("link", { name: "Política de privacidade" }),
|
|
).toHaveAttribute("href", "/privacidade");
|
|
expect(analyticsWindow.dataLayer).toBeUndefined();
|
|
});
|
|
|
|
it("keeps the prompt as a thin bottom bar and publishes its height while it is open", async () => {
|
|
const user = userEvent.setup();
|
|
render(<GoogleAnalytics />);
|
|
|
|
const bar = await screen.findByRole("dialog");
|
|
expect(bar).toHaveClass("fixed", "inset-x-0", "bottom-0");
|
|
expect(document.documentElement).toHaveAttribute("data-cookie-bar", "open");
|
|
expect(
|
|
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
|
|
).toMatch(/^\d+px$/);
|
|
// o link vem antes dos botões na ordem de Tab dentro da barra
|
|
expect(bar.querySelector("a, button")).toBe(
|
|
screen.getByRole("link", { name: "Política de privacidade" }),
|
|
);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Recusar" }));
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(document.documentElement).not.toHaveAttribute("data-cookie-bar");
|
|
expect(
|
|
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
|
|
).toBe("");
|
|
});
|
|
|
|
it("persists acceptance and enables analytics without advertising consent", async () => {
|
|
const user = userEvent.setup();
|
|
render(<GoogleAnalytics />);
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
|
|
|
|
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
|
|
expect(screen.getByTestId("google-analytics-script")).toHaveAttribute(
|
|
"src",
|
|
GA_SRC,
|
|
);
|
|
const commands = analyticsWindow.dataLayer?.map((entry) =>
|
|
Array.from(entry),
|
|
);
|
|
expect(commands).toContainEqual([
|
|
"consent",
|
|
"default",
|
|
{
|
|
analytics_storage: "denied",
|
|
ad_storage: "denied",
|
|
ad_user_data: "denied",
|
|
ad_personalization: "denied",
|
|
},
|
|
]);
|
|
expect(commands).toContainEqual([
|
|
"consent",
|
|
"update",
|
|
{
|
|
analytics_storage: "granted",
|
|
ad_storage: "denied",
|
|
ad_user_data: "denied",
|
|
ad_personalization: "denied",
|
|
},
|
|
]);
|
|
expect(commands).toContainEqual([
|
|
"config",
|
|
"G-QTT9QT0T3S",
|
|
{
|
|
allow_ad_personalization_signals: false,
|
|
anonymize_ip: true,
|
|
},
|
|
]);
|
|
expect(commands).toContainEqual([
|
|
"config",
|
|
"AW-18432694647",
|
|
{ allow_ad_personalization_signals: false },
|
|
]);
|
|
expect(commands).toContainEqual(["set", "ads_data_redaction", true]);
|
|
// depois da escolha nada fica fixo na tela
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("persists refusal and keeps analytics and the bar absent after reload", async () => {
|
|
const user = userEvent.setup();
|
|
const firstRender = render(<GoogleAnalytics />);
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Recusar" }));
|
|
|
|
expect(localStorage.getItem(CONSENT_KEY)).toBe("denied");
|
|
expect(
|
|
screen.queryByTestId("google-analytics-script"),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
|
|
firstRender.unmount();
|
|
const reloaded = render(<GoogleAnalytics />);
|
|
|
|
await act(async () => {});
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(reloaded.container).toBeEmptyDOMElement();
|
|
expect(
|
|
screen.queryByTestId("google-analytics-script"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("reopens from the footer link and fully revokes analytics only", async () => {
|
|
const user = userEvent.setup();
|
|
localStorage.setItem(CONSENT_KEY, "granted");
|
|
setCookie("_ga=main; Path=/");
|
|
setCookie("_ga_PORTFOLIO=property; Path=/");
|
|
setCookie("portfolio_session=keep; Path=/");
|
|
const gtag = jest.fn();
|
|
analyticsWindow.gtag = gtag;
|
|
render(
|
|
<>
|
|
<button type="button" onClick={openCookiePreferences}>
|
|
Preferências de cookies
|
|
</button>
|
|
<GoogleAnalytics />
|
|
</>,
|
|
);
|
|
|
|
await screen.findByTestId("google-analytics-script");
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
const opener = screen.getByRole("button", {
|
|
name: "Preferências de cookies",
|
|
});
|
|
await user.click(opener);
|
|
expect(screen.getByRole("dialog")).toHaveAccessibleDescription(
|
|
/Escolha atual: permitido/,
|
|
);
|
|
expect(screen.getByRole("button", { name: "Aceitar" })).toHaveFocus();
|
|
await user.click(screen.getByRole("button", { name: "Recusar" }));
|
|
|
|
expect(localStorage.getItem(CONSENT_KEY)).toBe("denied");
|
|
expect(analyticsWindow[GA_DISABLE_KEY]).toBe(true);
|
|
expect(consentUpdates(gtag)).toContainEqual([
|
|
"consent",
|
|
"update",
|
|
{
|
|
analytics_storage: "denied",
|
|
ad_storage: "denied",
|
|
ad_user_data: "denied",
|
|
ad_personalization: "denied",
|
|
},
|
|
]);
|
|
expect(document.cookie).not.toContain("_ga=");
|
|
expect(document.cookie).not.toContain("_ga_PORTFOLIO=");
|
|
expect(document.cookie).toContain("portfolio_session=keep");
|
|
expect(
|
|
screen.queryByTestId("google-analytics-script"),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(opener).toHaveFocus();
|
|
});
|
|
|
|
it("restores focus after closing with Escape or applying opened preferences", async () => {
|
|
const user = userEvent.setup();
|
|
localStorage.setItem(CONSENT_KEY, "denied");
|
|
render(
|
|
<>
|
|
<button type="button" onClick={openCookiePreferences}>
|
|
Preferências de cookies
|
|
</button>
|
|
<GoogleAnalytics />
|
|
</>,
|
|
);
|
|
await act(async () => {});
|
|
|
|
const opener = screen.getByRole("button", {
|
|
name: "Preferências de cookies",
|
|
});
|
|
await user.click(opener);
|
|
expect(
|
|
screen.getByRole("link", { name: "Política de privacidade" }),
|
|
).toHaveAttribute("href", "/privacidade");
|
|
await user.keyboard("{Escape}");
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(opener).toHaveFocus();
|
|
expect(localStorage.getItem(CONSENT_KEY)).toBe("denied");
|
|
|
|
await user.click(opener);
|
|
await user.click(screen.getByRole("button", { name: "Aceitar" }));
|
|
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(opener).toHaveFocus();
|
|
});
|
|
|
|
it("fails closed when local storage is unavailable", async () => {
|
|
const user = userEvent.setup();
|
|
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
|
throw new Error("storage blocked");
|
|
});
|
|
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("storage blocked");
|
|
});
|
|
render(<GoogleAnalytics />);
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.queryByTestId("google-analytics-script"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("reports when revocation cannot be persisted", async () => {
|
|
const user = userEvent.setup();
|
|
localStorage.setItem(CONSENT_KEY, "granted");
|
|
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("storage blocked");
|
|
});
|
|
render(<GoogleAnalytics />);
|
|
|
|
await screen.findByTestId("google-analytics-script");
|
|
act(() => openCookiePreferences());
|
|
await user.click(await screen.findByRole("button", { name: "Recusar" }));
|
|
|
|
expect(
|
|
screen.getByRole("alert", {
|
|
name: "Falha ao salvar o consentimento",
|
|
}),
|
|
).toHaveTextContent(
|
|
"Não foi possível salvar a revogação. Tente novamente para concluir.",
|
|
);
|
|
expect(analyticsWindow[GA_DISABLE_KEY]).not.toBe(true);
|
|
expect(screen.getByRole("button", { name: "Recusar" })).toBeVisible();
|
|
});
|
|
|
|
it("reports when refusal cannot be persisted", async () => {
|
|
const user = userEvent.setup();
|
|
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("storage blocked");
|
|
});
|
|
render(<GoogleAnalytics />);
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Recusar" }));
|
|
|
|
expect(
|
|
screen.getByRole("alert", {
|
|
name: "Falha ao salvar o consentimento",
|
|
}),
|
|
).toHaveTextContent(
|
|
"Não foi possível salvar sua recusa. Tente novamente para concluir.",
|
|
);
|
|
expect(screen.getByRole("button", { name: "Recusar" })).toBeVisible();
|
|
expect(screen.getByRole("dialog")).toBeVisible();
|
|
});
|
|
});
|