test(labs): cover the biomarker assessment, detail page, and marker delete/range

Add coverage for the new biomarker-assessment seam and the marker detail
surface:

- the generator serves today's cache, re-stamps unchanged readings without an
  LLM call, and regenerates only when the latest-reading fingerprint changes
- the read-only endpoint returns the envelope, 422s a missing id, and honours
  the auth + insightStatus gates
- the detail page paints the stat strip, the chart placeholder, and the
  assessment card
- the biomarker resource route drops the marker with its readings in one
  userId-narrowed transaction and rejects an inverted target range

Hold the input gate independent of the worker's forced run so an idle marker
re-stamps cached text instead of regenerating, and list the new route on the
non-Coach gate allowlist.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 13:58:43 +02:00
1 parent 0790a96ca4
commit f17131dbdf
6 files changed
+699 -2

No files matched your search

@@ -0,0 +1,142 @@
import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import type {
BiomarkerDto,
LabResultDto,
LabResultListResponse,
} from "../types";
/**
* The per-biomarker detail page mirrors the metric sub-pages: a numbers-first
* stat strip over the numeric readings, the (lazy) chart, the history, and the
* AI assessment card. This test seeds the marker + reading queries and asserts
* the strip, the chart placeholder, and the assessment card all paint.
*/
vi.mock("@/lib/api/api-fetch", () => ({
apiGet: () => new Promise(() => {}),
apiDelete: vi.fn(),
apiPut: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ isAuthenticated: true, user: { id: "u1" } }),
}));
vi.mock("@/hooks/use-feature-flags", () => ({
useFeatureFlags: () => ({ insightStatus: true }),
}));
import { LabBiomarkerDetail } from "../lab-biomarker-detail";
const BIOMARKER_ID = "bm-1";
const MARKER: BiomarkerDto = {
id: BIOMARKER_ID,
name: "LDL Cholesterol",
unit: "mg/dL",
lowerBound: 0,
upperBound: 100,
panel: null,
hasContext: false,
context: null,
hidden: false,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
};
function reading(id: string, value: number, takenAt: string): LabResultDto {
return {
id,
biomarkerId: BIOMARKER_ID,
panel: null,
analyte: "LDL Cholesterol",
value,
valueText: null,
unit: "mg/dL",
referenceLow: 0,
referenceHigh: 100,
takenAt,
source: "MANUAL",
hasNote: false,
rangeStatus: "in-range",
createdAt: takenAt,
updatedAt: takenAt,
};
}
const LIST: LabResultListResponse = {
results: [
reading("r3", 95, "2026-06-20T08:00:00.000Z"),
reading("r2", 88, "2026-05-20T08:00:00.000Z"),
reading("r1", 102, "2026-04-20T08:00:00.000Z"),
],
meta: { total: 3, limit: 500, offset: 0 },
};
function renderWithCache() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
queryClient.setQueryData(queryKeys.biomarkerDetail(BIOMARKER_ID), MARKER);
queryClient.setQueryData(
queryKeys.labResultsList({
biomarkerId: BIOMARKER_ID,
analyte: undefined,
panel: undefined,
from: undefined,
to: undefined,
page: 0,
sortDir: "desc",
}),
LIST,
);
queryClient.setQueryData(
queryKeys.insightsBiomarkerAssessment(BIOMARKER_ID, "en"),
{
hasProvider: true,
text: "Your LDL is steady inside the reference range.",
cached: true,
updatedAt: "2026-06-20T09:00:00.000Z",
},
);
return renderToStaticMarkup(
<QueryClientProvider client={queryClient}>
<I18nProvider initialLocale="en">
<LabBiomarkerDetail biomarkerId={BIOMARKER_ID} />
</I18nProvider>
</QueryClientProvider>,
);
}
describe("<LabBiomarkerDetail>", () => {
const html = renderWithCache();
it("paints the marker heading", () => {
expect(html).toContain("LDL Cholesterol");
});
it("paints the numbers-first stat strip", () => {
expect(html).toContain('data-slot="metric-stat-strip"');
});
it("paints the chart placeholder (lazy chart shell)", () => {
// The recharts chart is `next/dynamic({ ssr: false })`; the server render
// paints the layout-stable skeleton shell in its place.
expect(html).toContain('data-slot="skeleton"');
});
it("paints the assessment card", () => {
// useMounted() is false during SSR, so the card renders its loading
// skeleton geometry — proof the assessment card mounted on the page.
expect(html).toContain('data-testid="insight-status-card-loading"');
});
});