Files
supabase/e2e/docs/global-elements/docs-global-elements.spec.ts
Miranda Limonczenko 98db26278b test(docs): scan global elements outside the article
The page scan stops at the article wrapper, so nothing checked the top nav,
sidebar nav, table-of-contents sidebar, breadcrumbs, or footer. A violation in
any of those lands on every docs page.

Adds a global-elements Playwright project that scans the document with the
article excluded, across six pages, one per layout, at two viewports, plus one
pass with the mobile menu open. It runs from a docs_components paths filter in
the existing workflow, so a content-only pull request never triggers it.

Rules that pass today are enforced, so they catch a newly introduced violation
without failing on existing debt. Everything else annotates.

Supersedes #48909, which predated e2e/shared.
2026-08-17 11:23:27 -07:00

153 lines
4.5 KiB
TypeScript

import { expect, test } from '@playwright/test'
import type { Page, TestInfo } from '@playwright/test'
import {
annotate,
attachScanReport,
blockingViolations,
dedupeViolations,
scanExcluding,
shouldEnforceAll,
unloadedResult,
} from '../../shared/a11y.ts'
import { formatViolations, settleForAxe, violationIds } from '../../shared/axe.ts'
import { elementsForViewport, renderedLocator, VIEWPORTS } from '../../shared/viewports.ts'
import type { ViewportName } from '../../shared/viewports.ts'
import {
GLOBAL_ELEMENTS_ENFORCED_RULES,
GLOBAL_ELEMENTS_EXCLUDED_RULES,
} from '../utils/axe-helpers.js'
import {
articleSelectorsOnPage,
GLOBAL_ELEMENT_PAGES,
GLOBAL_ELEMENTS,
MOBILE_MENU_SELECTOR,
type GlobalElement,
} from '../utils/docs-global-elements.js'
// One broken global element would otherwise report on every test that scans it.
const seenFindings = new Set<string>()
async function load(page: Page, testInfo: TestInfo, path: string, surface: string) {
let response
try {
response = await page.goto(path)
} catch (error) {
await attachScanReport(
testInfo,
unloadedResult(surface, path, null, 'document', GLOBAL_ELEMENTS_EXCLUDED_RULES)
)
throw error
}
const status = response?.status() ?? null
if (!response?.ok()) {
await attachScanReport(
testInfo,
unloadedResult(surface, page.url(), status, 'document', GLOBAL_ELEMENTS_EXCLUDED_RULES)
)
expect(response?.ok(), `Expected a successful response for ${path}, got ${status}`).toBeTruthy()
}
return status
}
async function scanAndAssert(
page: Page,
testInfo: TestInfo,
surface: string,
status: number | null
) {
const exclude = await articleSelectorsOnPage(page)
const result = await scanExcluding(page, {
surface,
exclude,
excludeRules: GLOBAL_ELEMENTS_EXCLUDED_RULES,
})
result.status = status
result.violations = dedupeViolations(result.violations, seenFindings)
await attachScanReport(testInfo, result)
const blocking = blockingViolations(result, GLOBAL_ELEMENTS_ENFORCED_RULES)
const reported = result.violations.filter((violation) => !blocking.includes(violation))
if (reported.length) {
annotate(
testInfo,
`${surface} global elements have ${reported.length} non-blocking accessibility finding(s): ` +
reported.map((violation) => `${violation.id} (${violation.nodes.length})`).join(', ')
)
}
const enforced = shouldEnforceAll()
? 'all WCAG 2.1 A/AA rules'
: GLOBAL_ELEMENTS_ENFORCED_RULES.join(', ')
expect(
violationIds(blocking).sort(),
`${surface} global elements have blocking a11y violations (${enforced}):\n${formatViolations(blocking)}`
).toEqual([])
}
function configsFor(elements: GlobalElement[], viewport: ViewportName) {
return elementsForViewport(
elements.map((name) => GLOBAL_ELEMENTS[name]),
viewport
)
}
test.describe('Docs global elements', () => {
for (const { path, layout, elements } of GLOBAL_ELEMENT_PAGES) {
for (const viewport of Object.keys(VIEWPORTS) as ViewportName[]) {
const surface = `${path} at ${viewport}`
test(`${surface} (${layout}) global elements have no blocking accessibility violations @global-elements`, async ({
page,
}, testInfo) => {
await page.setViewportSize(VIEWPORTS[viewport])
const status = await load(page, testInfo, path, surface)
await settleForAxe(page)
// Visible, not attached: axe skips hidden subtrees.
// Soft, so a missing element still reports its scan.
for (const { selector, label } of configsFor(elements, viewport)) {
await expect
.soft(
renderedLocator(page, selector),
`${surface} should render the ${label} (${selector})`
)
.toBeVisible()
}
await scanAndAssert(page, testInfo, surface, status)
})
}
}
// Same overlay on every page, and its markup only exists once opened.
test('the mobile menu overlay has no blocking accessibility violations @global-elements', async ({
page,
}, testInfo) => {
const path = '/docs'
const surface = '/docs with the mobile menu open'
await page.setViewportSize(VIEWPORTS.mobile)
const status = await load(page, testInfo, path, surface)
await renderedLocator(page, GLOBAL_ELEMENTS.menuTrigger.selector).click()
await expect(
page.locator(MOBILE_MENU_SELECTOR),
`${surface} should open the mobile menu overlay`
).toBeVisible()
await settleForAxe(page)
await scanAndAssert(page, testInfo, surface, status)
})
})