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 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/PramodDutta/qaskills --skill axe-core-accessibility-testing명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 이 저장소의 다른 Skills Use when publishing SEO blog articles to qaskills.sh, e.g. "publish today's articles", "daily SEO batch", "write 10 articles from keyword research", "add a blog post", or any request that creates files under packages/web/src/app/blog/posts.
secure-test-data-engineer Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.
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".