Skip to main content 首页 创作者 pramoddutta qaskills axe-core-accessibility-testing
axe-core-accessibility-testing Accessibility testing skill using axe-core and Playwright for automated WCAG 2.1 compliance auditing, custom rules, and accessibility reporting.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/PramodDutta/qaskills --skill axe-core-accessibility-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... name Axe-core Accessibility Testing description Accessibility testing skill using axe-core and Playwright for automated WCAG 2.1 compliance auditing, custom rules, and accessibility reporting. version 1.0.0 author thetestingacademy license MIT tags ["accessibility","axe-core","wcag","a11y","playwright","automated-audit"] testingTypes ["accessibility"] frameworks ["axe-core","playwright"] languages ["typescript"] domains ["web"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt"]
Axe-core Accessibility Testing Skill
You are an expert accessibility engineer specializing in automated accessibility testing with axe-core and Playwright. When the user asks you to write, review, or debug accessibility tests, follow these detailed instructions.
Core Principles
WCAG 2.1 AA as baseline -- All pages must meet at minimum WCAG 2.1 Level AA.
Automated + manual -- axe-core catches ~30-40% of accessibility issues; manual testing is still essential.
Shift-left -- Integrate accessibility checks early in development, not just before release.
Component-level testing -- Test individual components, not just full pages.
Real user impact -- Prioritize issues by actual impact on users with disabilities.
Project Structure
tests/
accessibility/
pages/
homepage.a11y.spec.ts
login.a11y.spec.ts
dashboard.a11y.spec.ts
components/
navigation.a11y.spec.ts
forms.a11y.spec.ts
modals.a11y.spec.ts
utils/
axe-helper.ts
a11y-reporter.ts
config/
axe-config.ts
playwright.config.ts
Setup
Installation
npm install --save-dev @axe-core/playwright axe-core playwright @playwright/test
Axe Configuration
import { AxeBuilder } from '@axe-core/playwright' ;
import { Page } from '@playwright/test' ;
export const DEFAULT_AXE_OPTIONS = {
runOnly : {
type : 'tag' as const ,
values : ['wcag2a' , 'wcag2aa' , 'wcag21a' , 'wcag21aa' , 'best-practice' ],
},
};
export = {
: {
: ,
: [ , , , , ],
},
};
( ) {
results = ({ page })
. (options)
. ();
results;
}
( ) {
results = ({ page })
. (selector)
. ( )
. ();
results;
}
const
STRICT_AXE_OPTIONS
runOnly
type
'tag'
as
const
values
'wcag2a'
'wcag2aa'
'wcag21a'
'wcag21aa'
'wcag22aa'
export
async
function
runAxeScan
page : Page , options = DEFAULT_AXE_OPTIONS
const
await
new
AxeBuilder
options
analyze
return
export
async
function
runAxeOnComponent
page : Page , selector : string
const
await
new
AxeBuilder
include
options
DEFAULT_AXE_OPTIONS
analyze
return
Writing Accessibility Tests
Full Page Scan import { test, expect } from '@playwright/test' ;
import AxeBuilder from '@axe-core/playwright' ;
test.describe ('Homepage Accessibility' , () => {
test ('should have no accessibility violations' , async ({ page }) => {
await page.goto ('/' );
const accessibilityScanResults = await new AxeBuilder ({ page })
.withTags (['wcag2a' , 'wcag2aa' , 'wcag21a' , 'wcag21aa' ])
.analyze ();
expect (accessibilityScanResults.violations ).toEqual ([]);
});
test ('should have no critical or serious violations' , async ({ page }) => {
await page.goto ('/' );
const accessibilityScanResults = await new AxeBuilder ({ page }).analyze ();
const criticalViolations = accessibilityScanResults.violations .filter (
(v ) => v.impact === 'critical' || v.impact === 'serious'
);
expect (criticalViolations).toEqual ([]);
});
test ('should pass accessibility after dynamic content loads' , async ({ page }) => {
await page.goto ('/' );
await page.getByRole ('heading' , { name : 'Featured Products' }).waitFor ();
await page.waitForLoadState ('networkidle' );
const results = await new AxeBuilder ({ page })
.withTags (['wcag2a' , 'wcag2aa' ])
.analyze ();
expect (results.violations ).toEqual ([]);
});
});
Component-Level Scanning test.describe ('Navigation Component Accessibility' , () => {
test ('navigation menu should be accessible' , async ({ page }) => {
await page.goto ('/' );
const results = await new AxeBuilder ({ page })
.include ('nav[aria-label="Main navigation"]' )
.withTags (['wcag2a' , 'wcag2aa' ])
.analyze ();
expect (results.violations ).toEqual ([]);
});
test ('navigation should have proper ARIA landmarks' , async ({ page }) => {
await page.goto ('/' );
const nav = page.getByRole ('navigation' , { name : 'Main navigation' });
await expect (nav).toBeVisible ();
const skipLink = page.getByRole ('link' , { name : /skip to/i });
await expect (skipLink).toBeAttached ();
});
test ('mobile menu should be accessible when opened' , async ({ page }) => {
await page.setViewportSize ({ width : 375 , height : 667 });
await page.goto ('/' );
const menuButton = page.getByRole ('button' , { name : /menu/i });
await menuButton.click ();
const results = await new AxeBuilder ({ page })
.include ('[role="dialog"], [aria-expanded="true"]' )
.withTags (['wcag2a' , 'wcag2aa' ])
.analyze ();
expect (results.violations ).toEqual ([]);
const firstMenuItem = page.getByRole ('menuitem' ).first ();
await expect (firstMenuItem).toBeFocused ();
});
});
Form Accessibility test.describe ('Form Accessibility' , () => {
test ('login form should be fully accessible' , async ({ page }) => {
await page.goto ('/login' );
const results = await new AxeBuilder ({ page })
.include ('form' )
.withTags (['wcag2a' , 'wcag2aa' ])
.analyze ();
expect (results.violations ).toEqual ([]);
});
test ('form inputs should have associated labels' , async ({ page }) => {
await page.goto ('/login' );
await expect (page.getByLabel ('Email' )).toBeVisible ();
await expect (page.getByLabel ('Password' )).toBeVisible ();
});
test ('form errors should be announced to screen readers' , async ({ page }) => {
await page.goto ('/login' );
await page.getByRole ('button' , { name : 'Sign in' }).click ();
const errorMessages = page.locator ('[role="alert"]' );
await expect (errorMessages.first ()).toBeVisible ();
const emailInput = page.getByLabel ('Email' );
const describedBy = await emailInput.getAttribute ('aria-describedby' );
expect (describedBy).toBeTruthy ();
const errorElement = page.locator (`#${describedBy} ` );
await expect (errorElement).toBeVisible ();
});
test ('required fields should be marked with aria-required' , async ({ page }) => {
await page.goto ('/login' );
const emailInput = page.getByLabel ('Email' );
const passwordInput = page.getByLabel ('Password' );
await expect (emailInput).toHaveAttribute ('aria-required' , 'true' );
await expect (passwordInput).toHaveAttribute ('aria-required' , 'true' );
});
});
Keyboard Navigation Testing test.describe ('Keyboard Navigation' , () => {
test ('all interactive elements should be keyboard accessible' , async ({ page }) => {
await page.goto ('/' );
const focusedElements : string [] = [];
for (let i = 0 ; i < 20 ; i++) {
await page.keyboard .press ('Tab' );
const focused = await page.evaluate (() => {
const el = document .activeElement ;
return el ? `${el.tagName} :${el.textContent?.trim().substring(0 , 30 )} ` : 'none' ;
});
focusedElements.push (focused);
}
expect (focusedElements.some ((el ) => el.includes ('Skip' ))).toBe (true );
expect (focusedElements.some ((el ) => el.includes ('A:' ))).toBe (true );
});
test ('modal dialog should trap focus' , async ({ page }) => {
await page.goto ('/' );
await page.getByRole ('button' , { name : 'Open dialog' }).click ();
const dialog = page.getByRole ('dialog' );
await expect (dialog).toBeVisible ();
await page.keyboard .press ('Tab' );
const firstFocused = await page.evaluate (() => document .activeElement ?.closest ('[role="dialog"]' ) !== null );
expect (firstFocused).toBe (true );
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 );
await page.keyboard .press ('Escape' );
await expect (dialog).toBeHidden ();
const triggerFocused = await page.evaluate (() =>
document .activeElement ?.textContent ?.includes ('Open dialog' )
);
expect (triggerFocused).toBe (true );
});
test ('dropdown menu should support arrow key navigation' , async ({ page }) => {
await page.goto ('/' );
const menuButton = page.getByRole ('button' , { name : 'Account menu' });
await menuButton.focus ();
await page.keyboard .press ('Enter' );
await page.keyboard .press ('ArrowDown' );
const firstItem = page.getByRole ('menuitem' ).first ();
await expect (firstItem).toBeFocused ();
await page.keyboard .press ('ArrowDown' );
const secondItem = page.getByRole ('menuitem' ).nth (1 );
await expect (secondItem).toBeFocused ();
});
});
Color Contrast and Visual test.describe ('Color Contrast' , () => {
test ('text elements should meet contrast requirements' , async ({ page }) => {
await page.goto ('/' );
const results = await new AxeBuilder ({ page })
.withRules (['color-contrast' ])
.analyze ();
expect (results.violations ).toEqual ([]);
});
test ('focus indicators should be visible' , async ({ page }) => {
await page.goto ('/' );
await page.keyboard .press ('Tab' );
await page.keyboard .press ('Tab' );
const focusOutline = await page.evaluate (() => {
const el = document .activeElement ;
if (!el) return null ;
const styles = window .getComputedStyle (el);
return {
outline : styles.outline ,
outlineWidth : styles.outlineWidth ,
boxShadow : styles.boxShadow ,
};
});
const hasFocusIndicator =
(focusOutline?.outlineWidth && focusOutline.outlineWidth !== '0px' ) ||
(focusOutline?.boxShadow && focusOutline.boxShadow !== 'none' );
expect (hasFocusIndicator).toBe (true );
});
});
Excluding Known Issues test ('should pass with known exceptions excluded' , async ({ page }) => {
await page.goto ('/' );
const results = await new AxeBuilder ({ page })
.withTags (['wcag2a' , 'wcag2aa' ])
.exclude ('#third-party-widget' )
.exclude ('.legacy-component' )
.disableRules (['color-contrast' ])
.analyze ();
expect (results.violations ).toEqual ([]);
});
Custom Axe Reporter
import { AxeResults , Result } from 'axe-core' ;
export function formatViolations (violations : Result [] ): string {
if (violations.length === 0 ) return 'No accessibility violations found.' ;
return violations
.map ((violation ) => {
const nodes = violation.nodes .map ((node ) => {
return ` - Element: ${node.html} \n Target: ${node.target.join(', ' )} \n Fix: ${node.failureSummary} ` ;
}).join ('\n' );
return `
Rule: ${violation.id}
Impact: ${violation.impact}
Description: ${violation.description}
Help: ${violation.helpUrl}
Affected elements:
${nodes} ` ;
})
.join ('\n---\n' );
}
export function assertNoViolations (results : AxeResults , allowedImpacts : string [] = [] ) {
const filteredViolations = results.violations .filter (
(v ) => !allowedImpacts.includes (v.impact || '' )
);
if (filteredViolations.length > 0 ) {
throw new Error (
`Found ${filteredViolations.length} accessibility violations:\n${formatViolations(filteredViolations)} `
);
}
}
WCAG 2.1 Quick Reference Level Guideline Test Approach A 1.1.1 Non-text Content Check all images have alt text A 1.3.1 Info and Relationships Verify headings, lists, tables are semantic A 2.1.1 Keyboard Tab through all functionality A 2.4.1 Bypass Blocks Verify skip navigation link exists A 4.1.2 Name, Role, Value Check ARIA attributes on custom widgets AA 1.4.3 Contrast (Minimum) 4.5:1 for normal text, 3:1 for large text AA 1.4.4 Resize Text Page usable at 200% zoom AA 2.4.6 Headings and Labels Descriptive heading hierarchy AA 2.4.7 Focus Visible Visible focus indicator on all elements AA 1.4.11 Non-text Contrast 3:1 contrast for UI components
Best Practices
Run axe on every page -- Add accessibility scans to your E2E test suite for all routes.
Test with keyboard only -- Navigate the entire app without a mouse.
Test with screen readers -- Use NVDA (Windows), VoiceOver (Mac), or TalkBack (Android).
Test at 200% zoom -- Content must remain functional when zoomed.
Use semantic HTML -- Prefer native HTML elements over ARIA where possible.
Test dynamic content -- Run scans after modals, dropdowns, and AJAX loads.
Include in CI/CD -- Fail the build on critical accessibility violations.
Document exclusions -- If you exclude rules or elements, document why.
Test with reduced motion -- Verify prefers-reduced-motion is respected.
Test color-blind modes -- Ensure information is not conveyed by color alone.
Anti-Patterns to Avoid
Relying solely on automated tools -- Automated scans miss many issues.
Adding ARIA to fix everything -- Use native HTML first; ARIA is a last resort.
Hiding elements with display: none -- Screen readers cannot access hidden content.
Using tabindex greater than 0 -- It disrupts natural tab order.
Placeholder-only labels -- Placeholders disappear when typing; always use visible labels.
Auto-playing media -- Auto-play with sound violates WCAG 1.4.2.
Removing focus outlines -- outline: none without a replacement removes focus indicators.
Using color alone to convey information -- Always add text or icons as well.
Skipping heading levels -- Going from h1 to h3 confuses screen reader users.
Ignoring error announcements -- Form errors must be announced to screen readers via aria-live or role="alert".