Files
HealthLog/e2e/day-view.spec.ts
T
Marc-André Bombeck 8bcb3e6301 fix: keep the two-step on chart points on touch
A point and a calendar cell are small targets; one tap that opened the
day opened days by accident. On a touch screen the first tap shows the
value again and the tooltip's calm 44 px row opens the day, on every
day-linked chart and calendar alike. A date in a list still opens in one
tap; a click on a fine pointer still opens directly. The tooltip keeps
not gliding into place.
2026-10-08 16:04:54 +02:00

193 lines
7.5 KiB
TypeScript

import type { Page } from "@playwright/test";
import { mockDay } from "./setup/day-mock";
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";
/**
* v1.42 — the day view and its doors.
*
* The record is the phone-sweep account `globalSetup` seeds: a week of blood
* pressure, two lab readings, mood and an episode. The day itself
* (`GET /api/day/{date}`, `GET /api/day/index`) is answered by a route mock in
* the contract's shape, so the journeys pin the client: which door opens which
* day, what the history does, and where no door is.
*
* - a chart point opens the day beside the chart (desktop);
* - on a touch screen the first tap shows the value and the tooltip offers
* the day, the second opens it as a bottom sheet;
* - a date in a list opens the day while the row keeps its own target;
* - `?day=` opens the layer on any page, Back and Escape close it, a date in
* the future is dropped;
* - stepping to the neighbouring days never adds history entries;
* - the dashboard's today area offers no door; its charts below do.
*
* Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`,
* `chart-plot[data-day-links]`, `chart-tooltip-open-day`, `day-prev` /
* `day-next` / `day-close`.
*/
const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
function isoDaysAgo(days: number): string {
const d = new Date(Date.now() - days * 86_400_000);
return d.toISOString().slice(0, 10);
}
test.describe("the day view", () => {
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
test.beforeEach(async ({ page, context }, testInfo) => {
const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
await context.addCookies([
{ name: "healthlog-locale", value: "en", url: baseURL },
]);
await mockDay(page);
});
test("a chart point opens its day beside the chart; Back closes it", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "desktop click");
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/insights/blood-pressure");
const plot = page.locator(
'[data-slot="chart-plot"][data-day-links="true"]',
);
await expect(plot).toBeVisible();
// The plot's right half holds the most recent points.
const box = (await plot.boundingBox())!;
await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);
await page.mouse.click(box.x + box.width * 0.8, box.y + box.height / 2);
await expect(panel(page)).toBeVisible();
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
// A landmark beside the page, named by its date.
await expect(panel(page)).toHaveJSProperty("tagName", "ASIDE");
await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
// The chart beside it stays usable: the panel is not modal.
await expect(page.locator('[data-slot="sheet-overlay"]')).toHaveCount(0);
await page.goBack();
await expect(panel(page)).toHaveCount(0);
await expect(page).not.toHaveURL(/[?&]day=/);
});
test("a date in a list opens the day; the row keeps its own target", async ({
page,
}) => {
await page.goto("/labs");
await page
.locator('[data-slot="lab-list-analyte"]')
.filter({ hasText: MOBILE_ROUTES_ANALYTE })
.first()
.click();
// The marker's readings, each dated.
await page.locator('a[href$="/values"]').first().click();
const link = page.locator('[data-slot="day-link"]:visible').first();
await expect(link).toBeVisible();
const day = await link.getAttribute("data-day");
const before = page.url();
await link.click();
await expect(panel(page)).toBeVisible();
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
"data-day",
day!,
);
// Same page underneath: the click opened the day, not the reading.
expect(new URL(page.url()).pathname).toBe(new URL(before).pathname);
// The reading the person came from sits on top, with the one before it.
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
});
test("?day= opens the layer on any page; Escape closes it", async ({
page,
}) => {
const day = isoDaysAgo(3);
await page.goto(`/mood?day=${day}`);
await expect(panel(page)).toBeVisible();
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
"data-day",
day,
);
await page.keyboard.press("Escape");
await expect(panel(page)).toHaveCount(0);
await expect(page).toHaveURL(/\/mood$/);
});
test("a future or malformed ?day= is dropped without a word", async ({
page,
}) => {
await page.goto("/mood?day=2999-01-01");
await expect(page).toHaveURL(/\/mood$/);
await expect(panel(page)).toHaveCount(0);
await page.goto("/mood?day=2026-02-30");
await expect(page).toHaveURL(/\/mood$/);
await expect(panel(page)).toHaveCount(0);
});
test("stepping between days never adds a history entry", async ({ page }) => {
await page.goto("/mood");
await page.locator('[data-slot="day-link"]:visible').first().click();
await expect(panel(page)).toBeVisible();
const start = await page.evaluate(() => window.history.length);
const first = await page
.locator('[data-slot="day-view"]')
.getAttribute("data-day");
for (let i = 0; i < 3; i += 1) {
await page.locator('[data-slot="day-prev"]').click();
}
await expect(page.locator('[data-slot="day-view"]')).not.toHaveAttribute(
"data-day",
first!,
);
expect(await page.evaluate(() => window.history.length)).toBe(start);
// One Back leaves the layer, however far it stepped.
await page.goBack();
await expect(panel(page)).toHaveCount(0);
await expect(page).toHaveURL(/\/mood$/);
});
test("on a phone the first tap shows the value, the second opens the day", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile", "touch only");
await page.goto("/insights/blood-pressure");
const plot = page.locator(
'[data-slot="chart-plot"][data-day-links="true"]',
);
await plot.scrollIntoViewIfNeeded();
await expect(plot).toBeVisible();
const box = (await plot.boundingBox())!;
await page.touchscreen.tap(box.x + box.width * 0.8, box.y + box.height / 2);
const open = page.locator('[data-slot="chart-tooltip-open-day"]');
await expect(open).toBeVisible();
// A tap alone never opens a sheet.
await expect(panel(page)).toHaveCount(0);
await open.tap();
await expect(panel(page)).toBeVisible();
await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
// The tapped value heads the day.
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
});
test("the dashboard's today area offers no day door", async ({ page }) => {
await page.goto("/");
await expect(
page.locator('[data-slot="main-content-wrapper"]'),
).toBeVisible();
// The hero is today: nothing in it opens another day. (The charts
// further down do; `day-doors.spec.ts` walks one.)
await expect(
page.locator('[data-slot^="today-hero"]').first(),
).toBeVisible();
await expect(
page.locator(
'[data-slot^="today-hero"] [data-slot="day-link"], [data-slot^="today-hero"] [data-day-links="true"]',
),
).toHaveCount(0);
});
});