From 2a9bbe151b399e6c73ff978ea89338095573350a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 01:35:38 +0000 Subject: [PATCH 1/2] test(ui): content-first fallback regression tests for medications + registry Locks in the content-first behavior shipped in #1024, including the clinical-governance guarantee CodeRabbit flagged (no authoritative verification badge before live governance reconciles). registry-content-first.dom.test.tsx (RegistryRecordLoader): - content-first: the public fixture paints during loading, not a spinner - governance safety: a fixture's `locallyVerified` is neutralized to false in the provisional paint, then reconciled from authoritative governance on ready - owner-only slugs (no fixture) still show the spinner medication-content-first.dom.test.tsx (MedicationRecordPage): - content-first: the fallback record paints during loading, not the skeleton - the live record swaps in on resolve - no fallback -> skeleton while loading / error state on a failed fetch - graceful: a failed live fetch keeps showing fallback content, not an error Addresses outstanding-issues #015. Tests-only; no runtime change. Verified: typecheck, lint, and the full offline unit suite pass (9 new tests). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UbhUVWVJRwDibC2YtJ6aRX --- tests/medication-content-first.dom.test.tsx | 64 ++++++++++++ tests/registry-content-first.dom.test.tsx | 104 ++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 tests/medication-content-first.dom.test.tsx create mode 100644 tests/registry-content-first.dom.test.tsx diff --git a/tests/medication-content-first.dom.test.tsx b/tests/medication-content-first.dom.test.tsx new file mode 100644 index 00000000..55230488 --- /dev/null +++ b/tests/medication-content-first.dom.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { ReactElement } from "react"; + +import { MedicationRecordPage } from "@/components/clinical-dashboard/medication-record-page"; +import { PatientProfileProvider } from "@/components/clinical-dashboard/patient-profile-context"; +import { loadMedicationSnapshot } from "@/lib/medication-snapshot"; +import type { MedicationRecord } from "@/lib/medications"; + +type DetailState = { + data: { record: MedicationRecord } | null; + loading: boolean; + error: string | null; +}; + +// Mutable holder so each test can drive the mocked detail hook. +const hook = vi.hoisted(() => ({ detail: null as unknown as DetailState })); +vi.mock("@/components/clinical-dashboard/use-medication-catalog", () => ({ + useMedicationDetail: () => hook.detail, + useMedicationCatalog: () => ({ data: null, loading: false, error: null }), +})); + +const record = loadMedicationSnapshot()[0]; +const liveRecord: MedicationRecord = { ...record, name: `${record.name} (live)` }; + +// The detail body renders PatientProfilePanel, which reads the sessionStorage-backed +// patient-profile context — provide the real provider so the tree renders faithfully. +function renderPage(ui: ReactElement) { + return render(ui, { wrapper: PatientProfileProvider }); +} + +describe("MedicationRecordPage content-first fallback", () => { + it("paints the fallback record immediately during loading instead of the skeleton", () => { + hook.detail = { data: null, loading: true, error: null }; + renderPage(); + expect(screen.getByRole("heading", { name: record.name })).toBeInTheDocument(); + expect(screen.queryByText(/Loading medication reference/i)).not.toBeInTheDocument(); + }); + + it("swaps in the live record once the fetch resolves", () => { + hook.detail = { data: { record: liveRecord }, loading: false, error: null }; + renderPage(); + expect(screen.getByRole("heading", { name: liveRecord.name })).toBeInTheDocument(); + }); + + it("shows the skeleton while loading when there is no fallback (owner-only slug)", () => { + hook.detail = { data: null, loading: true, error: null }; + renderPage(); + expect(screen.getByText(/Loading medication reference/i)).toBeInTheDocument(); + }); + + it("shows the error/not-found state when there is no fallback and the fetch fails", () => { + hook.detail = { data: null, loading: false, error: "Could not load medication." }; + renderPage(); + expect(screen.getByText("Could not load medication.")).toBeInTheDocument(); + }); + + it("keeps showing fallback content when the live fetch fails (graceful content-first)", () => { + hook.detail = { data: null, loading: false, error: "boom" }; + renderPage(); + expect(screen.getByRole("heading", { name: record.name })).toBeInTheDocument(); + expect(screen.queryByText("boom")).not.toBeInTheDocument(); + }); +}); diff --git a/tests/registry-content-first.dom.test.tsx b/tests/registry-content-first.dom.test.tsx new file mode 100644 index 00000000..96c1a243 --- /dev/null +++ b/tests/registry-content-first.dom.test.tsx @@ -0,0 +1,104 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { RegistryRecordLoader } from "@/components/registry-record-loader"; +import { serviceRecords, type ServiceRecord } from "@/lib/services"; +import type { RegistryRecordResult } from "@/lib/use-registry-records"; + +// Mutable holder so each test can drive the mocked hook's returned state. +const hook = vi.hoisted(() => ({ state: null as unknown as RegistryRecordResult })); +vi.mock("@/lib/use-registry-records", () => ({ + useRegistryRecord: () => hook.state, +})); + +const baseRecord = serviceRecords[0]; + +function serviceRecord(locallyVerified: boolean): ServiceRecord { + return { ...baseRecord, verification: { ...baseRecord.verification, locallyVerified } }; +} + +function loaderState(overrides: Partial): RegistryRecordResult { + return { + status: "loading", + record: null, + linkedDocuments: [], + demoMode: false, + governance: null, + refetch: vi.fn(), + ...overrides, + }; +} + +// Render-prop that surfaces the verified flag + title the loader hands down, so +// the tests can assert exactly what content-first rendered. +function child(record: ServiceRecord) { + return ( +
+ lv:{String(record.verification?.locallyVerified)}|{record.title} +
+ ); +} + +describe("RegistryRecordLoader content-first fallback", () => { + it("paints the public fixture immediately during loading instead of a spinner", () => { + hook.state = loaderState({ status: "loading" }); + render( + + {child} + , + ); + expect(screen.getByTestId("child")).toHaveTextContent(baseRecord.title); + expect(screen.queryByText(/Loading service record/i)).not.toBeInTheDocument(); + }); + + it("neutralizes a stale 'locally verified' flag during the provisional paint", () => { + hook.state = loaderState({ status: "loading" }); + render( + + {child} + , + ); + // Fixture claims verified=true, but the pre-reconciliation paint must show false + // so a stale authoritative-looking badge can't flash in. + expect(screen.getByTestId("child")).toHaveTextContent("lv:false"); + }); + + it("reconciles the verified flag from authoritative governance when ready", () => { + hook.state = loaderState({ + status: "ready", + record: serviceRecord(true), + governance: { sourceStatus: "current", validationStatus: "unverified" }, + }); + const { rerender } = render( + + {child} + , + ); + // Fixture verified=true, but authoritative governance says unverified -> false. + expect(screen.getByTestId("child")).toHaveTextContent("lv:false"); + + hook.state = loaderState({ + status: "ready", + record: serviceRecord(false), + governance: { sourceStatus: "current", validationStatus: "locally_reviewed" }, + }); + rerender( + + {child} + , + ); + // Authoritative governance says reviewed -> true (reconciled up). + expect(screen.getByTestId("child")).toHaveTextContent("lv:true"); + }); + + it("keeps the spinner for owner-only slugs with no fixture fallback", () => { + hook.state = loaderState({ status: "loading" }); + render( + + {child} + , + ); + expect(screen.getByText(/Loading service record/i)).toBeInTheDocument(); + expect(screen.queryByTestId("child")).not.toBeInTheDocument(); + }); +}); From 04d0b3e4d2893401eaf6838352b6a69658f1a435 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 01:47:09 +0000 Subject: [PATCH 2/2] test(ui): exercise the medication governance-on-error guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit review on #1054: the "graceful on error" test never passed fallbackGovernance, so it did not actually exercise the `governance = data?.governance ?? (error ? undefined : fallbackGovernance)` guard in medication-record-page.tsx — it would still pass if the `error ?` check were dropped. Partial-mock medication-badges (keep all real exports so the detail body renders; wrap medicationIdentityBadges in a call-through spy) and add two tests asserting the exact governance the page hands down: present while loading, dropped to undefined once the fetch fails. Now fails if the error guard regresses. (CodeRabbit's suggested `/locally verified/i` assertion was vacuous — there is no such text; the badge label is "Reviewed" and `isReviewed` also has record-based logic, so asserting the governance argument via the spy is the robust signal.) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UbhUVWVJRwDibC2YtJ6aRX --- tests/medication-content-first.dom.test.tsx | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/medication-content-first.dom.test.tsx b/tests/medication-content-first.dom.test.tsx index 55230488..f53bb38f 100644 --- a/tests/medication-content-first.dom.test.tsx +++ b/tests/medication-content-first.dom.test.tsx @@ -4,6 +4,7 @@ import type { ReactElement } from "react"; import { MedicationRecordPage } from "@/components/clinical-dashboard/medication-record-page"; import { PatientProfileProvider } from "@/components/clinical-dashboard/patient-profile-context"; +import { medicationIdentityBadges, type MedicationGovernance } from "@/lib/medication-badges"; import { loadMedicationSnapshot } from "@/lib/medication-snapshot"; import type { MedicationRecord } from "@/lib/medications"; @@ -20,6 +21,16 @@ vi.mock("@/components/clinical-dashboard/use-medication-catalog", () => ({ useMedicationCatalog: () => ({ data: null, loading: false, error: null }), })); +// Partial mock: keep every real export (the detail body renders normally) but wrap +// medicationIdentityBadges in a spy that still calls through, so we can assert the +// exact governance MedicationRecordPage hands down — robust against badge rendering, +// ordering, and BadgeCluster overflow. +vi.mock("@/lib/medication-badges", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, medicationIdentityBadges: vi.fn(actual.medicationIdentityBadges) }; +}); +const badgesSpy = vi.mocked(medicationIdentityBadges); + const record = loadMedicationSnapshot()[0]; const liveRecord: MedicationRecord = { ...record, name: `${record.name} (live)` }; @@ -61,4 +72,26 @@ describe("MedicationRecordPage content-first fallback", () => { expect(screen.getByRole("heading", { name: record.name })).toBeInTheDocument(); expect(screen.queryByText("boom")).not.toBeInTheDocument(); }); + + const fallbackGovernance: MedicationGovernance = { sourceStatus: "current", validationStatus: "approved" }; + + it("presents the SSR fallback governance to the record while the live fetch is loading", () => { + hook.detail = { data: null, loading: true, error: null }; + badgesSpy.mockClear(); + renderPage(); + // Loading with no error → the fixture-derived fallback governance is shown. + expect(badgesSpy.mock.calls.at(-1)?.[1]).toEqual(fallbackGovernance); + }); + + it("drops the fallback governance (not authoritative) once the live fetch fails", () => { + hook.detail = { data: null, loading: false, error: "boom" }; + badgesSpy.mockClear(); + renderPage(); + // A failed fetch means the authoritative status is unknown, so governance must + // NOT keep presenting the fixture value — this is the `error ? undefined : ...` + // guard, and the test fails if that error check is dropped. + expect(badgesSpy.mock.calls.at(-1)?.[1]).toBeUndefined(); + // ...while the record content itself still renders (graceful content-first). + expect(screen.getByRole("heading", { name: record.name })).toBeInTheDocument(); + }); });