mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import type { Locator, Page, TestInfo } from "@playwright/test";
|
|
|
|
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
|
|
import { DAY_DOORS_ANALYTE } from "./setup/day-doors-fixture";
|
|
import { mockDay } from "./setup/day-mock";
|
|
import {
|
|
DAY_DOORS_STORAGE_STATE_PATH,
|
|
MOBILE_ROUTES_STORAGE_STATE_PATH,
|
|
} from "./setup/global-setup";
|
|
import { expect, test } from "./setup/test";
|
|
|
|
/**
|
|
* v1.42 — every chart drawn in days opens its days, the same way.
|
|
*
|
|
* The day view first reached six metric pages; the recovery page beside them
|
|
* offered no day at all. These journeys walk the surfaces that came after,
|
|
* each through the same doors: on a fine pointer a click on a point (or a
|
|
* calendar cell) opens the day beside the page; on a touch screen the first
|
|
* tap shows the value and the tooltip's "View the whole day" opens it as a
|
|
* bottom sheet, the tapped value on top. A date in a list opens in one tap.
|
|
* The dashed line (or outline) marks the open day, the row of dots and the
|
|
* caption sit under every chart, and the data table's dates are the
|
|
* keyboard way to the same days.
|
|
*
|
|
* - recovery (the page the beta reported), sleep, a lab marker, a chart
|
|
* with its comparison period on and a dashboard chart: line charts;
|
|
* - the mood calendar: a heatmap;
|
|
* - a metric's "all values" list: a date in a row.
|
|
*
|
|
* The day itself is a route mock in the contract's shape, as in
|
|
* `day-view.spec.ts`: these journeys pin the client's doors, not the day.
|
|
* Stable data attributes only.
|
|
*/
|
|
|
|
const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
|
|
const isPhone = (testInfo: TestInfo) =>
|
|
testInfo.project.name === "chromium-mobile";
|
|
|
|
/** Open the day of the point at 80 % across a day-linked plot. */
|
|
async function openFromPlot(page: Page, plot: Locator, testInfo: TestInfo) {
|
|
await plot.scrollIntoViewIfNeeded();
|
|
await expect(plot).toHaveAttribute("data-day-links", "true");
|
|
const box = (await plot.boundingBox())!;
|
|
const x = box.x + box.width * 0.8;
|
|
const y = box.y + box.height / 2;
|
|
if (isPhone(testInfo)) {
|
|
await page.touchscreen.tap(x, y);
|
|
const open = page.locator('[data-slot="chart-tooltip-open-day"]:visible');
|
|
await expect(open).toBeVisible();
|
|
// A tap alone never opens a sheet: a point is a small target.
|
|
await expect(panel(page)).toHaveCount(0);
|
|
// The button is a calm, full 44 px row. The tooltip re-renders while the
|
|
// tap settles, so the measurement retries instead of reading a box from
|
|
// a node that has just been replaced.
|
|
await expect
|
|
.poll(async () => (await open.boundingBox())?.height ?? 0)
|
|
.toBeGreaterThanOrEqual(44);
|
|
await open.tap();
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
|
|
} else {
|
|
await page.mouse.move(x, y);
|
|
await page.mouse.click(x, y);
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
|
|
}
|
|
await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
|
|
await expect(page.locator('[data-slot="day-view"]')).toBeVisible();
|
|
}
|
|
|
|
test.describe("every chart drawn in days opens its days", () => {
|
|
test.use({ storageState: DAY_DOORS_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, { index: "full" });
|
|
});
|
|
|
|
test("recovery: a point opens its day, and the open day is marked", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
if (!isPhone(testInfo)) {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
}
|
|
await page.goto("/insights/recovery");
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await expect(plot).toBeVisible();
|
|
// Under the chart: the row of day dots, which draws the
|
|
// days the index names, one of them ringed as notable.
|
|
const rug = page.locator('[data-slot="day-rug"]').first();
|
|
await expect(rug.locator("[data-day]").first()).toBeAttached();
|
|
await expect(rug.locator('[data-notable="true"]')).toHaveCount(1);
|
|
|
|
await openFromPlot(page, plot, testInfo);
|
|
const day = await page
|
|
.locator('[data-slot="day-view"]')
|
|
.getAttribute("data-day");
|
|
// The value the person came from heads the day.
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
// The open day's dot is the filled one.
|
|
await expect(rug.locator(`[data-day="${day}"]`)).toHaveAttribute(
|
|
"data-open",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
test("recovery: the data table's dates are the keyboard way to a day", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(isPhone(testInfo), "keyboard path");
|
|
await page.goto("/insights/recovery");
|
|
const toggle = page
|
|
.locator('[data-slot="chart-data-table-toggle"]')
|
|
.first();
|
|
await toggle.scrollIntoViewIfNeeded();
|
|
await toggle.focus();
|
|
await page.keyboard.press("Enter");
|
|
const link = page
|
|
.locator('[data-slot="chart-data-table-region"] [data-slot="day-link"]')
|
|
.first();
|
|
await link.focus();
|
|
const day = await link.getAttribute("data-day");
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day!,
|
|
);
|
|
// Escape closes the day and gives the keyboard back to the date.
|
|
await page.keyboard.press("Escape");
|
|
await expect(panel(page)).toHaveCount(0);
|
|
});
|
|
|
|
test("sleep: a night's point opens its day", async ({ page }, testInfo) => {
|
|
await page.goto("/insights/sleep");
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await expect(plot).toBeVisible();
|
|
await openFromPlot(page, plot, testInfo);
|
|
});
|
|
|
|
test("the mood calendar: a logged day's cell opens it", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.goto("/insights/mood");
|
|
// A logged, openable cell: it carries the pointer cursor.
|
|
const cell = page.locator("rect.cursor-pointer[data-day]").last();
|
|
await cell.scrollIntoViewIfNeeded();
|
|
await expect(cell).toBeVisible();
|
|
const day = await cell.getAttribute("data-day");
|
|
if (isPhone(testInfo)) {
|
|
await cell.tap();
|
|
const open = page.locator(
|
|
'[data-slot="heatmap-tooltip"] [data-slot="chart-tooltip-open-day"]',
|
|
);
|
|
await expect(open).toBeVisible();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await open.tap();
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
|
|
} else {
|
|
await cell.click();
|
|
}
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day!,
|
|
);
|
|
// The open day's cell is outlined.
|
|
await expect(page.locator('[data-slot="heatmap-open-day"]')).toHaveCount(1);
|
|
});
|
|
|
|
test("a lab marker's reading opens the day it was taken", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.goto("/labs");
|
|
await page
|
|
.locator('[data-slot="lab-list-analyte"]')
|
|
.filter({ hasText: DAY_DOORS_ANALYTE })
|
|
.first()
|
|
.click();
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await expect(plot).toBeVisible();
|
|
await openFromPlot(page, plot, testInfo);
|
|
// The reading on top, with the one before it beside it.
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
});
|
|
|
|
test("a chart with its comparison period on still opens its days", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.goto("/insights/weight");
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await expect(plot).toBeVisible();
|
|
await page
|
|
.locator('[data-slot="chart-overlay-controls-trigger"]')
|
|
.first()
|
|
.click();
|
|
await page
|
|
.locator('[data-slot="chart-overlay-comparison-lastMonth"]')
|
|
.click();
|
|
await page.keyboard.press("Escape");
|
|
// The overlay menu is closed before the chart is touched, so the tap
|
|
// lands on the chart and not on the menu's outside-dismiss.
|
|
await expect(
|
|
page.locator('[data-slot="chart-overlay-controls-content"]'),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.locator('[data-slot="chart-compare-caption"]').first(),
|
|
).toBeAttached();
|
|
await openFromPlot(page, plot, testInfo);
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("the dashboard and the list of all values", () => {
|
|
test.use({ storageState: DAY_DOORS_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, { index: "full" });
|
|
});
|
|
|
|
test("a dashboard chart opens its day; the today hero offers none", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
if (!isPhone(testInfo)) {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
}
|
|
await page.goto("/");
|
|
await expect(
|
|
page.locator(
|
|
'[data-slot^="today-hero"] [data-slot="day-link"], [data-slot^="today-hero"] [data-day-links="true"]',
|
|
),
|
|
).toHaveCount(0);
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await expect(plot).toBeVisible({ timeout: 20_000 });
|
|
await openFromPlot(page, plot, testInfo);
|
|
// Closing leaves the dashboard on today: a sheet from its close button,
|
|
// the docked day from its strip, the only control it has.
|
|
await page
|
|
.locator(
|
|
isPhone(testInfo)
|
|
? '[data-slot="day-close"]'
|
|
: '[data-slot="day-strip-toggle"]',
|
|
)
|
|
.click();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await expect(page).toHaveURL(/\/$/);
|
|
});
|
|
|
|
test("a date in a metric's list of all values opens its day", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await page.goto("/insights/values/WEIGHT");
|
|
const link = page.locator('[data-slot="day-link"]:visible').first();
|
|
await expect(link).toBeVisible();
|
|
const day = await link.getAttribute("data-day");
|
|
if (isPhone(testInfo)) await link.tap();
|
|
else await link.click();
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day!,
|
|
);
|
|
});
|
|
|
|
test("the docked day, open or collapsed, moves the Coach button off it", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(isPhone(testInfo), "docked from 1280 px");
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
// The launcher follows the `coach` capability, which needs a provider
|
|
// the suite cannot reach, so the capability reads available here; this
|
|
// journey is about where the button sits, not what it opens.
|
|
await serveAiBlock(page, aiBlockAvailable());
|
|
await page.goto("/insights/weight");
|
|
const fab = page.locator('[data-slot="coach-fab"]');
|
|
await expect(fab).toBeVisible();
|
|
const plot = page
|
|
.locator('[data-slot="chart-plot"][data-day-links="true"]')
|
|
.first();
|
|
await openFromPlot(page, plot, testInfo);
|
|
const panelBox = (await panel(page).boundingBox())!;
|
|
// Wait out the 150 ms move, then the launcher sits left of the column.
|
|
await expect
|
|
.poll(async () => {
|
|
const b = (await fab.boundingBox())!;
|
|
return b.x + b.width;
|
|
})
|
|
.toBeLessThanOrEqual(panelBox.x);
|
|
|
|
// Folded, the day keeps its strip; the launcher stays clear of it.
|
|
await page.locator('[data-slot="day-strip-toggle"]').click();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
const edge = (await page.locator('[data-slot="day-strip"]').boundingBox())!;
|
|
await expect
|
|
.poll(async () => {
|
|
const b = (await fab.boundingBox())!;
|
|
return b.x + b.width;
|
|
})
|
|
.toBeLessThanOrEqual(edge.x);
|
|
});
|
|
});
|
|
|
|
test.describe("the readiness link into a medication's edit form", () => {
|
|
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
|
|
|
|
test("?edit=1 opens the medication's edit form and leaves the URL", async ({
|
|
page,
|
|
}) => {
|
|
const res = await page.request.get("/api/medications");
|
|
const body = (await res.json()) as {
|
|
data: Array<{ id: string }> | { medications?: Array<{ id: string }> };
|
|
};
|
|
const list = Array.isArray(body.data)
|
|
? body.data
|
|
: (body.data.medications ?? []);
|
|
expect(list.length).toBeGreaterThan(0);
|
|
await page.goto(`/medications/${list[0]!.id}?edit=1`);
|
|
await expect(page.getByRole("dialog")).toBeVisible();
|
|
// A reload after closing the form must not open it again.
|
|
await expect(page).not.toHaveURL(/[?&]edit=1/);
|
|
});
|
|
});
|