112c61ab1c
Lines 28 and 40 access mock.calls[0] which is possibly undefined under strict TypeScript. Adding ! to satisfy TS2532. Co-Authored-By: Paperclip <noreply@paperclip.ing>
84 lines
3.6 KiB
TypeScript
84 lines
3.6 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|