From cee6c706383ad68b4d594a98c6152ee81ca13ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 9 Sep 2026 23:02:46 +0200 Subject: [PATCH] test(a11y): gate the metric scans on the chart's own slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two metric sub-page cases waited for `.recharts-wrapper`, a class the charting library writes and every chart on the page carries, so the gate was satisfied by whichever chart rendered first rather than by the one the route exists to show. On `/insights/mood` that is exactly what happened: the mock carried a single mood entry on a fixed date, the chart withholds its line below three distinct days, and the sparse placeholder painted while some other chart answered the gate. The mood line was never waited for and may never have been scanned. Both charts now carry `data-slot="chart-plot"` on the wrapper their data branch renders — an empty window still paints the empty state and the scan still waits — and the mood fixture is anchored on now with ten daily scores, the same way the measurement rows above it are. --- e2e/a11y.spec.ts | 40 +++++++++++++++++++------- src/components/charts/health-chart.tsx | 5 ++++ src/components/charts/mood-chart.tsx | 4 +++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index cbdf84d14..febb55d52 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -75,6 +75,20 @@ const fixedMeasurementRows = Array.from({ length: 10 }, (_, index) => ({ notes: null, })); +// Ten daily mood scores ending yesterday, anchored for the same reason the +// measurement rows above are: the mood chart windows by calendar day and +// withholds its line below three DISTINCT days, so a fixed date left the mood +// sub-page painting its sparse placeholder. The scan's painted gate was then +// satisfied by a different chart on the page, and the mood line — the reason +// the case exists — was never waited for and might never have been scanned. +const fixedMoodEntries = Array.from({ length: 10 }, (_, index) => ({ + date: new Date(Date.now() - (index + 1) * 86_400_000) + .toISOString() + .slice(0, 10), + score: 3 + (index % 3) * 0.5, + samples: 1, +})); + const workoutListEntry = { id: A11Y_WORKOUT_ID, sportType: "running", @@ -330,8 +344,8 @@ async function installA11yMocks(page: Page) { await page.route("**/api/mood/analytics", (route) => fulfilJson(route, { - entries: [{ date: "2026-07-20", score: 4 }], - summary: { count: 1 }, + entries: fixedMoodEntries, + summary: { count: fixedMoodEntries.length }, }), ); @@ -342,11 +356,8 @@ async function installA11yMocks(page: Page) { // present-but-empty; the correlations block carries every metric key. await page.route("**/api/mood/insights", (route) => fulfilJson(route, { - summary: { totalEntries: 1, inTargetPct: null }, - heatmap: { - windowDays: 30, - cells: [{ date: "2026-07-20", score: 4, samples: 1 }], - }, + summary: { totalEntries: fixedMoodEntries.length, inTargetPct: null }, + heatmap: { windowDays: 30, cells: fixedMoodEntries }, distribution: [], weekday: [], timeOfDay: { buckets: [], reliable: false, best: null, worst: null }, @@ -712,18 +723,25 @@ const INSIGHTS_ROUTES: readonly RouteCase[] = [ }), }, { + // `.recharts-wrapper` is a class the charting library writes, not a + // contract this repository keeps, and it is shared by every chart on the + // page — so the gate could be satisfied by a chart other than the one the + // route exists to render, and it moves whenever the library's markup does. + // `chart-plot` is the measurement chart's own slot and the data branch is + // the only branch that renders it: an empty window paints `ChartEmptyState` + // instead and this scan still waits, which is what it is for. name: "/insights/weight metric subpage", path: "/insights/weight", - painted: (page) => page.locator(".recharts-wrapper").first(), + painted: (page) => page.locator('[data-slot="chart-plot"]').first(), }, { // Mood is event-driven, so its sub-page renders the heatmap and the mood // line chart from `/api/mood/insights` rather than a MeasurementType series. - // Gate on the line chart's recharts wrapper — the same painted signal the - // weight sub-page uses — so the scan waits for the heavy content to land. + // The mood chart carries the same `chart-plot` slot on the same terms, so + // the scan waits for the heavy content to land here too. name: "/insights/mood metric subpage", path: "/insights/mood", - painted: (page) => page.locator(".recharts-wrapper").first(), + painted: (page) => page.locator('[data-slot="chart-plot"]').first(), }, { // The two states this page's OWN render produces: the list, or the diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx index 675dd643c..6948494cf 100644 --- a/src/components/charts/health-chart.tsx +++ b/src/components/charts/health-chart.tsx @@ -1945,6 +1945,11 @@ export function HealthChart({ ) : null}