Files
portifolio-evandrodevbr/app/components/GoogleAnalytics.paths.test.tsx

442 lines
15 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";
/**
* Testes de caminhos-base de GoogleAnalytics (complemento de GoogleAnalytics.test.tsx):
* modo de desenvolvimento, teclas da barra, reaceite, nova concessão depois de revogar,
* cookies de analytics x outros cookies, reaproveitamento de gtag/dataLayer e recuperação de falha de gravação.
*/
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";
type AnalyticsWindow = Window & {
dataLayer?: IArguments[];
gtag?: (...args: unknown[]) => void;
[GA_DISABLE_KEY]?: boolean;
};
const analyticsWindow = window as AnalyticsWindow;
function setCookie(value: string) {
// biome-ignore lint/suspicious/noDocumentCookie: JSDOM cookie setup is the behavior boundary exercised by revocation.
document.cookie = value;
}
function expireCookie(name: string) {
setCookie(`${name}=; Max-Age=0; Path=/`);
}
const COOKIES = ["_ga", "_ga_ABC123", "_gid", "_gat", "portfolio_session"];
function commands() {
return (analyticsWindow.dataLayer ?? []).map((entry) => Array.from(entry));
}
function commandsNamed(...names: string[]) {
return commands().filter((entry) =>
names.every((name, index) => entry[index] === name),
);
}
function OpenPreferencesButton() {
return (
<button type="button" onClick={openCookiePreferences}>
Preferências de cookies
</button>
);
}
beforeEach(() => {
jest.replaceProperty(process.env, "NODE_ENV", "production");
document.getElementById("google-analytics-script")?.remove();
localStorage.clear();
for (const name of COOKIES) expireCookie(name);
analyticsWindow.dataLayer = undefined;
analyticsWindow.gtag = undefined;
analyticsWindow[GA_DISABLE_KEY] = undefined;
});
afterEach(() => {
jest.restoreAllMocks();
document.getElementById("google-analytics-script")?.remove();
for (const name of COOKIES) expireCookie(name);
});
describe("GoogleAnalytics: modo e teclas", () => {
it("path: outside production -> the cookie bar and the choice are still there (dev and production behave the same), but the gtag script is never loaded", async () => {
jest.replaceProperty(process.env, "NODE_ENV", "development");
const user = userEvent.setup();
render(<GoogleAnalytics />);
// a barra (e o foco por Tab) é a mesma da produção
const dialog = await screen.findByRole("dialog", {
name: "Cookies e privacidade",
});
expect(dialog).toBeVisible();
expect(
document.documentElement.style.getPropertyValue("--cookie-bar-height"),
).not.toBe("");
await user.click(screen.getByRole("button", { name: "Aceitar" }));
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
expect(screen.queryByRole("dialog")).toBeNull();
// mesmo com o consentimento, fora da produção o gtag não é baixado
await act(async () => {});
expect(screen.queryByTestId("google-analytics-script")).toBeNull();
expect(document.getElementById("google-analytics-script")).toBeNull();
});
it("path: the same consent in production loads the gtag script (the only thing tied to NODE_ENV)", async () => {
const user = userEvent.setup();
render(<GoogleAnalytics />);
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
expect(
await screen.findByTestId("google-analytics-script"),
).toBeInTheDocument();
});
it("path: Escape on the first-visit bar -> bar stays and no choice is stored", async () => {
const user = userEvent.setup();
render(<GoogleAnalytics />);
const accept = await screen.findByRole("button", { name: "Aceitar" });
accept.focus();
await user.keyboard("{Escape}");
expect(screen.getByRole("dialog")).toBeVisible();
expect(localStorage.getItem(CONSENT_KEY)).toBeNull();
expect(screen.queryByText(/Escolha atual/)).toBeNull();
});
it("path: another key on the reopened bar -> stays open", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "denied");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await act(async () => {});
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
await user.keyboard("x{Tab}");
expect(screen.queryByRole("dialog")).not.toBeNull();
});
it("path: reopened with a refusal stored -> the bar announces 'recusado'", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "denied");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await act(async () => {});
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
expect(screen.getByRole("dialog")).toHaveAccessibleDescription(
/Escolha atual: recusado/,
);
});
});
describe("GoogleAnalytics: aceitar e recusar", () => {
it("path: reopened while already granted, 'Aceitar' only closes the bar", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
analyticsWindow.gtag = jest.fn();
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
const gtag = analyticsWindow.gtag as jest.Mock;
const callsAfterMount = gtag.mock.calls.length;
const opener = screen.getByRole("button", {
name: "Preferências de cookies",
});
await user.click(opener);
await user.click(screen.getByRole("button", { name: "Aceitar" }));
expect(screen.queryByRole("dialog")).toBeNull();
expect(gtag.mock.calls).toHaveLength(callsAfterMount);
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
expect(screen.getByTestId("google-analytics-script")).toBeInTheDocument();
expect(opener).toHaveFocus();
});
it("path: granting again after a revocation -> bootstrap runs once, GA is re-enabled", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
const opener = screen.getByRole("button", {
name: "Preferências de cookies",
});
await user.click(opener);
await user.click(screen.getByRole("button", { name: "Recusar" }));
expect(analyticsWindow[GA_DISABLE_KEY]).toBe(true);
expect(screen.queryByTestId("google-analytics-script")).toBeNull();
await user.click(opener);
await user.click(screen.getByRole("button", { name: "Aceitar" }));
expect(analyticsWindow[GA_DISABLE_KEY]).toBe(false);
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
expect(screen.getByTestId("google-analytics-script")).toBeInTheDocument();
// "default" e "js" só na primeira ativação; a configuração dos dois destinos roda a cada concessão
expect(commandsNamed("consent", "default")).toHaveLength(1);
expect(commandsNamed("js")).toHaveLength(1);
expect(commandsNamed("config", "G-QTT9QT0T3S")).toHaveLength(2);
expect(commandsNamed("config", "AW-18432694647")).toHaveLength(2);
});
it("path: first visit refused -> nothing is revoked because nothing was granted", async () => {
const user = userEvent.setup();
setCookie("_ga=keep-me; Path=/");
const cookieWrites = jest.spyOn(document, "cookie", "set");
render(<GoogleAnalytics />);
await user.click(await screen.findByRole("button", { name: "Recusar" }));
expect(localStorage.getItem(CONSENT_KEY)).toBe("denied");
expect(analyticsWindow[GA_DISABLE_KEY]).toBeUndefined();
expect(analyticsWindow.dataLayer).toBeUndefined();
expect(cookieWrites).not.toHaveBeenCalled();
expect(document.cookie).toContain("_ga=keep-me");
});
it("path: revoking after gtag disappeared -> still persists, disables GA and does not throw", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
delete analyticsWindow.gtag;
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
await user.click(screen.getByRole("button", { name: "Recusar" }));
expect(localStorage.getItem(CONSENT_KEY)).toBe("denied");
expect(analyticsWindow[GA_DISABLE_KEY]).toBe(true);
expect(screen.queryByRole("dialog")).toBeNull();
});
});
describe("GoogleAnalytics: cookies na revogação", () => {
it("path: only _ga and _ga_* are expired; look-alikes and other cookies stay", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
for (const name of COOKIES) setCookie(`${name}=1; Path=/`);
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
await user.click(screen.getByRole("button", { name: "Recusar" }));
const remaining = document.cookie.split(";").map((c) => c.trim());
expect(remaining).toEqual(
expect.arrayContaining(["_gid=1", "_gat=1", "portfolio_session=1"]),
);
expect(remaining.some((c) => c.startsWith("_ga="))).toBe(false);
expect(remaining.some((c) => c.startsWith("_ga_ABC123="))).toBe(false);
});
it("path: no analytics cookie present -> revocation writes no cookie", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
setCookie("portfolio_session=1; Path=/");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
const cookieWrites = jest.spyOn(document, "cookie", "set");
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
await user.click(screen.getByRole("button", { name: "Recusar" }));
expect(cookieWrites).not.toHaveBeenCalled();
expect(document.cookie).toContain("portfolio_session=1");
});
it("path: host is not the site domain (localhost) -> expiry is written without a Domain attribute", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "granted");
setCookie("_ga=1; Path=/");
render(
<>
<OpenPreferencesButton />
<GoogleAnalytics />
</>,
);
await screen.findByTestId("google-analytics-script");
const cookieWrites = jest.spyOn(document, "cookie", "set");
await user.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
await user.click(screen.getByRole("button", { name: "Recusar" }));
expect(cookieWrites).toHaveBeenCalledTimes(1);
expect(cookieWrites.mock.calls[0][0]).toMatch(/^_ga=; Max-Age=0;/);
expect(cookieWrites.mock.calls[0][0]).not.toContain("Domain=");
});
});
describe("GoogleAnalytics: gtag, dataLayer e carregador", () => {
it("path: dataLayer already exists -> the same array receives the commands", async () => {
const user = userEvent.setup();
const existing: IArguments[] = [];
analyticsWindow.dataLayer = existing;
render(<GoogleAnalytics />);
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
expect(analyticsWindow.dataLayer).toBe(existing);
expect(existing.length).toBeGreaterThan(0);
expect(typeof analyticsWindow.gtag).toBe("function");
});
it("path: a gtag already exists -> it is reused instead of being replaced", async () => {
const user = userEvent.setup();
const gtag = jest.fn();
analyticsWindow.gtag = gtag;
render(<GoogleAnalytics />);
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
expect(analyticsWindow.gtag).toBe(gtag);
expect(gtag).toHaveBeenCalledWith("js", expect.any(Date));
expect(analyticsWindow.dataLayer).toBeDefined();
});
it.each([
["denied", "denied"],
["no choice yet", null],
])(
"path: %s -> a loader left by next/script is removed",
async (_label, stored) => {
if (stored) localStorage.setItem(CONSENT_KEY, stored);
const loader = document.createElement("script");
loader.id = "google-analytics-script";
document.head.append(loader);
render(<GoogleAnalytics />);
await act(async () => {});
expect(document.getElementById("google-analytics-script")).toBeNull();
},
);
it("path: granted -> the loader stays", async () => {
localStorage.setItem(CONSENT_KEY, "granted");
const loader = document.createElement("script");
loader.id = "google-analytics-script";
document.head.append(loader);
render(<GoogleAnalytics />);
await act(async () => {});
expect(document.getElementById("google-analytics-script")).not.toBeNull();
});
});
describe("GoogleAnalytics: foco e falha de gravação", () => {
it("path: opener is not an HTML element (SVG) -> closing the bar has no element to refocus", async () => {
const user = userEvent.setup();
localStorage.setItem(CONSENT_KEY, "denied");
render(
<>
{/* biome-ignore lint/a11y/noNoninteractiveTabindex: elemento SVG de teste só para receber o foco */}
<svg data-testid="svg-opener" tabIndex={0} />
<GoogleAnalytics />
</>,
);
await act(async () => {});
const svg = screen.getByTestId("svg-opener");
svg.focus();
expect(document.activeElement).toBe(svg);
act(() => openCookiePreferences());
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).toBeNull();
expect(document.activeElement).not.toBe(svg);
});
it("path: acceptance fails to persist, then succeeds -> the alert goes away and analytics turns on", async () => {
const user = userEvent.setup();
const setItem = jest
.spyOn(Storage.prototype, "setItem")
.mockImplementation(() => {
throw new Error("storage blocked");
});
render(<GoogleAnalytics />);
await user.click(await screen.findByRole("button", { name: "Aceitar" }));
expect(
screen.getByRole("alert", { name: "Falha ao salvar o consentimento" }),
).toHaveTextContent(
"Não foi possível salvar sua aceitação. Tente novamente para concluir.",
);
expect(screen.getByRole("dialog")).toBeVisible();
expect(analyticsWindow.dataLayer).toBeUndefined();
setItem.mockRestore();
await user.click(screen.getByRole("button", { name: "Aceitar" }));
await waitFor(() => expect(screen.queryByRole("alert")).toBeNull());
expect(localStorage.getItem(CONSENT_KEY)).toBe("granted");
expect(screen.getByTestId("google-analytics-script")).toBeInTheDocument();
});
});