Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions tests/medication-content-first.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 { medicationIdentityBadges, type MedicationGovernance } from "@/lib/medication-badges";
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 }),
}));

// 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<typeof import("@/lib/medication-badges")>();
return { ...actual, medicationIdentityBadges: vi.fn(actual.medicationIdentityBadges) };
});
const badgesSpy = vi.mocked(medicationIdentityBadges);

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(<MedicationRecordPage slug="example" fallbackRecord={record} />);
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(<MedicationRecordPage slug="example" fallbackRecord={record} />);
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(<MedicationRecordPage slug="owner-only" />);
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(<MedicationRecordPage slug="owner-only" />);
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(<MedicationRecordPage slug="example" fallbackRecord={record} />);
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(<MedicationRecordPage slug="example" fallbackRecord={record} fallbackGovernance={fallbackGovernance} />);
// 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(<MedicationRecordPage slug="example" fallbackRecord={record} fallbackGovernance={fallbackGovernance} />);
// 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();
});
});
104 changes: 104 additions & 0 deletions tests/registry-content-first.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -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>): 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 (
<div data-testid="child">
lv:{String(record.verification?.locallyVerified)}|{record.title}
</div>
);
}

describe("RegistryRecordLoader content-first fallback", () => {
it("paints the public fixture immediately during loading instead of a spinner", () => {
hook.state = loaderState({ status: "loading" });
render(
<RegistryRecordLoader kind="service" slug="example" fallbackRecord={serviceRecord(false)}>
{child}
</RegistryRecordLoader>,
);
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(
<RegistryRecordLoader kind="service" slug="example" fallbackRecord={serviceRecord(true)}>
{child}
</RegistryRecordLoader>,
);
// 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(
<RegistryRecordLoader kind="service" slug="example">
{child}
</RegistryRecordLoader>,
);
// 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(
<RegistryRecordLoader kind="service" slug="example">
{child}
</RegistryRecordLoader>,
);
// 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(
<RegistryRecordLoader kind="service" slug="owner-only">
{child}
</RegistryRecordLoader>,
);
expect(screen.getByText(/Loading service record/i)).toBeInTheDocument();
expect(screen.queryByTestId("child")).not.toBeInTheDocument();
});
});
Loading