marmar-context / web / context / test / clinician-events.test.ts
clinician-events.test.ts
Raw
// The live check-in follower: without EventSource it polls /updates every 3 seconds,
// passes each event on once, and stops when closed.
import { afterEach, describe, expect, it, vi } from "vitest";
import { followCheckIn, POLL_MS } from "../src/api/events";
import type { CheckInEvent } from "../src/api/types";

const status = (id: number): CheckInEvent => ({ id, type: "status", at: `2026-10-30T10:00:0${id}Z`, status: "ringing", detail: null, later: null });

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

describe("following a check-in (YCI-23)", () => {
  it("polls every 3 seconds from the last event id, without repeating events", async () => {
    vi.useFakeTimers();
    const seen: string[] = [];
    const answers = [
      { events: [status(4), status(5)], lastEventId: 5 },
      { events: [status(5), status(6)], lastEventId: 6 },
      { events: [], lastEventId: 6 },
    ];
    const fetchMock = vi.fn(async (url: string, init: RequestInit) => {
      seen.push(`${url} ${(init.headers as Record<string, string>)["X-Context-Background"] ?? ""}`);
      return new Response(JSON.stringify(answers.shift() ?? { events: [], lastEventId: 6 }), { status: 200 });
    });
    vi.stubGlobal("fetch", fetchMock);
    const got: number[] = [];
    const modes: string[] = [];
    const sub = followCheckIn("c1", 3, (events) => got.push(...events.map((e) => e.id)), (m) => modes.push(m), { forcePolling: true });
    await vi.advanceTimersByTimeAsync(10);
    expect(got).toEqual([4, 5]);
    await vi.advanceTimersByTimeAsync(POLL_MS);
    expect(got).toEqual([4, 5, 6]);
    expect(seen[0]).toBe("/api/clinician/check-ins/c1/updates?after=3 1");
    expect(seen[1]).toContain("after=5");
    sub.close();
    const calls = fetchMock.mock.calls.length;
    await vi.advanceTimersByTimeAsync(POLL_MS * 3);
    expect(fetchMock.mock.calls.length).toBe(calls);
    expect(modes).toEqual(["polling", "closed"]);
  });

  it("keeps polling after a failed request", async () => {
    vi.useFakeTimers();
    let fail = true;
    vi.stubGlobal(
      "fetch",
      vi.fn(async () => {
        if (fail) {
          fail = false;
          throw new TypeError("offline");
        }
        return new Response(JSON.stringify({ events: [status(2)], lastEventId: 2 }), { status: 200 });
      }),
    );
    const got: number[] = [];
    const sub = followCheckIn("c1", 1, (events) => got.push(...events.map((e) => e.id)), () => undefined, { forcePolling: true });
    await vi.advanceTimersByTimeAsync(10);
    expect(got).toEqual([]);
    await vi.advanceTimersByTimeAsync(POLL_MS);
    expect(got).toEqual([2]);
    sub.close();
  });
});