mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Two specs have been red on main since the v1.4.20 hero rewrite, spamming
the GitHub Actions failure mailer on every push:
1. chart-overlay-controls.spec.ts — the test opens the dashboard's chart
overlay popover and toggles 'target range' on. The Radix dropdown
content portals to the document body, so on the 393×851 Pixel 5 and
near the bottom of the 1280×720 desktop viewport the toggle's bbox
lands below the fold. `click({force:true})` still asserts the bbox
intersects the viewport and refuses to fire, and
`scrollIntoViewIfNeeded` on the portalled content is a no-op because
the portal sits outside the page scroll container. Switching to
`dispatchEvent('click')` synthesises a click directly on the element
without any actionability or viewport check; Radix Switch's native
click handler fires `onCheckedChange` and the PUT lands as expected.
2. charts-mobile.spec.ts — the 30-day-window tick-count assertion times
out waiting for `.recharts-xAxis .recharts-cartesian-axis-tick text`
to be visible. Recharts does paint the <text> ticks into the SVG, but
the chart card on Pixel-5 is taller than the 851-px viewport, so the
bottom-anchored x-axis labels stay below the fold even after
`scrollIntoViewIfNeeded` brings the card's top into view. The
downstream `evaluate` only needs the ticks attached to the DOM (it
counts them via querySelectorAll), so `state: 'attached'` is both
sufficient and viewport-tolerant.
Neither fix changes the assertion semantics — both still exercise the
same user-visible behaviour against the same selectors. The CI mailer
should fall silent after the next push.
140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
|
||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||
|
||
/**
|
||
* v1.4.18 — per-chart overlay-controls popover.
|
||
*
|
||
* Marc reverted v1.4.16's always-on chart overlays (gradient fill,
|
||
* personal-baseline line, target-zone shading) and asked for per-chart
|
||
* switches that the user can flip on or off. This spec exercises the
|
||
* end-to-end behaviour against the seeded test user:
|
||
*
|
||
* 1. Dashboard renders with a chart.
|
||
* 2. The chart card has a settings cog in its header.
|
||
* 3. Clicking the cog opens a popover with three switches.
|
||
* 4. Toggling "Target range" on triggers a PUT /api/dashboard/chart-overlay-prefs.
|
||
*/
|
||
test.describe("chart overlay controls", () => {
|
||
test.use({ storageState: STORAGE_STATE_PATH });
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
// Mock analytics + measurements so the dashboard paints at least
|
||
// one chart card. Same seed shape the dashboard.spec.ts uses.
|
||
await page.route("**/api/analytics", (route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: {
|
||
summaries: {
|
||
WEIGHT: {
|
||
latest: 78.5,
|
||
avg7: 78.2,
|
||
avg30: 77.9,
|
||
slope30: { slope: -0.05, direction: "down" },
|
||
count: 30,
|
||
},
|
||
},
|
||
bpInTargetPct: 0,
|
||
glucoseByContext: {},
|
||
},
|
||
error: null,
|
||
}),
|
||
}),
|
||
);
|
||
await page.route("**/api/mood/analytics", (route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: { entries: [], summary: { count: 0 } },
|
||
error: null,
|
||
}),
|
||
}),
|
||
);
|
||
await page.route("**/api/measurements*", (route) => {
|
||
const measurements = Array.from({ length: 10 }, (_, i) => ({
|
||
id: `m_${i}`,
|
||
type: "WEIGHT",
|
||
value: 78 + (i % 3) - 1,
|
||
measuredAt: new Date(Date.now() - i * 86_400_000).toISOString(),
|
||
notes: null,
|
||
}));
|
||
return route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: { measurements, meta: { total: measurements.length } },
|
||
error: null,
|
||
}),
|
||
});
|
||
});
|
||
});
|
||
|
||
test("opens the overlay-controls popover and saves a toggle change", async ({
|
||
page,
|
||
}) => {
|
||
let putRequestCount = 0;
|
||
await page.route("**/api/dashboard/chart-overlay-prefs", async (route) => {
|
||
if (route.request().method() === "PUT") {
|
||
putRequestCount += 1;
|
||
}
|
||
return route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({ data: { saved: true }, error: null }),
|
||
});
|
||
});
|
||
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
// Wait for the dashboard tile-strip to settle so we know the user
|
||
// is past initial auth + layout-fetch.
|
||
await expect(
|
||
page.locator('[data-slot="dashboard-tile-strip"]'),
|
||
).toBeVisible({ timeout: 10_000 });
|
||
|
||
// The weight chart is one of the always-visible default charts and
|
||
// it ships an overlay-controls trigger because the dashboard plumbs
|
||
// a chartKey into it.
|
||
const trigger = page
|
||
.locator('[data-slot="chart-overlay-controls-trigger"]')
|
||
.first();
|
||
await expect(trigger).toBeVisible({ timeout: 10_000 });
|
||
|
||
// Scroll the trigger into view before opening the dropdown so the
|
||
// Radix Popper places its portalled content inside the viewport.
|
||
// On the seeded test user the dashboard renders multiple stacked
|
||
// chart cards, and the first cog can sit near the bottom edge of
|
||
// the desktop 1280×720 viewport — opening the popover then drops
|
||
// the toggle below the fold and Playwright refuses to click an
|
||
// off-viewport element after auto-scrolling.
|
||
await trigger.scrollIntoViewIfNeeded();
|
||
await trigger.click();
|
||
|
||
// The popover content paints three switches.
|
||
const targetRangeToggle = page.locator(
|
||
'[data-slot="chart-overlay-toggle-target-range"]',
|
||
);
|
||
await expect(targetRangeToggle).toBeVisible({ timeout: 5_000 });
|
||
|
||
// Toggle target-range on. Radix Switch is a button[role=switch].
|
||
// The Radix dropdown content portals to the body root and can land
|
||
// below the viewport on a 393×851 Pixel 5 (or near the bottom edge
|
||
// on the 1280×720 desktop profile). Playwright's `click({force:true})`
|
||
// still computes the element's bbox against the viewport and refuses
|
||
// to dispatch the click when it falls outside, so neither
|
||
// `scrollIntoViewIfNeeded()` on the portalled content nor `force`
|
||
// can rescue the action. `dispatchEvent('click')` synthesises a DOM
|
||
// click on the element without any viewport / actionability check,
|
||
// which is exactly the semantic we want for an off-screen portalled
|
||
// Radix Switch — and it still fires the onCheckedChange handler that
|
||
// the surface listens for.
|
||
await targetRangeToggle.dispatchEvent("click");
|
||
|
||
// The PUT fires once for the toggle change.
|
||
await expect.poll(() => putRequestCount).toBeGreaterThan(0);
|
||
});
|
||
});
|