mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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.
This commit is contained in:
1 parent
d805ccf28b
commit
5c300a8d9c
2 files changed
+127
-11
No files matched your search
@@ -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>): 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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<LabList />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function count(html: string, needle: string): number {
|
||||
return html.split(needle).length - 1;
|
||||
}
|
||||
|
||||
describe("<LabList> 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 data-slot="lab-list-badge-cell"[^>]*><\/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/);
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div data-slot="lab-list-badge-cell" className={MOBILE_CELL.badge}>
|
||||
<ReferenceRangeBadge status={status} compact className="lg:w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LabRangeBarSlot({ reading }: { reading: LabResultDto }) {
|
||||
return (
|
||||
<div className={cn("w-full lg:w-48", MOBILE_CELL.rangeBar)}>
|
||||
@@ -274,11 +287,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
)}
|
||||
>
|
||||
{inner}
|
||||
<ReferenceRangeBadge
|
||||
status={group.latest.rangeStatus}
|
||||
compact
|
||||
className={cn("lg:w-full", MOBILE_CELL.badge)}
|
||||
/>
|
||||
<RangeBadgeCell status={group.latest.rangeStatus} />
|
||||
<LabRangeBarSlot reading={group.latest} />
|
||||
<div className={cn("w-[72px]", MOBILE_CELL.trend)}>
|
||||
<LabTrendSparkline
|
||||
@@ -287,9 +296,11 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
referenceHigh={group.latest.referenceHigh}
|
||||
/>
|
||||
</div>
|
||||
{/* With a pointer the row's hover already says it opens; a
|
||||
phone has no hover, so the chevron stays there. */}
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"text-muted-foreground h-4 w-4 shrink-0",
|
||||
"text-muted-foreground h-4 w-4 shrink-0 lg:invisible",
|
||||
MOBILE_CELL.end,
|
||||
)}
|
||||
/>
|
||||
@@ -300,11 +311,7 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
// link next to its clickable neighbours.
|
||||
<div key={group.key} className={ROW_CLASS}>
|
||||
{inner}
|
||||
<ReferenceRangeBadge
|
||||
status={group.latest.rangeStatus}
|
||||
compact
|
||||
className={cn("lg:w-full", MOBILE_CELL.badge)}
|
||||
/>
|
||||
<RangeBadgeCell status={group.latest.rangeStatus} />
|
||||
<LabRangeBarSlot reading={group.latest} />
|
||||
<div className={cn("w-[72px]", MOBILE_CELL.trend)}>
|
||||
<LabTrendSparkline
|
||||
|
||||
Reference in new issue
Block a user