From 33fc2383059ba8be5a4d280083c07e5588f771fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 2 Aug 2026 19:45:05 +0200 Subject: [PATCH] Scan the dashboard once its charts are in the accessibility tree An unrevealed chart cell is `invisible` and `aria-hidden`, so the chart row is not in the accessibility tree at all until the reveal gate fires. Waiting on the tile strip left it to chance whether the row was read: locally the cells were still hidden when the strip appeared and revealed six seconds later, while the runner that reported the broken heading order had already revealed them. The row is the largest surface on the page, so a scan that sometimes skips it is not a check of the page. Gating on a revealed cell makes the coverage the same on every run. With the heading level put back to its old value the case now fails 16 out of 16 repeats across both themes and both viewports, where the same defect previously surfaced twice in several full runs. --- e2e/a11y.spec.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index dc6ece761..090f940d2 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -778,13 +778,23 @@ const ADMIN_AND_BASELINE_ROUTES: readonly RouteCase[] = [ // is `app/loading.tsx` — a silhouette with no heading in it. The scan was // therefore free to run against a document that legitimately had no `

` // yet and report the app as missing one. Measured under an 8× CPU - // throttle: `#main-content` at 759 ms, the tile strip at 4896 ms. The - // strip is the dashboard's own content and has a separate - // `dashboard-tile-strip-skeleton` marker for its loading phase, so the two - // states cannot be confused for one another. + // throttle: `#main-content` at 759 ms, the tile strip at 4896 ms. + // + // The gate is the revealed chart cell rather than the tile strip because + // an unrevealed cell is `invisible` AND `aria-hidden` — the chart cards + // are simply not in the accessibility tree, so axe walks past the whole + // chart row without reading it. Gating on the strip alone left it a + // coin flip whether the row was scanned at all: locally the cells were + // still hidden at strip time and revealed six seconds later, while the + // runner that reported the heading-order break had already revealed + // them. The row is the dashboard's largest surface; the scan has to see + // it every time or not claim to have checked the page. name: "/ dashboard", path: "/", - painted: (page) => page.locator('[data-slot="dashboard-tile-strip"]'), + painted: (page) => + page + .locator('[data-slot="dashboard-chart-cell"][data-revealed="true"]') + .first(), }, { // The settings heading belongs to ``, i.e. the chrome —