88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|||
|
|
import { attributionForApi, getAttribution, inferPlatform, parseAttribution } from "@/lib/attribution";
|
||
|
|
|
||
|
|
describe("parseAttribution", () => {
|
||
|
|
const now = new Date("2026-09-22T12:00:00Z");
|
||
|
|
it("captures utm, click ids, variant, referrer and landing url", () => {
|
||
|
|
const a = parseAttribution(
|
||
|
|
"https://visit.waterschurch.org/?utm_source=facebook&utm_medium=paid_social&utm_campaign=fall-visit&utm_content=video1&utm_term=church+near+me&fbclid=ABC123&v=b",
|
||
|
|
"https://l.facebook.com/",
|
||
|
|
now,
|
||
|
|
"a",
|
||
|
|
);
|
||
|
|
expect(a.utm_source).toBe("facebook");
|
||
|
|
expect(a.utm_medium).toBe("paid_social");
|
||
|
|
expect(a.utm_campaign).toBe("fall-visit");
|
||
|
|
expect(a.utm_content).toBe("video1");
|
||
|
|
expect(a.utm_term).toBe("church near me");
|
||
|
|
expect(a.fbclid).toBe("ABC123");
|
||
|
|
expect(a.variant).toBe("b");
|
||
|
|
expect(a.referrer).toBe("https://l.facebook.com/");
|
||
|
|
expect(a.landing_url).toContain("utm_campaign=fall-visit");
|
||
|
|
expect(a.first_seen_at).toBe(now.toISOString());
|
||
|
|
});
|
||
|
|
it("supports Google, TikTok and X identifiers", () => {
|
||
|
|
const a = parseAttribution("https://x.test/?gclid=G1&gbraid=GB&wbraid=WB&ttclid=T1&twclid=X1", "", now, "a");
|
||
|
|
expect(a).toMatchObject({ gclid: "G1", gbraid: "GB", wbraid: "WB", ttclid: "T1", twclid: "X1" });
|
||
|
|
});
|
||
|
|
it("accepts tt_click_id as a TikTok alias", () => {
|
||
|
|
expect(parseAttribution("https://x.test/?tt_click_id=T9", "", now, "a").ttclid).toBe("T9");
|
||
|
|
});
|
||
|
|
it("tolerates missing or malformed parameters", () => {
|
||
|
|
const a = parseAttribution("not a url", "", now, "a");
|
||
|
|
expect(a.variant).toBe("a");
|
||
|
|
expect(a.utm_source).toBeUndefined();
|
||
|
|
const b = parseAttribution("https://x.test/?utm_source=&gclid=%3Cscript%3Ealert(1)%3C/script%3E", "", now, "a");
|
||
|
|
expect(b.utm_source).toBeUndefined();
|
||
|
|
expect(b.gclid).toBe("scriptalert(1)/script");
|
||
|
|
});
|
||
|
|
it("caps overly long values", () => {
|
||
|
|
const long = "x".repeat(500);
|
||
|
|
expect(parseAttribution(`https://x.test/?utm_campaign=${long}`, "", now, "a").utm_campaign).toHaveLength(200);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("inferPlatform", () => {
|
||
|
|
it("derives the ad platform", () => {
|
||
|
|
expect(inferPlatform({ gclid: "1" })).toBe("google");
|
||
|
|
expect(inferPlatform({ fbclid: "1" })).toBe("facebook");
|
||
|
|
expect(inferPlatform({ utm_source: "instagram" })).toBe("instagram");
|
||
|
|
expect(inferPlatform({ ttclid: "1" })).toBe("tiktok");
|
||
|
|
expect(inferPlatform({ utm_source: "x" })).toBe("x");
|
||
|
|
expect(inferPlatform({})).toBe("direct");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("getAttribution (browser)", () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
window.sessionStorage.clear();
|
||
|
|
window.history.replaceState({}, "", "/?utm_source=tiktok&utm_campaign=c1&ttclid=T1");
|
||
|
|
});
|
||
|
|
it("captures once and persists across calls", () => {
|
||
|
|
const first = getAttribution("a");
|
||
|
|
expect(first.utm_source).toBe("tiktok");
|
||
|
|
window.history.replaceState({}, "", "/");
|
||
|
|
const second = getAttribution("a");
|
||
|
|
expect(second.utm_campaign).toBe("c1");
|
||
|
|
expect(second.first_seen_at).toBe(first.first_seen_at);
|
||
|
|
});
|
||
|
|
it("merges a new click id from a second ad click without losing first-touch data", () => {
|
||
|
|
const first = getAttribution("a");
|
||
|
|
window.history.replaceState({}, "", "/?utm_source=facebook&fbclid=F1");
|
||
|
|
const merged = getAttribution("a");
|
||
|
|
expect(merged.fbclid).toBe("F1");
|
||
|
|
expect(merged.ttclid).toBe("T1");
|
||
|
|
expect(merged.first_seen_at).toBe(first.first_seen_at);
|
||
|
|
});
|
||
|
|
it("lets an explicit ?v= override the session variant", () => {
|
||
|
|
expect(getAttribution("a").variant).toBe("a");
|
||
|
|
window.history.replaceState({}, "", "/?v=b");
|
||
|
|
expect(getAttribution("a").variant).toBe("b");
|
||
|
|
});
|
||
|
|
it("flattens to strings for the API", () => {
|
||
|
|
const flat = attributionForApi(getAttribution("a"));
|
||
|
|
for (const v of Object.values(flat)) expect(typeof v).toBe("string");
|
||
|
|
expect(flat.utm_source).toBe("tiktok");
|
||
|
|
});
|
||
|
|
});
|