From 8dda0c3910cca10a9a4d39ffbf49c34a2ad4faa2 Mon Sep 17 00:00:00 2001 From: Miranda Limonczenko Date: Fri, 31 Jul 2026 12:31:39 -0700 Subject: [PATCH] Add heading-hierarchy a11y check to docs E2E tests (#48422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes DOCS-1232 ## Problem We do not have any tests to verify that we are following a proper heading hierarchy. For a documentation site that deals in mostly static content, this test is important. Single h1 + logical heading hierarchy (h1→h2→h3, no skips) matters because screen reader users navigate by jumping between headings — broken structure breaks that navigation. Relevant: WCAG 1.3.1 Info and Relationships (Level A) — https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html ## Solution Add Playwright axe-core, which we plan to expand later, to test only the h1 and header-hierarchy rule. This is added to our current suite that dynamically checks only pages that are edited. ## Manual testing 1. Find a docs guide and intentionally break the header hierarchy. 2. Run `pnpm e2e:docs:a11y` and see your errors. 3. Resolve the issue and run again to see errors resolved. Ensure there is at least a line changed to see the page tested. ## Summary by CodeRabbit * **Tests** * Added automated accessibility checks for documentation pages. * Verified heading order and the presence of a level-one heading on each page. * Added a dedicated command to run documentation accessibility tests. Co-authored-by: Claude Sonnet 5 --- e2e/docs/features/docs-pages.spec.ts | 23 +++++++++++++++++++---- e2e/docs/package.json | 2 ++ pnpm-lock.yaml | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/e2e/docs/features/docs-pages.spec.ts b/e2e/docs/features/docs-pages.spec.ts index 773ca40c980..3881a0b54d6 100644 --- a/e2e/docs/features/docs-pages.spec.ts +++ b/e2e/docs/features/docs-pages.spec.ts @@ -1,3 +1,4 @@ +import { AxeBuilder } from '@axe-core/playwright' import { expect, test } from '@playwright/test' import { @@ -39,10 +40,6 @@ test.describe('Docs owned pages', () => { const article = page.locator(articleSelector) await expect(article, 'Page article should be present').toBeVisible() - await expect( - article.getByRole('heading', { level: 1 }), - 'Page article should include an h1' - ).toBeVisible() const links = await collectDocsOwnedLinks(page, baseURL!, articleSelector) const userAgent = await browserLikeUserAgent(page) @@ -64,4 +61,22 @@ test.describe('Docs owned pages', () => { } }) } + + for (const pagePath of pagePaths) { + test(`${pagePath} has a valid heading hierarchy @a11y`, async ({ page }) => { + const articleSelector = articleSelectorForPagePath(pagePath) + const response = await page.goto(pagePath) + expect(response?.ok(), `Expected a successful response for ${pagePath}`).toBeTruthy() + + const axeResults = await new AxeBuilder({ page }) + .include(articleSelector) + .withRules(['heading-order', 'page-has-heading-one']) + .analyze() + + expect( + axeResults.violations, + `Heading hierarchy issues in ${articleSelector}:\n${JSON.stringify(axeResults.violations, null, 2)}` + ).toEqual([]) + }) + } }) diff --git a/e2e/docs/package.json b/e2e/docs/package.json index e54703326c0..b1e0371f232 100644 --- a/e2e/docs/package.json +++ b/e2e/docs/package.json @@ -6,6 +6,7 @@ "scripts": { "e2e:docs": "node --experimental-strip-types scripts/run-e2e-docs.ts", "e2e:docs:all": "node --experimental-strip-types scripts/run-e2e-docs.ts --all", + "e2e:docs:a11y": "node --experimental-strip-types scripts/run-e2e-docs.ts --grep @a11y", "e2e:ui": "node --experimental-strip-types scripts/run-e2e-docs.ts --ui", "e2e:docs:local-smoke": "playwright test --config=playwright.local-smoke.config.ts", "resolve-docs-scope": "node --experimental-strip-types scripts/resolve-docs-scope.ts" @@ -14,6 +15,7 @@ "@playwright/test": "^1.59.1" }, "devDependencies": { + "@axe-core/playwright": "^4.12.1", "@types/node": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99d33c59376..55dcc827d87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2021,6 +2021,9 @@ importers: specifier: ^1.59.1 version: 1.59.1 devDependencies: + '@axe-core/playwright': + specifier: ^4.12.1 + version: 4.12.1(playwright-core@1.59.1) '@types/node': specifier: 'catalog:' version: 22.13.14 @@ -3133,6 +3136,11 @@ packages: resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -9549,6 +9557,10 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + axios@1.18.1: resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} @@ -18498,6 +18510,11 @@ snapshots: '@aws/lambda-invoke-store@0.2.4': {} + '@axe-core/playwright@4.12.1(playwright-core@1.59.1)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.59.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -25765,6 +25782,8 @@ snapshots: axe-core@4.10.3: {} + axe-core@4.12.1: {} + axios@1.18.1(supports-color@8.1.1): dependencies: follow-redirects: 1.16.0