marmar-context / web / context / test / clinician-patient-detail.test.ts
clinician-patient-detail.test.ts
Raw
import { describe, expect, it } from "vitest";
import type { CheckInStatus, CheckRun, PatientDetail } from "../src/api/types";
import { statusLabels } from "../src/logic/format";
import { cancellable, details, findingsCell, headerLine, notesLabel, quietHoursLabel, statCells, summaryLabel } from "../src/logic/patient-detail";
import { checkInPill, checkInPills, patientStatePill, recordPill, urgentPill } from "../src/logic/pills";

const now = new Date("2026-10-30T10:00:00Z");

const patient = (change: Partial<PatientDetail> = {}): PatientDetail => ({
  id: "p-mariam",
  kind: "connected",
  name: "Mariam Adeyemi",
  age: 67,
  pronoun: null,
  sharing: {
    status: "connected",
    sharedBy: ["Mariam", "Funke"],
    scopes: ["medicines", "doses", "check_ins", "records"],
    allowsCheckInCalls: true,
    withheldSome: false,
    changedAt: "2026-10-30T09:00:00Z",
  },
  medicines: { count: 3 },
  doses: { taken: 12, expected: 14, onTime: 11 },
  lastCheckIn: { at: "2026-10-29T09:00:00Z", kind: "yasmar_call", status: "completed" },
  attention: { urgent: false, unreadNotes: 0, newSinceLastCheck: false },
  lastActivityAt: "2026-10-30T09:00:00Z",
  firstName: "Mariam",
  dateOfBirth: "1959-03-04",
  birthYear: 1959,
  sex: "female",
  phone: "+234 803 555 0147",
  preferredTitle: null,
  language: "en",
  hasOwnAccount: true,
  identityOnFile: "date_of_birth",
  timeZone: "Africa/Lagos",
  quietHours: { start: "22:00", end: "07:00" },
  quietHoursKnown: true,
  blockedTimes: [],
  checkInLimits: { usedToday: 0, usedThisWeek: 0, maxPerDay: 1, maxPerWeek: 3, nextPossibleAt: null },
  activeCheckInId: null,
  lastCheckId: "chk-1",
  newInformation: null,
  ...change,
});

const check = (tiers: string[]) =>
  ({ ranAt: "2026-10-30T08:00:00Z", assessment: { contextual: { findings: tiers.map((tier, i) => ({ id: `f${i}`, tier })) } } }) as unknown as CheckRun;

describe("check-in state pills (YCI-28)", () => {
  const all: CheckInStatus[] = ["sent", "ringing", "missed", "later", "in_progress", "completed", "declined", "not_reached", "identity_not_confirmed", "ended_early", "cancelled"];
  it("gives every state the backend sends its own words, icon and meaning", () => {
    expect(Object.keys(checkInPills).sort()).toEqual([...all].sort());
    for (const s of all) {
      const pill = checkInPill(s);
      expect(pill.label.length).toBeGreaterThan(2);
      expect(pill.meaning.length).toBeGreaterThan(10);
    }
    expect(new Set(all.map((s) => checkInPill(s).icon)).size).toBe(all.length);
  });
  it("uses the agreed colours", () => {
    expect(checkInPill("sent").tone).toBe("grey");
    expect(checkInPill("ringing").tone).toBe("indigo");
    expect([checkInPill("in_progress").tone, checkInPill("completed").tone]).toEqual(["green", "green"]);
    expect([checkInPill("missed").tone, checkInPill("not_reached").tone]).toEqual(["amber", "amber"]);
    expect(checkInPill("declined").tone).toBe("purple");
    expect(checkInPill("cancelled").tone).toBe("red-outline");
    expect(urgentPill).toMatchObject({ label: "Urgent", tone: "red" });
    expect(checkInPill("in_progress").label).toBe("Live");
  });
  it("names a state it does not know in plain words, never a code", () => {
    expect(checkInPill("expired").label).toBe("Expired");
    expect(checkInPill("waiting_for_phone")).toMatchObject({ label: "Waiting for phone", tone: "grey" });
  });
  it("keeps the older dot chips in step with the pills", () => {
    expect(statusLabels.in_progress).toEqual({ label: "Live", tone: "green" });
    expect(statusLabels.cancelled).toEqual({ label: "Cancelled", tone: "red" });
    expect(statusLabels.ringing.tone).toBe("");
  });
  it("never words a meaning as a decision or advice", () => {
    for (const pill of [...Object.values(checkInPills), urgentPill]) expect(pill.meaning).not.toMatch(/decide|diagnos|prescrib|recommend/i);
  });
});

describe("record and patient pills", () => {
  const rec = { reading: "read" as const, heldBack: null, lineCount: 4 };
  it("shows each reading state, held back first", () => {
    expect(recordPill(rec)).toMatchObject({ label: "Read · 4 lines", tone: "green" });
    expect(recordPill({ ...rec, lineCount: 1 }).label).toBe("Read · 1 line");
    expect(recordPill({ ...rec, reading: "failed" }).label).toBe("Not read");
    expect(recordPill({ ...rec, reading: "reading" }).label).toBe("Reading");
    expect(recordPill({ ...rec, reading: "queued" }).label).toBe("Waiting to be read");
    expect(recordPill({ ...rec, heldBack: { reason: "x" } }).label).toBe("Held back");
  });
  it("puts Connected or Clinic-added on the right of the header", () => {
    expect(patientStatePill(patient()).label).toBe("Connected");
    expect(patientStatePill(patient({ sharing: null })).label).toBe("Clinic-added");
    expect(patientStatePill(patient({ sharing: { ...patient().sharing!, status: "paused" } })).label).toBe("Paused");
  });
});

describe("the patient header and stat row", () => {
  it("says the date of birth, the clinic and where the patient came from", () => {
    expect(headerLine(patient(), "Surulere Family Clinic")).toEqual(["Born 4 Mar 1959", "Surulere Family Clinic", "Shared from MARMAR Health"]);
    expect(headerLine(patient({ sharing: null, kind: "clinic_added", dateOfBirth: null, birthYear: null }), "Surulere Family Clinic")).toEqual([
      "No date of birth on file",
      "Surulere Family Clinic",
      "Clinic patient",
    ]);
    expect(headerLine(patient({ dateOfBirth: null }), "")[0]).toBe("Born 1959");
  });
  it("fills medicines, doses (taken of due), last check-in and findings", () => {
    const cells = statCells(patient(), check(["today", "this_week"]), false, now);
    expect(cells.map((c) => [c.label, c.value])).toEqual([
      ["Medicines", "3"],
      ["Doses this week", "12 of 14"],
      ["Last check-in", "Yesterday"],
      ["Findings", "2"],
    ]);
    expect(cells[3].note).toBe("Checked today");
  });
  it('says "Not shared" for each area kept back, and for a clinic patient', () => {
    const cells = statCells(patient({ sharing: { ...patient().sharing!, scopes: ["medicines"], allowsCheckInCalls: false }, lastCheckIn: null }), null, false, now);
    expect(cells.slice(1, 3).map((c) => c.value)).toEqual(["Not shared", "Not shared"]);
    const clinic = statCells(patient({ kind: "clinic_added", sharing: null, lastCheckId: null }), null, false, now);
    expect(clinic.map((c) => c.value)).toEqual(["Not shared", "Not shared", "Not shared", "No check yet"]);
  });
  it("counts urgent review findings and never calls zero safe", () => {
    expect(findingsCell(patient(), check(["get_help_now", "today"]), false, now)).toEqual({ value: "2", note: "1 urgent review · checked today" });
    expect(findingsCell(patient(), check([]), false, now).value).toBe("0");
    expect(JSON.stringify(findingsCell(patient(), check([]), false, now))).not.toMatch(/safe/i);
    expect(findingsCell(patient(), null, true, now).value).toBe("…");
  });
});

describe("the rail's Details", () => {
  it("lists who shares, what, calls, quiet hours and the identity on file", () => {
    const d = details(patient(), now);
    expect(d.sharedBy).toMatchObject({ label: "Shared by", value: "Mariam and Funke" });
    expect(d.scopes!.map((s) => s.shared)).toEqual([true, true, true, true]);
    expect([d.calls, d.quietHours, d.identity]).toEqual(["Yasmar may call", "22:00 to 07:00", "Date of birth · 4 Mar 1959"]);
  });
  it("marks what is kept back and when quiet hours are not known", () => {
    const d = details(patient({ sharing: { ...patient().sharing!, scopes: ["medicines", "doses"], allowsCheckInCalls: false } }), now);
    expect(d.scopes!.filter((s) => !s.shared).map((s) => s.label)).toEqual(["Check-ins", "Records"]);
    expect(d.calls).toBe("Not allowed");
    expect(quietHoursLabel(patient({ quietHoursKnown: false }))).toBe("Not known");
    expect(quietHoursLabel(patient({ quietHours: { start: "00:00", end: "00:00" } }))).toBe("Off");
    expect(details(patient({ kind: "clinic_added", sharing: null }), now)).toMatchObject({ scopes: null, calls: "Needs MARMAR Health", quietHours: "Not known: no app" });
  });
});

describe("once sharing stops (INV-8)", () => {
  const stopped = patient({ sharing: { ...patient().sharing!, status: "stopped", changedAt: "2026-10-29T08:00:00Z" } });
  it("shows nothing live, says since when, and lists no scopes", () => {
    expect(statCells(stopped, null, false, now).slice(0, 3).map((c) => `${c.value} ${c.note}`)).toEqual(Array(3).fill("Stopped Since yesterday"));
    expect(details(stopped, now)).toMatchObject({ scopes: null, calls: "Not now", scopesNote: "Live access has ended. You keep the checks and check-ins you saved." });
  });
});

describe("check-in rows", () => {
  it("counts notes, the new ones, and whether a summary can still come", () => {
    expect(notesLabel({ noteCount: 6, unreadNotes: 2 })).toBe("6 notes · 2 new");
    expect(notesLabel({ noteCount: 0, unreadNotes: 0 })).toBe("None");
    expect(summaryLabel({ status: "completed", summaryReady: true })).toBe("Ready");
    expect(summaryLabel({ status: "in_progress", summaryReady: false })).toBe("Not yet");
    expect(summaryLabel({ status: "declined", summaryReady: false })).toBe("None");
  });
  it("offers Cancel only before the call starts, as the call view does", () => {
    expect(["sent", "ringing", "later", "missed"].every(cancellable)).toBe(true);
    expect(["in_progress", "completed", "cancelled"].some(cancellable)).toBe(false);
  });
});