Files
supabase/e2e/docs/utils/axe-helpers.ts
Anthony Lio 085d218bff fix(docs): button a11y enhancements (#49952)
## What kind of change does this PR introduce?

bug fix for accessibility, fixes
[docs-1265](https://linear.app/supabase/issue/DOCS-1365/add-aria-label-to-icon-only-buttons-button-name)

## What is the current behavior?

some controles in the docs app have no accessible name, so screen
readers announce them as an unlabelled "button"

## What is the new behavior?

- adds each control a name the way its neighbours already do
- adds menu toggle a state-dependent `aria-label` and `aria-expanded`
- adds `button-name` to `enforced_rules`

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

- **Accessibility**
- Added descriptive labels to configuration selectors and mobile menu
controls.
- Mobile menu controls now announce whether the menu is open or closed.
- Expanded accessibility validation to check button names across
documentation pages.

- **UI Improvements**
- Updated the raw throughput table control with a clearer button
appearance and expandable chevron indicator.
  - Replaced a schema-migration tooltip with a direct in-page link.

- **Documentation**
- Removed the Info Tooltip component guidance from the contributing
documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-04 10:32:57 +03:00

104 lines
2.5 KiB
TypeScript

import type { Page, TestInfo } from '@playwright/test'
import type { Result } from 'axe-core'
import { scan } from '../../shared/axe.ts'
export const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']
export const ENFORCED_RULES = ['heading-order', 'page-has-heading-one', 'button-name']
export const EXCLUDED_RULES = [
'color-contrast',
'html-has-lang',
'html-lang-valid',
'html-xml-lang-mismatch',
'document-title',
'aria-hidden-body',
'meta-viewport',
'meta-refresh',
'css-orientation-lock',
]
export interface A11yScanResult {
surface: string
url: string
include: string
excludedRules: string[]
loaded: boolean
status: number | null
elementCount: number
violations: Result[]
}
export function shouldEnforceAll(): boolean {
return !!process.env.A11Y_ENFORCE_ALL
}
export async function scanArticle(
page: Page,
surface: string,
include: string
): Promise<A11yScanResult> {
const reported = await scan(page, { tags: WCAG_TAGS, excludeRules: EXCLUDED_RULES, include })
const enforced = await scan(page, { rules: ENFORCED_RULES })
const byRule = new Map([...reported, ...enforced].map((violation) => [violation.id, violation]))
const elementCount = await page.evaluate(
(selector) => document.querySelector(selector)?.querySelectorAll('*').length ?? 0,
include
)
return {
surface,
url: page.url(),
include,
excludedRules: EXCLUDED_RULES,
loaded: true,
status: null,
elementCount,
violations: [...byRule.values()],
}
}
export function unloadedResult(
surface: string,
url: string,
status: number | null,
include: string
): A11yScanResult {
return {
surface,
url,
include,
excludedRules: EXCLUDED_RULES,
loaded: false,
status,
elementCount: 0,
violations: [],
}
}
export const MIN_MEANINGFUL_ELEMENTS = 20
export function scanLooksEmpty(
result: A11yScanResult,
minElements: number = MIN_MEANINGFUL_ELEMENTS
): boolean {
return result.elementCount < minElements
}
export async function attachScanReport(testInfo: TestInfo, result: A11yScanResult): Promise<void> {
await testInfo.attach('axe-results.json', {
body: JSON.stringify(result, null, 2),
contentType: 'application/json',
})
}
export function blockingViolations(result: A11yScanResult): Result[] {
if (shouldEnforceAll()) return result.violations
return result.violations.filter((violation) => ENFORCED_RULES.includes(violation.id))
}
export { formatViolations, settleForAxe, violationIds } from '../../shared/axe.ts'