From 5c300a8d9c287e2db8b71876ff9a77ad8fbdc8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 3 Oct 2026 18:38:31 +0200 Subject: [PATCH] fix(labs): keep the range bar and trend in their columns when a reading has no range The range badge renders nothing for a reading without a reference range, so that row had four cells instead of five and the range bar and the trend moved one column to the left. The badge now sits in a cell of its own. The chevron is hidden from lg up, where the row's hover already says it opens; a phone keeps it. --- .../labs/__tests__/lab-list-columns.test.tsx | 109 ++++++++++++++++++ src/components/labs/lab-list.tsx | 29 +++-- 2 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 src/components/labs/__tests__/lab-list-columns.test.tsx diff --git a/src/components/labs/__tests__/lab-list-columns.test.tsx b/src/components/labs/__tests__/lab-list-columns.test.tsx new file mode 100644 index 000000000..6e3e8540d --- /dev/null +++ b/src/components/labs/__tests__/lab-list-columns.test.tsx @@ -0,0 +1,109 @@ +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 { LabResultDto, LabResultListResponse } from "../types"; + +/** + * Every row of the compact list is five grid cells from `lg` up. The range + * badge renders nothing for a reading without a reference range, so without + * its own cell the range bar and the trend slid one column to the left on + * exactly those rows. + */ + +vi.mock("@/lib/api/api-fetch", () => ({ + apiGet: () => new Promise(() => {}), + apiDelete: vi.fn(), +})); + +import { LabList } from "../lab-list"; + +function reading(over: Partial): LabResultDto { + return { + id: "r1", + biomarkerId: "b1", + panel: null, + analyte: "Ferritin", + value: 80, + valueText: null, + unit: "ng/mL", + referenceLow: 30, + referenceHigh: 400, + catalogReferenceLow: 30, + catalogReferenceHigh: 400, + sourceReferenceLow: null, + sourceReferenceHigh: null, + sourceReferenceText: null, + referenceOrigin: "catalog", + referenceDivergesFromCatalog: false, + takenAt: "2026-09-01T12:00:00.000Z", + source: "MANUAL", + hasNote: false, + rangeStatus: "in_range", + createdAt: "2026-09-01T12:00:00.000Z", + updatedAt: "2026-09-01T12:00:00.000Z", + ...over, + } as LabResultDto; +} + +function renderList(results: LabResultDto[]) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }); + const data: LabResultListResponse = { + results, + meta: { total: results.length, limit: 500, offset: 0 }, + }; + queryClient.setQueryData( + queryKeys.labResultsList({ + biomarkerId: undefined, + analyte: undefined, + panel: undefined, + from: undefined, + to: undefined, + page: 0, + sortDir: "desc", + }), + data, + ); + return renderToStaticMarkup( + + + + + , + ); +} + +function count(html: string, needle: string): number { + return html.split(needle).length - 1; +} + +describe(" columns", () => { + it("gives the badge its own cell even when a reading has no reference range", () => { + const html = renderList([ + reading({ id: "a", biomarkerId: "b1", analyte: "Ferritin" }), + reading({ + id: "b", + biomarkerId: "b2", + analyte: "Lipoprotein (a)", + referenceLow: null, + referenceHigh: null, + catalogReferenceLow: null, + catalogReferenceHigh: null, + rangeStatus: "unknown", + }), + ]); + expect(count(html, 'href="/labs/')).toBe(2); + // One cell per row, rendered even where the badge itself renders nothing. + expect(count(html, 'data-slot="lab-list-badge-cell"')).toBe(2); + expect(html).toMatch(/
]*><\/div>/); + }); + + it("hides the chevron from lg up and keeps it on a phone", () => { + const html = renderList([reading({})]); + expect(html).toMatch(/class="[^"]*lucide-chevron-right[^"]*lg:invisible/); + }); +}); diff --git a/src/components/labs/lab-list.tsx b/src/components/labs/lab-list.tsx index f57e1c5d3..60c1d0a82 100644 --- a/src/components/labs/lab-list.tsx +++ b/src/components/labs/lab-list.tsx @@ -78,6 +78,19 @@ function groupReadings(results: LabResultDto[]): MarkerGroup[] { ); } +/** + * The badge's grid cell. The badge renders nothing for a reading without a + * reference range, and a missing cell would pull the range bar and the trend + * one column to the left; the wrapper keeps every row at five cells. + */ +function RangeBadgeCell({ status }: { status: LabResultDto["rangeStatus"] }) { + return ( +
+ +
+ ); +} + function LabRangeBarSlot({ reading }: { reading: LabResultDto }) { return (
@@ -274,11 +287,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) { )} > {inner} - +
void } = {}) { referenceHigh={group.latest.referenceHigh} />
+ {/* With a pointer the row's hover already says it opens; a + phone has no hover, so the chevron stays there. */} @@ -300,11 +311,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) { // link next to its clickable neighbours.
{inner} - +