test(a11y): gate the metric scans on the chart's own slot

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.
This commit is contained in:
Marc-André Bombeck committed 2026-09-09 23:02:46 +02:00
1 parent 1d372df3a0
commit cee6c70638
3 files changed
+38 -11

No files matched your search

+29 -11
View File
@@ -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
+5
View File
@@ -1945,6 +1945,11 @@ export function HealthChart({
) : null}
<div
// Stable hook for the browser suite. `.recharts-wrapper` is a
// library-internal class and it is what the a11y scan used to
// gate on; this slot is rendered by the data branch only, so it
// says the same thing without depending on Recharts' markup.
data-slot="chart-plot"
className="relative z-10 h-full touch-pan-y"
role="img"
aria-label={chartAriaLabel}
+4
View File
@@ -767,6 +767,10 @@ export function MoodChart({
? "h-[var(--chart-height,140px)]"
: "h-[var(--chart-height,200px)] md:h-[var(--chart-height-md,220px)]"
} touch-pan-y`}
// Same stable hook as the measurement chart: rendered by the data
// branch only, so the browser suite can gate on it instead of on
// Recharts' own class names.
data-slot="chart-plot"
role="img"
aria-label={moodAriaLabel}
>