Axe-Core Accessibility Testing Expert
Эксперт по автоматизированному тестированию доступности с использованием axe-core — индустриального стандарта для проверки соответствия WCAG.
Основная философия
Используйте shift-left подход — интегрируйте проверки доступности на ранних этапах разработки, а не после релиза. Комбинируйте автоматизированное сканирование axe-core с ручным тестированием.
Настройка axe-core
Установка
npm install axe-core @axe-core/playwright @axe-core/react
Базовое использование в браузере
import axe from 'axe-core';
async function runAccessibilityAudit(element = document) { try { const results = await axe.run(element, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa'] } });
return {
violations: results.violations,
passes: results.passes,
incomplete: results.incomplete,
inapplicable: results.inapplicable
};
} catch (error) { console.error('Accessibility audit failed:', error); throw error; } }
Интеграция с Playwright
import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Tests', () => { test('should not have accessibility violations', async ({ page }) => { await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('specific component accessibility', async ({ page }) => { await page.goto('/components/modal');
const results = await new AxeBuilder({ page })
.include('#modal-component')
.exclude('.decorative-element')
.analyze();
expect(results.violations).toEqual([]);
}); });
Интеграция со Storybook
// .storybook/test-runner.js const { injectAxe, checkA11y } = require('axe-playwright');
module.exports = { async preRender(page) { await injectAxe(page); }, async postRender(page) { await checkA11y(page, '#storybook-root', { detailedReport: true, detailedReportOptions: { html: true } }); } };
CI/CD интеграция (GitHub Actions)
name: Accessibility Tests
on: [push, pull_request]
jobs: a11y: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run accessibility tests
run: npm run test:a11y
- name: Upload results
if: failure()
uses: actions/upload-artifact@v3
with:
name: a11y-report
path: a11y-results.json
Обработка результатов
function processViolations(violations) { const report = { critical: [], serious: [], moderate: [], minor: [] };
violations.forEach(violation => { const issue = { id: violation.id, impact: violation.impact, description: violation.description, help: violation.help, helpUrl: violation.helpUrl, nodes: violation.nodes.map(node => ({ html: node.html, target: node.target, failureSummary: node.failureSummary })) };
report[violation.impact].push(issue);
});
return report; }
Конфигурация правил
const axeConfig = { rules: [ // Отключение правил для декоративных элементов { id: 'image-alt', enabled: true }, { id: 'color-contrast', enabled: true }, { id: 'label', enabled: true },
// Исключения для специфичных случаев
{
id: 'button-name',
selector: 'button:not(.icon-only)'
}
],
// Исключение областей exclude: [ '.third-party-widget', '#ads-container' ] };
React интеграция
import React from 'react'; import ReactDOM from 'react-dom'; import axe from '@axe-core/react';
if (process.env.NODE_ENV !== 'production') { axe(React, ReactDOM, 1000); }
// Компонент отчёта function AccessibilityReport({ violations }) { if (!violations.length) { return <div className="a11y-pass">No accessibility violations found</div>; }
return ( <div className="a11y-violations" role="alert"> <h2>Accessibility Issues Found: {violations.length}</h2> <ul> {violations.map(violation => ( <li key={violation.id}> <strong>{violation.impact}</strong>: {violation.description} <a href={violation.helpUrl} target="_blank" rel="noopener"> Learn more </a> </li> ))} </ul> </div> ); }
Типичные нарушения и исправления
Недостаточный контраст цвета
/* Плохо */ .text-light { color: #999; background: #fff; }
/* Хорошо - соотношение 4.5:1 */ .text-light { color: #767676; background: #fff; }
Отсутствие label у формы
<!-- Плохо --> <input type="email" placeholder="Email">
<!-- Хорошо --> <label for="email">Email</label> <input type="email" id="email" placeholder="email@example.com">
Кнопка без текста
<!-- Плохо --> <button><svg>...</svg></button>
<!-- Хорошо --> <button aria-label="Close menu"> <svg aria-hidden="true">...</svg> </button>
Мониторинг и отчётность
class AccessibilityMonitor { constructor() { this.history = []; }
async audit(url) { const results = await runAccessibilityAudit();
this.history.push({
timestamp: new Date().toISOString(),
url,
violationCount: results.violations.length,
violations: results.violations
});
return this.generateTrend();
}
generateTrend() { const recent = this.history.slice(-10); return { current: recent[recent.length - 1]?.violationCount || 0, trend: this.calculateTrend(recent), history: recent }; } }
Лучшие практики
-
Интегрируйте в CI/CD — блокируйте merge при critical нарушениях
-
Тестируйте рано — используйте в dev режиме React/Vue
-
Комбинируйте методы — автоматические тесты + ручное тестирование
-
Документируйте исключения — объясняйте почему правило отключено
-
Отслеживайте тренды — мониторьте количество нарушений во времени