
Playwright Testing
- 225 installs
- 381 repo stars
- Updated August 4, 2026
- community-access/accessibility-agents
Automate keyboard, focus, ARIA, and axe-core checks in Playwright suites to catch regressions on critical flows before release.
About
Implements Playwright-based accessibility testing with automated scans and interaction checks. Validates focus order, ARIA semantics, and WCAG violations on user flows so teams block regressions in CI before shipping frontend changes to production.
- axe-core integration in Playwright
- Keyboard tab order assertions
- Focus management on dialogs
- ARIA role and name checks
- CI-friendly HTML and JUnit reports
Playwright Testing by the numbers
- 225 all-time installs (skills.sh)
- Ranked #775 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/community-access/accessibility-agents --skill playwright-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 225 |
|---|---|
| repo stars | ★ 381 |
| Last updated | August 4, 2026 |
| Repository | community-access/accessibility-agents ↗ |
What it does
Automate keyboard, focus, ARIA, and axe-core checks in Playwright suites to catch regressions on critical flows before release.
Files
Playwright Accessibility Testing
Reusable knowledge module for browser-based accessibility testing using Playwright and @axe-core/playwright.
MCP Tools Available
| Tool | Purpose | Requires @axe-core/playwright |
|---|---|---|
run_playwright_keyboard_scan | Tab-order traversal, keyboard trap detection | No |
run_playwright_state_scan | Click triggers, scan revealed content with axe-core | Yes |
run_playwright_viewport_scan | Multi-viewport axe-core + touch target measurement | Yes |
run_playwright_contrast_scan | Computed-style contrast ratio after CSS cascade | No |
run_playwright_a11y_tree | Browser accessibility tree snapshot | No |
@axe-core/playwright Patterns
Full Page Scan
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();Scoped Element Scan
const results = await new AxeBuilder({ page })
.include('.modal-content')
.withTags(['wcag2a', 'wcag2aa'])
.analyze();Single Rule Verification
const results = await new AxeBuilder({ page })
.include('#hero-image')
.withRules(['image-alt'])
.analyze();
expect(results.violations).toEqual([]);Scan After Interaction
await page.click('[aria-expanded="false"]');
await page.waitForSelector('.accordion-content', { state: 'visible' });
const results = await new AxeBuilder({ page })
.include('.accordion-content')
.analyze();Keyboard Traversal Patterns
Record Tab Sequence
const tabStops = [];
for (let i = 0; i < maxTabs; i++) {
await page.keyboard.press('Tab');
const info = await page.evaluate(() => {
const el = document.activeElement;
return {
tagName: el?.tagName,
role: el?.getAttribute('role'),
name: el?.getAttribute('aria-label') || el?.textContent?.trim().slice(0, 50),
id: el?.id,
tabIndex: el?.tabIndex
};
});
tabStops.push(info);
}Detect Keyboard Traps
A keyboard trap is detected when the same element receives focus after consecutive Tab presses:
let trapCount = 0;
let lastSelector = '';
for (const stop of tabStops) {
const currentSelector = `${stop.tagName}#${stop.id}`;
if (currentSelector === lastSelector) {
trapCount++;
if (trapCount >= 3) { /* TRAP DETECTED */ }
} else {
trapCount = 0;
}
lastSelector = currentSelector;
}Focus Management After Modal Open
await page.click('[data-modal-trigger]');
await page.waitForSelector('[role="dialog"]', { state: 'visible' });
const focusedRole = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') ? 'inside-dialog' : 'outside-dialog'
);
// focusedRole should be 'inside-dialog'Focus Management Test Templates
Modal Focus Trap Test
test('modal traps focus correctly', async ({ page }) => {
await page.goto(url);
await page.click('[data-open-modal]');
await page.waitForSelector('[role="dialog"]', { state: 'visible' });
// Focus should be inside the dialog
const inDialog = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') !== null
);
expect(inDialog).toBe(true);
// Tab through dialog — should not escape
for (let i = 0; i < 20; i++) {
await page.keyboard.press('Tab');
const stillInDialog = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') !== null
);
expect(stillInDialog).toBe(true);
}
// Escape should close and return focus to trigger
await page.keyboard.press('Escape');
const focusedId = await page.evaluate(() => document.activeElement?.id);
expect(focusedId).toBe('modal-trigger-id');
});Skip Link Test
test('skip link moves focus to main content', async ({ page }) => {
await page.goto(url);
await page.keyboard.press('Tab'); // Focus skip link
await page.keyboard.press('Enter'); // Activate it
const focusedId = await page.evaluate(() => document.activeElement?.id);
expect(focusedId).toBe('main-content');
});CI Integration
GitHub Actions with Playwright
name: Accessibility Tests
on: [push, pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Start dev server
run: npm run dev &
env:
CI: true
- name: Wait for server
run: npx wait-on http://localhost:3000 --timeout 30000
- name: Run accessibility tests
run: npx playwright test tests/a11y/
- uses: actions/upload-artifact@v4
if: failure()
with:
name: a11y-test-results
path: test-results/Playwright Config for Accessibility Tests
// playwright.config.js (a11y section)
export default {
testDir: './tests/a11y',
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Use Chromium only — @axe-core/playwright is Chromium-validated
browserName: 'chromium',
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
};Graceful Degradation
Detection Pattern
let _playwrightAvailable = null;
async function isPlaywrightAvailable() {
if (_playwrightAvailable !== null) return _playwrightAvailable;
try {
await import('playwright');
_playwrightAvailable = true;
} catch {
_playwrightAvailable = false;
}
return _playwrightAvailable;
}Degradation Matrix
| Playwright | @axe-core/playwright | Available Scans |
|---|---|---|
| Yes | Yes | All 5 tools (keyboard, state, viewport, contrast, tree) |
| Yes | No | 3 tools (keyboard, contrast, tree) |
| No | — | None — fall back to code review + axe-core CLI |
User-Facing Messages
When unavailable:
Playwright not installed. Behavioral testing (keyboard traversal, dynamic states,
responsive viewport, rendered contrast) is unavailable.
Install: npm install -D playwright @axe-core/playwright && npx playwright install chromiumWhen partially available:
@axe-core/playwright not installed. State scanning and viewport scanning are
unavailable. Keyboard, contrast, and accessibility tree scans will proceed.
Install: npm install -D @axe-core/playwrightWCAG Coverage Map
| WCAG SC | Description | Playwright Tool |
|---|---|---|
| 1.3.1 | Info and Relationships | a11y tree, state scan |
| 1.4.3 | Contrast (Minimum) | contrast scan |
| 1.4.6 | Contrast (Enhanced) | contrast scan |
| 1.4.10 | Reflow | viewport scan |
| 2.1.1 | Keyboard | keyboard scan |
| 2.1.2 | No Keyboard Trap | keyboard scan |
| 2.4.3 | Focus Order | keyboard scan |
| 2.4.7 | Focus Visible | keyboard scan |
| 2.5.5 | Target Size (Enhanced) | viewport scan |
| 2.5.8 | Target Size (Minimum) | viewport scan |
| 4.1.2 | Name, Role, Value | a11y tree, state scan |