Files
supabase/e2e/docs/features/docs-pages.spec.ts
Miranda LimonczenkoandClaude Sonnet 5 8dda0c3910 Add heading-hierarchy a11y check to docs E2E tests (#48422)
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.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 12:31:39 -07:00

83 lines
3.0 KiB
TypeScript

import { AxeBuilder } from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
import {
articleSelectorForPagePath,
browserLikeUserAgent,
collectDocsOwnedLinks,
parseDocsE2EPagePaths,
} from '../utils/docs-links.js'
const pagePaths = parseDocsE2EPagePaths(process.env.DOCS_E2E_PAGE_PATHS)
test.describe('Docs owned pages', () => {
// playwright.config.ts sets fullyParallel: false, and Playwright shards
// work by file rather than by test in that mode — without this, every test
// in this single spec file runs on one worker no matter what --workers is
// passed. Opt this describe block into parallel scheduling explicitly.
test.describe.configure({ mode: 'parallel' })
test('resolved page list must not be empty', () => {
expect(
pagePaths.length,
'No pages to test. `pnpm e2e:docs` resolves pages from git changes by default, ' +
'or set DOCS_E2E_PAGE_PATHS explicitly.'
).toBeGreaterThan(0)
})
for (const pagePath of pagePaths) {
test(`${pagePath} loads and docs-owned article links resolve`, async ({ page }, testInfo) => {
const baseURL = testInfo.project.use.baseURL
expect(baseURL, 'A Playwright base URL should be configured').toBeTruthy()
const articleSelector = articleSelectorForPagePath(pagePath)
const response = await page.goto(pagePath)
expect(response, `Expected a response for ${pagePath}`).not.toBeNull()
expect(
response!.ok(),
`Page should return a successful status, got ${response!.status()}`
).toBeTruthy()
const article = page.locator(articleSelector)
await expect(article, 'Page article should be present').toBeVisible()
const links = await collectDocsOwnedLinks(page, baseURL!, articleSelector)
const userAgent = await browserLikeUserAgent(page)
for (const url of links) {
try {
const linkResponse = await page.request.get(url, { headers: { 'user-agent': userAgent } })
expect
.soft(linkResponse.ok(), `${url} should resolve (status ${linkResponse.status()})`)
.toBeTruthy()
} catch (error) {
expect
.soft(
null,
`${url} should be reachable (${error instanceof Error ? error.message : error})`
)
.toBeTruthy()
}
}
})
}
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([])
})
}
})