import { describe, it, expect, vi } from "vitest"; import { ANALYTICS_EVENTS, fireAnalyticsEvent } from "../lib/analytics"; describe("analytics", () => { describe("ANALYTICS_EVENTS constants", () => { it("exports all required event names", () => { expect(ANALYTICS_EVENTS.BOOKING_STEP_SERVICE).toBe("booking_step_service"); expect(ANALYTICS_EVENTS.BOOKING_STEP_TIME).toBe("booking_step_time"); expect(ANALYTICS_EVENTS.BOOKING_STEP_CONTACT).toBe("booking_step_contact"); expect(ANALYTICS_EVENTS.BOOKING_STEP_SUBMIT).toBe("booking_step_submit"); expect(ANALYTICS_EVENTS.BOOKING_CONFIRMED).toBe("booking_confirmed"); expect(ANALYTICS_EVENTS.BOOKING_ERROR).toBe("booking_error"); }); it("has no duplicate event names", () => { const values = Object.values(ANALYTICS_EVENTS); const unique = new Set(values); expect(unique.size).toBe(values.length); }); }); describe("fireAnalyticsEvent", () => { it("dispatches a CustomEvent with the correct event name", () => { const listener = vi.fn(); window.addEventListener(ANALYTICS_EVENTS.BOOKING_STEP_SERVICE, listener); fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_SERVICE, { step: "service", flow: "public" }); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0]![0] as CustomEvent; expect(event.type).toBe("booking_step_service"); expect(event.detail.step).toBe("service"); expect(event.detail.flow).toBe("public"); expect(event.detail.timestamp).toBeDefined(); window.removeEventListener(ANALYTICS_EVENTS.BOOKING_STEP_SERVICE, listener); }); it("includes a timestamp in the event detail", () => { const listener = vi.fn(); window.addEventListener(ANALYTICS_EVENTS.BOOKING_CONFIRMED, listener); fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_CONFIRMED, { step: "confirmed", flow: "public" }); const event = listener.mock.calls[0]![0] as CustomEvent; expect(event.detail.timestamp).toBeTruthy(); expect(new Date(event.detail.timestamp as string)).toBeInstanceOf(Date); window.removeEventListener(ANALYTICS_EVENTS.BOOKING_CONFIRMED, listener); }); it("does not throw when called with no payload", () => { expect(() => { fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_ERROR, {}); }).not.toThrow(); }); it("does not throw when window.dispatchEvent throws", () => { const original = window.dispatchEvent; window.dispatchEvent = () => { throw new Error("analytics blocked"); }; expect(() => { fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_SUBMIT, { step: "submit", flow: "public" }); }).not.toThrow(); window.dispatchEvent = original; }); it("fires events for all event types", () => { const events = Object.values(ANALYTICS_EVENTS); for (const eventName of events) { const listener = vi.fn(); window.addEventListener(eventName, listener); fireAnalyticsEvent(eventName as typeof events[number], { step: "test", flow: "public" }); expect(listener).toHaveBeenCalledTimes(1); window.removeEventListener(eventName, listener); } }); it("does not include PII in payload", () => { // Payload only contains step, flow, and timestamp — no names, emails, or phones const payload = { step: "contact", flow: "public" }; const keys = Object.keys(payload); const piish = ["name", "email", "phone", "clientName", "clientEmail", "clientPhone", "petName"]; const hasPII = piish.some((k) => keys.includes(k)); expect(hasPII).toBe(false); }); }); });