/*
RFC-1192: tests for the conditional cookie-consent surface — self-gating on the
host node, recorded-choice skip, accept/decline flag writes, and the wg:cookie-consent
CustomEvent (AC-2 conditional render, AC-3 flag record).
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1192: initial consent-surface tests.
- RFC-1198: cookie-free referral attribution — /r/ partner landings + voluntary referrer field
Replace the RFC-1191 wg_ref cookie capture with URL+form attribution:
static /r/ and //r/ partner landings (noindex, zero JS,
consent-gated via publicNameConsentRef), ?ref prefill on the existing
referrerField rail, server-side slug resolution against the bundled
partners.yaml, referral_partner_slug/name on the Lagebild ingress contract,
legacy /?ref -> /r/ 302 in the generated worker, and full removal of
the cookie capture module (wg_ref, referral.attribution entitlement,
payload.attribution).
*/
import { afterEach, describe, expect, it, vi } from "vitest";
// RFC-1198: the live catalog is empty (consent surface dormant). The surface
// mechanism is still contract-covered — the tests inject a synthetic catalog
// entry so the gating/choice behavior stays exercised for future modules.
vi.mock("../../policy/cookies.ts", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
COOKIE_MODULES: [
{
moduleId: "example.module",
entitlement: "example.module",
cookies: [{ name: "example_pref", maxAgeDays: 30, sameSite: "Lax", purpose: "test" }],
},
],
};
});
import { COOKIE_CONSENT_EVENT, initCookieConsent } from "../cookie-consent.ts";
import { CONSENT_FLAG_KEY, readConsentFlag } from "../cookie-write-gate.ts";
interface FakeElement {
attrs: Record;
children: FakeElement[];
listeners: Record void) | undefined>;
style: { cssText: string };
textContent: string;
type: string;
setAttribute: (name: string, value: string) => void;
appendChild: (child: FakeElement) => void;
addEventListener: (type: string, fn: () => void) => void;
remove: ReturnType;
click: () => void;
}
function fakeElement(): FakeElement {
const el: FakeElement = {
attrs: {},
children: [],
listeners: {},
style: { cssText: "" },
textContent: "",
type: "",
setAttribute: (n, v) => {
el.attrs[n] = v;
},
appendChild: (c) => {
el.children.push(c);
},
addEventListener: (t, fn) => {
el.listeners[t] = fn;
},
remove: vi.fn(),
click: () => el.listeners.click?.(),
};
return el;
}
function stubDom(opts: { hostPresent: boolean; flagRaw?: string | null }) {
const data = new Map();
if (opts.flagRaw !== undefined && opts.flagRaw !== null) {
data.set(CONSENT_FLAG_KEY, opts.flagRaw);
}
const host = fakeElement();
const documentStub = {
querySelector: vi.fn((sel: string) =>
sel === "[data-cookie-consent-host]" && opts.hostPresent ? host : null,
),
createElement: vi.fn(() => fakeElement()),
};
const windowStub = {
localStorage: {
getItem: (k: string) => data.get(k) ?? null,
setItem: (k: string, v: string) => void data.set(k, v),
removeItem: (k: string) => void data.delete(k),
},
dispatchEvent: vi.fn(),
};
class FakeCustomEvent {
type: string;
detail: unknown;
constructor(type: string, init?: { detail?: unknown }) {
this.type = type;
this.detail = init?.detail;
}
}
vi.stubGlobal("document", documentStub);
vi.stubGlobal("window", windowStub);
vi.stubGlobal("CustomEvent", FakeCustomEvent);
return { host, documentStub, windowStub, data };
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("initCookieConsent gating (AC-2)", () => {
it("renders nothing when the host node is absent (unentitled site)", () => {
const { host, documentStub } = stubDom({ hostPresent: false });
initCookieConsent(["example.module"]);
expect(documentStub.createElement).not.toHaveBeenCalled();
expect(host.children).toHaveLength(0);
});
it("renders nothing when a consent flag already exists", () => {
const { documentStub } = stubDom({
hostPresent: true,
flagRaw: JSON.stringify({ v: 1, at: new Date().toISOString(), granted: false }),
});
initCookieConsent(["example.module"]);
expect(documentStub.createElement).not.toHaveBeenCalled();
});
it("renders nothing when no catalog modules are active for the site", () => {
const { host, documentStub } = stubDom({ hostPresent: true });
initCookieConsent([]);
expect(documentStub.createElement).not.toHaveBeenCalled();
expect(host.children).toHaveLength(0);
});
});
describe("initCookieConsent choices (AC-3)", () => {
it("accept writes {v:1, at, granted:true} and dispatches the consent event", () => {
const { host, windowStub } = stubDom({ hostPresent: true });
initCookieConsent(["example.module"]);
expect(host.children).toHaveLength(1);
const banner = host.children[0];
// last two appended children are the accept and decline buttons
const accept = banner.children[banner.children.length - 2];
accept.click();
expect(readConsentFlag()?.granted).toBe(true);
expect(windowStub.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: COOKIE_CONSENT_EVENT, detail: { granted: true } }),
);
expect(banner.remove).toHaveBeenCalled();
});
it("decline writes granted:false and dispatches the consent event", () => {
const { host, windowStub } = stubDom({ hostPresent: true });
initCookieConsent(["example.module"]);
const banner = host.children[0];
const decline = banner.children[banner.children.length - 1];
decline.click();
expect(readConsentFlag()?.granted).toBe(false);
expect(windowStub.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: COOKIE_CONSENT_EVENT, detail: { granted: false } }),
);
});
});