Files
HealthLog/e2e/labs-list-wide.spec.ts
Marc-André Bombeck 29798a76e3 fix(labs): keep the trend column as wide as its sparkline on a wide list
The wide tier let the trend column grow to 8rem while the sparkline it
holds is a fixed 72px glyph, so at 1920 px an empty 56 px strip opened
between the sparkline and the chevron. The column now stays 72px in both
tiers; the extra width goes to the range bar and the name, as before.

The wide-list spec reads the resolved grid tracks and asserts the trend
track is 72px at every width it covers.
2026-10-04 14:28:04 +02:00

141 lines
5.5 KiB
TypeScript

import { expect, test } from "./setup/test";
import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
import {
expectNoHorizontalOverflow,
settleForOverflowMeasurement,
} from "./utils/horizontal-overflow";
/**
* The lab list uses the width it has, at every width, inside a frame that does
* not move.
*
* The signed-in frame is 1280 px wide on purpose, so a page never jumps
* sideways when you move between routes. Inside it the compact list was one
* five-column row whose only flexible column was the name: on a wide window the
* name column sat mostly empty while the range bar and the trend stayed small
* at the right edge. The columns now follow the width of the list's own card
* (a container query), not the window's:
*
* - below 64 rem of card the row is the `lg` table it always was, and the
* range bar keeps its 12 rem;
* - from 64 rem the bar follows its column up to 18 rem, and the trend
* column stays as wide as its 72 px sparkline.
*
* From `lg` a long name wraps instead of being cut off. The tiers are measured
* on the card's width and not the viewport's, so the spec reads the card and
* asserts the tier that width belongs to; a width at which no card ever reaches
* 64 rem would otherwise pass the wide check by never running it.
*
* The account is the one `globalSetup` seeds for the mobile route sweep: a long
* analyte name, readings with reference ranges. Desktop only; the phone layout
* has its own specs.
*/
const WIDTHS = [1280, 1920, 3440] as const;
const REM = 16;
const BAR = '[data-slot="lab-reference-range-bar"]';
test.describe("the lab list uses its width", () => {
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
test.beforeEach(async ({ context }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"desktop-only spec",
);
const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
await context.addCookies([
{ name: "healthlog-locale", value: "de", url: baseURL },
]);
});
for (const width of WIDTHS) {
test(`at ${width} px: no sideways scroll, names read in full, the bar follows its tier`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 1000 });
await page.goto("/labs", { waitUntil: "domcontentloaded" });
const names = page.locator('[data-slot="lab-list-analyte"]');
await expect(names.first()).toBeVisible();
await settleForOverflowMeasurement(page);
await expectNoHorizontalOverflow(page, `/labs at ${width}px`);
// The long name is the point of the wrap: it is fully on screen.
const long = page
.locator('[data-slot="lab-list-analyte"]')
.filter({ hasText: MOBILE_ROUTES_ANALYTE });
await expect(long.first()).toBeVisible();
const clipped = await names.evaluateAll(
(nodes) =>
nodes.filter((node) => node.scrollWidth > node.clientWidth + 1)
.length,
);
expect(clipped, "an analyte name is cut off").toBe(0);
const cardWidth = await page
.locator('[data-slot="lab-list"] [data-slot="card"]')
.first()
.evaluate((node) => node.getBoundingClientRect().width);
const bars = page.locator(BAR);
expect(await bars.count()).toBeGreaterThan(0);
const barWidths = await bars.evaluateAll((nodes) =>
nodes.map((node) => node.getBoundingClientRect().width),
);
if (cardWidth < 64 * REM) {
// The narrow tier: exactly what the list had, 12 rem.
for (const w of barWidths) expect(Math.round(w)).toBe(12 * REM);
} else {
// The wide tier: wider than 12 rem, no wider than its column's cap.
for (const w of barWidths) {
expect(w).toBeGreaterThan(12 * REM);
expect(w).toBeLessThanOrEqual(18 * REM + 1);
}
}
// The trend column is exactly as wide as the sparkline it holds, in both
// tiers. The sparkline is a fixed 72 px glyph; a wider column would only
// open an empty strip between it and the chevron.
const tracks = await page
.locator('[data-slot="lab-list"] [data-slot="card-content"]')
.first()
.evaluate((node) => getComputedStyle(node).gridTemplateColumns);
const trendTrack = tracks.split(" ")[3];
expect(trendTrack, `trend column in "${tracks}"`).toBe("72px");
// The widest windows must actually reach the wide tier, or the branch
// above never ran.
if (width >= 1920) {
expect(
cardWidth,
"the card never reached 64 rem",
).toBeGreaterThanOrEqual(64 * REM);
}
});
}
test("the frame is the same width on /labs, /measurements and /mood at every viewport", async ({
page,
}) => {
for (const width of WIDTHS) {
await page.setViewportSize({ width, height: 1000 });
const frameWidths: Record<string, number> = {};
for (const path of ["/labs", "/measurements", "/mood"]) {
await page.goto(path, { waitUntil: "domcontentloaded" });
const frame = page.locator('[data-slot="main-content-wrapper"]');
await expect(frame).toBeVisible();
frameWidths[path] = Math.round(
await frame.evaluate((node) => node.getBoundingClientRect().width),
);
}
const widths = Object.values(frameWidths);
expect(
new Set(widths).size,
`the frame differs between routes at ${width}px: ${JSON.stringify(frameWidths)}`,
).toBe(1);
}
});
});