用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill accessibility-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
| Use when this capability is needed.
> Use when this capability is needed.
Review architecture and API design for the vfs-s3 project. Use when the user mentions @architect, asks to review an issue's design, discuss module boundaries, API shape, or architectural decisions for vfs-s3. Also trigger when the user wants to create an ADR (Architecture Decision Record) or evaluate a technical approach for the project. Intended for dispatch from Codex automation or Claude routines; GitHub trigger phrase: @vfs-s3-bot please prepare design doc Use when this capability is needed.
基于 SOC 职业分类
正在显示 SKILL.md
| name | accessibility-testing |
| description | > Use when this capability is needed. |
Comprehensive guide for integrating accessibility (a11y) testing into your Playwright test automation.
npm install --save-dev @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage should not have accessibility violations', async ({ page }) => {
await page.goto('https://your-app.com');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Checks', () => {
test('prescription search page should be accessible', async ({ page }) => {
await page.goto('/prescriptions/search');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('refill confirmation page should be accessible', async ({ page }) => {
await page.goto('/prescriptions/refill/confirm');
const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#third-party-widget') // Exclude elements you don't control
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
});
test('user can navigate prescription list with keyboard', async ({ page }) => {
await page.goto('/prescriptions');
// Tab to first prescription
await page.keyboard.press('Tab');
const firstPrescription = page.getByRole('article').first();
await expect(firstPrescription).toBeFocused();
// Navigate with arrow keys
await page.keyboard.press('ArrowDown');
const secondPrescription = page.getByRole('article').nth(1);
await expect(secondPrescription).toBeFocused();
// Activate with Enter
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/.*prescription\/[0-9]+/);
});
test('modal can be closed with Escape key', async ({ page }) => {
await page.goto('/prescriptions');
// Open modal
await page.(, { : }).();
modal = page.();
(modal).();
page..();
(modal).();
});
test('images have alt text', async ({ page }) => {
await page.goto('/prescriptions');
const images = page.getByRole('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
await expect(img).toHaveAttribute('alt');
}
});
test('form inputs have accessible labels', async ({ page }) => {
await page.goto('/patient/registration');
// All inputs should be accessible by label
await expect(page.getByLabel('First name')).toBeVisible();
await expect(page.getByLabel('Last name')).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Phone')).toBeVisible();
});
(, ({ page }) => {
page.();
buttons = page.();
count = buttons.();
( i = ; i < count; i++) {
button = buttons.(i);
accessibleName = button.() || button.();
(accessibleName).();
}
});
test('text has sufficient color contrast', async ({ page }) => {
await page.goto('/dashboard');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
// Check for color contrast violations
const contrastViolations = accessibilityScanResults.violations.filter(
v => v.id === 'color-contrast'
);
expect(contrastViolations).toEqual([]);
});
test('focus moves to error message after validation failure', async ({ page }) => {
await page.goto('/prescriptions/refill');
// Submit form without filling required fields
await page.getByRole('button', { name: 'Submit' }).click();
// Focus should move to error message or first invalid field
const errorMessage = page.getByRole('alert');
await expect(errorMessage).toBeFocused();
});
test('focus is trapped in modal dialog', async ({ page }) => {
await page.goto('/prescriptions');
await page.getByRole('button', { name: 'Delete' }).click();
const modal = page.getByRole('dialog');
const confirmButton = modal.getByRole('button', { name: 'Confirm' });
const cancelButton = modal.getByRole('button', { name: 'Cancel' });
// Tab through modal elements
await page..();
(confirmButton).();
page..();
(cancelButton).();
page..();
(confirmButton).();
});
// accessibility-suite.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Compliance - Prescription Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/prescriptions');
});
test('automated accessibility scan', async ({ page }) => {
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('keyboard navigation', async ({ page }) => {
await page.keyboard.press('Tab');
await expect(page.getByRole('link', { name: 'Skip to main content' })).toBeFocused();
await page.keyboard.();
(page.(, { : })).();
});
(, ({ page }) => {
(page.()).();
(page.()).();
(page.()).();
(page.()).();
});
(, ({ page }) => {
page.(, { : }).();
(page.()).();
(page.()).();
});
});
<!-- ❌ Bad -->
<img src="prescription.jpg">
<!-- ✅ Good -->
<img src="prescription.jpg" alt="Lisinopril 10mg prescription">
/* ❌ Bad - Low contrast */
.text {
color: #999999;
background: #ffffff;
}
/* ✅ Good - High contrast */
.text {
color: #333333;
background: #ffffff;
}
<!-- ❌ Bad - Div as button -->
<div onclick="submit()">Submit</div>
<!-- ✅ Good - Semantic button -->
<button type="submit">Submit</button>
<!-- ❌ Bad - No label -->
<input type="text" name="email" placeholder="Email">
<!-- ✅ Good - Proper label -->
<label for="email">Email</label>
<input type="text" id="email" name="email">
Playwright provides built-in matchers for element-level accessibility checks — no axe-core needed. Use these for targeted regression testing alongside full-page scans.
Verifies an element's accessible name (what screen readers announce).
test('buttons have correct accessible names', async ({ page }) => {
await page.goto('/prescriptions');
// Verify accessible names on interactive elements
await expect(page.getByRole('button', { name: 'Refill' }))
.toHaveAccessibleName('Refill prescription');
await expect(page.getByRole('link', { name: 'View details' }))
.toHaveAccessibleName('View details for Lisinopril 10mg');
// Icon-only button should have an accessible name via aria-label
await expect(page.getByTestId('close-btn'))
.toHaveAccessibleName('Close dialog');
});
test('form inputs have proper accessible names', async ({ page }) => {
await page.goto('/patient/registration');
await expect(page.getByRole('textbox', { name: 'First name' }))
.toHaveAccessibleName('First name');
await expect(page.getByRole(, { : }))
.();
(page.().())
.();
});
Verifies the element's accessible description (additional context for screen readers, often from aria-describedby).
test('form fields have helpful descriptions', async ({ page }) => {
await page.goto('/patient/registration');
// Password field should describe requirements
await expect(page.getByLabel('Password'))
.toHaveAccessibleDescription('Must be at least 8 characters with one number');
// Date field should describe format
await expect(page.getByLabel('Date of birth'))
.toHaveAccessibleDescription(/MM\/DD\/YYYY/);
});
Verifies error messages are properly associated with form inputs via aria-errormessage.
test('form validation shows accessible error messages', async ({ page }) => {
await page.goto('/patient/registration');
// Submit empty form to trigger validation
await page.getByRole('button', { name: 'Register' }).click();
// Verify error messages are programmatically associated with inputs
await expect(page.getByLabel('Email address'))
.toHaveAccessibleErrorMessage('Email is required');
await expect(page.getByLabel('Password'))
.toHaveAccessibleErrorMessage('Password must be at least 8 characters');
// After fixing the error, error message should clear
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Email address').blur();
await expect(page.getByLabel('Email address'))
.not.toHaveAccessibleErrorMessage();
});
test.describe('Comprehensive A11y: Scan + Element Assertions', () => {
test('login form is fully accessible', async ({ page }) => {
await page.goto('/login');
// 1. Full page scan with axe-core
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
// 2. Element-level assertions for regression safety
await expect(page.getByRole('textbox', { name: 'Email' }))
.toHaveAccessibleName('Email');
await expect(page.getByLabel('Password'))
.toHaveAccessibleDescription(/at least 8 characters/);
await expect(page.getByRole('button', { name: 'Sign in' }))
.toHaveAccessibleName('Sign in');
});
});
axe-playwright offers a simpler API than @axe-core/playwright. Good for teams that want minimal setup.
npm install --save-dev axe-playwright
import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y, getViolations } from 'axe-playwright';
test.describe('A11y with axe-playwright', () => {
test('homepage is accessible', async ({ page }) => {
await page.goto('/');
// Step 1: Inject axe-core into the page
await injectAxe(page);
// Step 2: Run accessibility check (auto-fails on violations)
await checkA11y(page);
});
test('scoped scan on specific element', async ({ page }) => {
await page.goto('/prescriptions');
await injectAxe(page);
// Check only the main content area
await checkA11y(page, '#main-content', {
axeOptions: {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa'],
},
},
});
});
test('get violations for custom reporting', async ({ page }) => {
await page.goto('/dashboard');
(page);
violations = (page);
(violations. > ) {
report = violations.( ({
: v.,
: v.,
: v.,
: v..,
}));
.(report);
}
(violations).();
});
});
| Feature | @axe-core/playwright | axe-playwright |
|---|---|---|
| Maintainer | Deque Systems (official) | Community |
| API style | Builder pattern (new AxeBuilder()) | Function calls (injectAxe + checkA11y) |
| Setup | Import and use directly | Inject into page first |
| Flexibility | High (include/exclude, tags, rules) | Moderate |
| Auto-fail on violations | No (you assert manually) | Yes (configurable) |
| Recommendation | ✅ Use for production projects | Good for quick checks |
Track a11y violations over time and prevent regressions in your pipeline.
// fixtures/a11y-fixture.ts
import { test as base, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type A11yFixtures = {
makeAxeBuilder: () => AxeBuilder;
};
export const test = base.extend<A11yFixtures>({
makeAxeBuilder: async ({ page }, use) => {
await use(() =>
new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
);
},
});
export { expect };
// tests/a11y-regression.spec.ts
import { test, expect } from '../fixtures/a11y-fixture';
const criticalPages = [
{ name: 'Home', path: '/' },
{ name: 'Login', path: '/login' },
{ name: 'Dashboard', path: '/dashboard' },
{ name: 'Prescriptions', path: '/prescriptions' },
{ name: 'Profile', path: '/profile' },
{ name: 'Settings', path: '/settings' },
];
for (const { name, path } of criticalPages) {
test(`a11y regression: ${name} page`, async ({ page, makeAxeBuilder }) => {
await page.goto(path);
const results = await makeAxeBuilder().analyze();
// Attach violations to test report for debugging
if (results.violations.length > 0) {
const violationSummary = results.violations.map( => ({
: v.,
: v.,
: v.,
: v..,
}));
.(, .(violationSummary, , ));
}
(results.).([]);
});
}
test('full a11y audit @a11y @regression', async ({ page, makeAxeBuilder }) => {
await page.goto('/');
const results = await makeAxeBuilder().analyze();
expect(results.violations).toEqual([]);
});
# Run only a11y tests in CI
npx playwright test --grep @a11y
# Run a11y tests nightly (not on every PR)
npx playwright test --grep @a11y --project=chromium
Converted and distributed by TomeVault — claim your Tome and manage your conversions.