/*
Unit tests for the RFC-1241 first-touch UTM acquisition module:
capture semantics (first-touch, sentinels, charset), storage-injected reads,
and the shared structural validator.
- Do not test browser APIs — storage is faked; no jsdom dependency.
- RFC-1311: Compass header sweep — declares the acquisition suite's contract for test-file scaffold coverage.
*/
import { test, expect } from "vitest";
import {
captureAcquisition,
readAcquisition,
isValidAcquisitionContext,
ACQUISITION_STORAGE_KEY,
type AcquisitionStorage,
} from "../acquisition.ts";
function fakeStorage(initial?: Record): AcquisitionStorage & {
data: Record;
} {
const data: Record = { ...(initial ?? {}) };
return {
data,
getItem: (k) => (k in data ? data[k]! : null),
setItem: (k, v) => {
data[k] = v;
},
};
}
const FULL_SEARCH =
"?utm_source=meta&utm_medium=paid_social&utm_campaign=sit001-bw-v1" +
"&utm_term=var_a1&utm_content=cre_b3d8e2f1&utm_id=exp_7f2a91c4";
test("capture: all six params stored verbatim", () => {
const s = fakeStorage();
captureAcquisition(FULL_SEARCH, s);
expect(readAcquisition(s)).toEqual({
schema: "acquisition-context/v1",
utm_source: "meta",
utm_medium: "paid_social",
utm_campaign: "sit001-bw-v1",
utm_term: "var_a1",
utm_content: "cre_b3d8e2f1",
utm_id: "exp_7f2a91c4",
});
});
test("capture: absent secondary keys become the '-' sentinel", () => {
const s = fakeStorage();
captureAcquisition("?utm_source=meta&utm_medium=cpc", s);
expect(readAcquisition(s)).toEqual({
schema: "acquisition-context/v1",
utm_source: "meta",
utm_medium: "cpc",
utm_campaign: "-",
utm_term: "-",
utm_content: "-",
utm_id: "-",
});
});
test("capture: no utm_source stores nothing", () => {
const s = fakeStorage();
captureAcquisition("?utm_medium=cpc&utm_campaign=x", s);
expect(s.data[ACQUISITION_STORAGE_KEY]).toBeUndefined();
});
test("capture: charset-invalid utm_source stores nothing", () => {
const s = fakeStorage();
captureAcquisition("?utm_source=meta%20ads+spam", s);
expect(s.data[ACQUISITION_STORAGE_KEY]).toBeUndefined();
captureAcquisition("?utm_source=facebook.com/x", s); // '/' not in charset
expect(s.data[ACQUISITION_STORAGE_KEY]).toBeUndefined();
});
test("capture: invalid secondary value becomes '-' while block still stores", () => {
const s = fakeStorage();
captureAcquisition("?utm_source=meta&utm_campaign=hello%20world", s);
const block = readAcquisition(s);
expect(block?.utm_source).toBe("meta");
expect(block?.utm_campaign).toBe("-");
});
test("capture: first-touch — earlier capture is never overwritten", () => {
const s = fakeStorage();
captureAcquisition("?utm_source=first&utm_medium=a", s);
captureAcquisition("?utm_source=second&utm_medium=b", s);
expect(readAcquisition(s)?.utm_source).toBe("first");
captureAcquisition("?utm_medium=only", s); // would be a no-op anyway
expect(readAcquisition(s)?.utm_source).toBe("first");
});
test("capture: accepts URLSearchParams", () => {
const s = fakeStorage();
captureAcquisition(new URLSearchParams("utm_source=meta"), s);
expect(readAcquisition(s)?.utm_source).toBe("meta");
});
test("capture: storage exceptions are swallowed", () => {
const throwing: AcquisitionStorage = {
getItem: () => {
throw new Error("denied");
},
setItem: () => {
throw new Error("denied");
},
};
expect(() => captureAcquisition("?utm_source=meta", throwing)).not.toThrow();
});
test("read: missing key returns undefined", () => {
expect(readAcquisition(fakeStorage())).toBeUndefined();
});
test("read: corrupt JSON returns undefined", () => {
const s = fakeStorage({ [ACQUISITION_STORAGE_KEY]: "{not-json" });
expect(readAcquisition(s)).toBeUndefined();
});
test("read: structurally invalid stored block returns undefined", () => {
const s = fakeStorage({
[ACQUISITION_STORAGE_KEY]: JSON.stringify({ schema: "acquisition-context/v1" }),
});
expect(readAcquisition(s)).toBeUndefined();
});
test("validator: rejects wrong schema, missing keys, extra keys, bad charset", () => {
const valid = {
schema: "acquisition-context/v1",
utm_source: "a",
utm_medium: "-",
utm_campaign: "c",
utm_term: "-",
utm_content: "-",
utm_id: "x",
};
expect(isValidAcquisitionContext(valid)).toBe(true);
expect(isValidAcquisitionContext({ ...valid, schema: "other/v1" })).toBe(false);
expect(isValidAcquisitionContext({ ...valid, extra: "x" })).toBe(false);
const { utm_id: _dropped, ...missing } = valid;
expect(isValidAcquisitionContext(missing)).toBe(false);
expect(isValidAcquisitionContext({ ...valid, utm_source: "has space" })).toBe(false);
expect(isValidAcquisitionContext({ ...valid, utm_medium: 42 })).toBe(false);
expect(isValidAcquisitionContext(null)).toBe(false);
expect(isValidAcquisitionContext("string")).toBe(false);
});