mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
test(timeline): pin the medication rows, the open day, the point card and the page height
The journey now writes two medications through the real routes (one with a start date and its course, one known only by an intake) and asserts each is drawn on exactly one row; carries an open day from /timeline to /labs and back after folding it; hovers and focuses a value point and checks the card names that line alone and stays inside the chart; and measures the page at 1280, 1440 and 1920 px, with and without the docked day: no sideways scroll, and no height past the content beyond the shell's own padding. Expanding all of a day's values scrolls the panel and leaves the page height as it was. The "Open <day>" link the bar no longer has is asserted absent; a chip opens the day instead. Label texts carry their lane under their own attribute, so a lane selector matches the lane and not its names.
This commit is contained in:
1 parent
b1a0598c9d
commit
b274ee51ad
2 files changed
+243
-14
No files matched your search
+241
-12
@@ -173,22 +173,21 @@ test("the timeline hands the selected day to the day view through ?day=", async
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page).toHaveURL(new RegExp(`[?&]day=${after}`));
|
||||
|
||||
// "Open <day>" in the selection bar does the same for the selected day.
|
||||
// An incoming ?day= selects that day; the selection bar has no separate
|
||||
// "Open <day>" link, a chip opens the day it touches.
|
||||
await page.goto("/timeline?day=2026-01-03");
|
||||
const open = page.locator('[data-slot="timeline-open-day"]');
|
||||
await expect(open).toHaveAttribute("data-date", "2026-01-03", {
|
||||
timeout: 20_000,
|
||||
});
|
||||
await expect(
|
||||
page.locator('[data-slot="timeline-selection"]'),
|
||||
).toHaveAttribute("data-date", "2026-01-03", { timeout: 20_000 });
|
||||
await expect(
|
||||
page.locator('[data-slot="timeline-zoom"] [data-value="quarter"]'),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
await expect(
|
||||
page.locator('[data-slot="timeline-selection-chip"][data-item="ill-5"]'),
|
||||
).toBeVisible();
|
||||
await page.goto("/timeline");
|
||||
await expect(chart).toBeVisible({ timeout: 20_000 });
|
||||
await page.locator('[data-slot="timeline-zoom"] [data-value="year"]').click();
|
||||
await open.click();
|
||||
await expect(page.locator('[data-slot="timeline-open-day"]')).toHaveCount(0);
|
||||
const chip = page.locator(
|
||||
'[data-slot="timeline-selection-chip"][data-item="ill-5"]',
|
||||
);
|
||||
await expect(chip).toBeVisible();
|
||||
await chip.click();
|
||||
await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
|
||||
});
|
||||
|
||||
@@ -546,3 +545,233 @@ test("on a phone the chronicle replaces the chart and opens a day per row", asyn
|
||||
.click();
|
||||
await expect(page).toHaveURL(/[?&]day=2026-01-03/);
|
||||
});
|
||||
|
||||
/** A calendar date `days` before today, in the account's zone. */
|
||||
function daysAgo(days: number): string {
|
||||
const at = new Date(`${TODAY}T12:00:00Z`);
|
||||
at.setUTCDate(at.getUTCDate() - days);
|
||||
return at.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
test("every medication still taken is drawn, from the account's real record", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setTimelineModuleOn(page);
|
||||
// Written through the app's own routes, once: a medication with a start
|
||||
// date (it opens a course), one without (its first intake is its start).
|
||||
const list = await page.request.get("/api/medications");
|
||||
const held = JSON.stringify(await list.json());
|
||||
const create = async (body: Record<string, unknown>) => {
|
||||
const res = await page.request.post("/api/medications", { data: body });
|
||||
expect(res.status()).toBeLessThan(300);
|
||||
return ((await res.json()) as { data: { id: string } }).data.id;
|
||||
};
|
||||
if (!held.includes("E2E Ramipril")) {
|
||||
await create({
|
||||
name: "E2E Ramipril",
|
||||
dose: "5 mg",
|
||||
startsOn: daysAgo(40),
|
||||
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
|
||||
});
|
||||
}
|
||||
if (!held.includes("E2E Vitamin D3")) {
|
||||
const id = await create({
|
||||
name: "E2E Vitamin D3",
|
||||
dose: "2000 IU",
|
||||
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
|
||||
});
|
||||
const intake = await page.request.post(`/api/medications/${id}/intake`, {
|
||||
data: { takenAt: new Date(Date.now() - 20 * 86_400_000).toISOString() },
|
||||
});
|
||||
expect(intake.status()).toBeLessThan(300);
|
||||
}
|
||||
|
||||
await page.goto("/timeline?zoom=quarter");
|
||||
const lane = page.locator('svg [data-lane="medications"]');
|
||||
await expect(lane).toHaveCount(1, { timeout: 20_000 });
|
||||
// One row each, and every one of them reaches today.
|
||||
for (const name of ["E2E Ramipril", "E2E Vitamin D3"]) {
|
||||
const bars = lane.locator("g[data-row]", {
|
||||
has: page.locator(`title:text-matches("^${name}")`),
|
||||
});
|
||||
await expect(bars.first()).toBeAttached();
|
||||
const rows = await bars.evaluateAll((els) =>
|
||||
els.map((el) => el.getAttribute("data-row")),
|
||||
);
|
||||
expect(new Set(rows).size).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("an open day stays open from the timeline to another page", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setTimelineModuleOn(page);
|
||||
await mockTimeline(page, "full", TODAY);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
const day = daysAgo(3);
|
||||
await page.goto(`/timeline?day=${day}`);
|
||||
const panel = page.locator('[data-slot="day-panel"]');
|
||||
await expect(panel).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
await page.locator('a[href="/labs"]').first().click();
|
||||
await expect(page).toHaveURL(new RegExp(`/labs\\?(.*&)?day=${day}`));
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
||||
"data-day",
|
||||
day,
|
||||
);
|
||||
|
||||
// Folded by the person, it stays folded on the next page.
|
||||
await page.locator('[data-slot="day-close"]').click();
|
||||
await expect(panel).toHaveCount(0);
|
||||
await page.locator('a[href="/timeline"]').first().click();
|
||||
await expect(page).toHaveURL(/\/timeline$/);
|
||||
await expect(page.locator('[data-slot="day-rail"]')).toBeVisible();
|
||||
await expect(panel).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a value point reads on its own, by pointer and by keyboard", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setTimelineModuleOn(page);
|
||||
await mockTimeline(page, "full", TODAY);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto("/timeline");
|
||||
const targets = page.locator('[data-slot="timeline-point-target"]');
|
||||
await expect(targets.first()).toBeAttached({ timeout: 20_000 });
|
||||
|
||||
const target = targets.last();
|
||||
const series = await target.getAttribute("data-series");
|
||||
const t = await target.getAttribute("data-t");
|
||||
await target.hover();
|
||||
const tip = page.locator('[data-slot="timeline-point-tip"]');
|
||||
await expect(tip).toBeVisible();
|
||||
await expect(tip).toHaveAttribute("data-series", series!);
|
||||
await expect(tip).toHaveAttribute("data-t", t!);
|
||||
// One line only, never the whole bucket.
|
||||
await expect(tip.locator('[data-slot="rich-chart-tooltip-row"]')).toHaveCount(
|
||||
1,
|
||||
);
|
||||
// Inside the chart, and the page does not scroll sideways for it.
|
||||
const chart = (await page
|
||||
.locator('[data-slot="timeline-chart"]')
|
||||
.boundingBox())!;
|
||||
const box = (await tip.boundingBox())!;
|
||||
expect(box.x).toBeGreaterThanOrEqual(chart.x - 1);
|
||||
expect(box.x + box.width).toBeLessThanOrEqual(chart.x + chart.width + 1);
|
||||
|
||||
// The keyboard: focus shows the card, the arrows walk the line.
|
||||
await page.mouse.move(0, 0);
|
||||
await target.focus();
|
||||
await expect(tip).toHaveAttribute("data-t", t!);
|
||||
await page.keyboard.press("ArrowLeft");
|
||||
await expect(tip).not.toHaveAttribute("data-t", t!);
|
||||
await expect(tip).toHaveAttribute("data-series", series!);
|
||||
});
|
||||
|
||||
test("the timeline page ends where its content does, at every desktop width", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setTimelineModuleOn(page);
|
||||
await mockTimeline(page, "full", TODAY);
|
||||
for (const width of [1280, 1440, 1920]) {
|
||||
for (const day of [null, daysAgo(3)]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(day ? `/timeline?day=${day}` : "/timeline");
|
||||
await expect(
|
||||
page.locator('[data-slot="timeline-chart"] > svg'),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
const m = await page.evaluate(() => {
|
||||
const main = document.getElementById("main-content")!;
|
||||
const page = document.querySelector('[data-slot="timeline-page"]')!;
|
||||
const doc = document.scrollingElement!;
|
||||
return {
|
||||
mainSw: main.scrollWidth,
|
||||
mainCw: main.clientWidth,
|
||||
docSw: doc.scrollWidth,
|
||||
docCw: doc.clientWidth,
|
||||
docSh: doc.scrollHeight,
|
||||
docCh: doc.clientHeight,
|
||||
mainSh: main.scrollHeight,
|
||||
contentBottom:
|
||||
page.getBoundingClientRect().bottom -
|
||||
main.getBoundingClientRect().top +
|
||||
main.scrollTop,
|
||||
};
|
||||
});
|
||||
const where = `${width}px ${day ? "with" : "without"} the day`;
|
||||
expect(m.mainSw, where).toBeLessThanOrEqual(m.mainCw + 1);
|
||||
expect(m.docSw, where).toBeLessThanOrEqual(m.docCw + 1);
|
||||
expect(m.docSh, where).toBeLessThanOrEqual(m.docCh + 1);
|
||||
// The shell's own buffer below the last card (pb-20 and the top
|
||||
// padding), nothing more.
|
||||
expect(m.mainSh, where).toBeLessThanOrEqual(m.contentBottom + 104 + 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("listing all of a day's values scrolls the panel, never the page", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setTimelineModuleOn(page);
|
||||
await mockTimeline(page, "full", TODAY);
|
||||
const day = daysAgo(3);
|
||||
const types = [
|
||||
"WEIGHT",
|
||||
"BODY_FAT",
|
||||
"PULSE",
|
||||
"RESTING_HEART_RATE",
|
||||
"HEART_RATE_VARIABILITY",
|
||||
"RESPIRATORY_RATE",
|
||||
"OXYGEN_SATURATION",
|
||||
"BODY_TEMPERATURE",
|
||||
"MUSCLE_MASS",
|
||||
"BONE_MASS",
|
||||
"FAT_MASS",
|
||||
"VISCERAL_FAT",
|
||||
"BLOOD_GLUCOSE",
|
||||
"ACTIVITY_STEPS",
|
||||
];
|
||||
await page.route(`**/api/day/${day}`, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
date: day,
|
||||
tz: "Europe/Berlin",
|
||||
counts: { values: types.length, entries: 0 },
|
||||
running: [],
|
||||
values: types.map((type, i) => ({
|
||||
type,
|
||||
value: 10 + i,
|
||||
unit: "u",
|
||||
at: `${day}T0${i % 10}:00:00.000Z`,
|
||||
source: "MANUAL",
|
||||
band: null,
|
||||
})),
|
||||
events: [],
|
||||
notable: [],
|
||||
scores: [],
|
||||
sections: {},
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.setViewportSize({ width: 1440, height: 800 });
|
||||
await page.goto(`/timeline?day=${day}`);
|
||||
const all = page.locator('[data-slot="day-values-all"]');
|
||||
await expect(all).toBeVisible({ timeout: 20_000 });
|
||||
const heights = () =>
|
||||
page.evaluate(() => ({
|
||||
doc: document.scrollingElement!.scrollHeight,
|
||||
main: document.getElementById("main-content")!.scrollHeight,
|
||||
}));
|
||||
const before = await heights();
|
||||
await all.click();
|
||||
await expect(all).toHaveAttribute("aria-expanded", "true");
|
||||
const after = await heights();
|
||||
expect(Math.abs(after.doc - before.doc)).toBeLessThanOrEqual(2);
|
||||
expect(Math.abs(after.main - before.main)).toBeLessThanOrEqual(2);
|
||||
});
|
||||
@@ -582,8 +582,8 @@ function ChartBody({
|
||||
lane.labels.map((label, i) => (
|
||||
<text
|
||||
key={`label-${lane.key}-${label.itemId}-${i}`}
|
||||
data-lane={lane.key}
|
||||
data-item={label.itemId}
|
||||
data-label-lane={lane.key}
|
||||
data-label-item={label.itemId}
|
||||
x={label.x}
|
||||
y={label.y}
|
||||
fontSize={11}
|
||||
|
||||
Reference in new issue
Block a user