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 => ({ 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); }); });