Ctrl K
ComunidadGuías…

lib/analytics-demo-events.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

/**
 * MIN-150 — do the three events in the dictation demo really happen
 * until `posthog.capture` ?
 *
 * The question is not rhetorical: between the component call and PostHog, there are
 * two gates which drop to SILENCE — the runtime allowlist
 * (`ALLOWED_ANALYTICS_EVENTS`, a name missing from the `EVENT_NAMES` array is thrown)
 * and sanitization of props (everything that is not a primitive disappears).
 * The typing covers the call site, not these two: a cataloged event but
 * forgotten in `EVENT_NAMES` compiles and never leaves.
 *
 * So we inject a fake client in place of PostHog and we see what
 * reaches it. What happens AFTER `capture` is up to posthog-js.
 */

type Captured = { event: string; props: Record<string, unknown> | undefined };

async function withStubClient() {
  vi.resetModules();
  // `trackEvent` refuse de partir hors navigateur (`typeof window === "undefined"`) :
  // the suite runs in a node environment, so you have to give it one.
  vi.stubGlobal("window", {});
  const mod = await import("./analytics");
  const captured: Captured[] = [];
  mod.setAnalyticsClient({
    __loaded: true,
    capture: (event: string, props?: Record<string, unknown>) =>
      captured.push({ event, props }),
  } as never);
  return { ...mod, captured };
}

describe("les événements de la démo de dictée atteignent PostHog", () => {
  beforeEach(() => {
    vi.resetModules();
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("émet le départ, avec la provenance de la prise", async () => {
    const { trackEvent, captured } = await withStubClient();

    trackEvent("landing_voice_demo_started", { input: "mic" });
    trackEvent("landing_voice_demo_started", { input: "sample" });

    expect(captured).toEqual([
      { event: "landing_voice_demo_started", props: { input: "mic" } },
      { event: "landing_voice_demo_started", props: { input: "sample" } },
    ]);
  });

  it("émet la réussite avec la tranche d'attente, jamais le texte dicté", async () => {
    const { trackEvent, captured } = await withStubClient();

    trackEvent("landing_voice_demo_completed", {
      input: "sample",
      duration_bucket: "1_5s",
    });

    expect(captured[0].event).toBe("landing_voice_demo_completed");
    expect(captured[0].props).toEqual({ input: "sample", duration_bucket: "1_5s" });
    // The content of the ticket and the dictated sentence never leave the browser.
    expect(JSON.stringify(captured[0].props)).not.toMatch(/Stripe|Léa/);
  });

  it("émet l'échec avec un motif STABLE, pas un message d'erreur", async () => {
    const { trackEvent, captured } = await withStubClient();

    for (const reason of ["rate_limited", "empty", "denied", "unsupported", "http_502"]) {
      trackEvent("landing_voice_demo_failed", { input: "mic", reason });
    }

    expect(captured.map((c) => c.props?.reason)).toEqual([
      "rate_limited",
      "empty",
      "denied",
      "unsupported",
      "http_502",
    ]);
  });

  it("jette un nom voisin mais absent du catalogue", async () => {
    const { trackEvent, captured } = await withStubClient();

    // The typical mistake: rename the event in the component and forget
    // `EVENT_NAMES`. It must not pass, and it does not pass.
    (trackEvent as (e: string, p?: unknown) => void)("landing_voice_demo_start", {
      input: "mic",
    });

    expect(captured).toEqual([]);
  });

  it("ne part pas tant que PostHog n'est pas chargé", async () => {
    vi.resetModules();
    vi.stubGlobal("window", {});
    const { trackEvent } = await import("./analytics");
    // No client filed: the call is inert, it should not be raised.
    expect(() => trackEvent("landing_voice_demo_started", { input: "mic" })).not.toThrow();
  });
});