Тестирование доступности
Введение
Playwright может использоваться для тестирования вашего приложения на наличие различных проблем с доступностью.
Некоторые примеры проблем, которые можно выявить:
- Текст, который будет трудно читать пользователям с нарушениями зрения из-за плохого контраста с фоном
- Элементы управления интерфейсом и формы без меток, которые может идентифицировать экранный диктор
- Интерактивные элементы с дублирующимися ID, которые могут запутать вспомогательные технологии
Следующие примеры полагаются на пакет @axe-core/playwright
, который добавляет поддержку запуска движка тестирования доступности axe в рамках ваших тестов Playwright.
Автоматизированные тесты доступности могут обнаружить некоторые распространенные проблемы, такие как отсутствующие или недействительные свойства. Но многие проблемы с доступностью можно обнаружить только с помощью ручного тестирования. Мы рекомендуем использовать комбинацию автоматизированного тестирования, ручных оценок доступности и инклюзивного тестирования пользователей.
Для ручных оценок мы рекомендуем Accessibility Insights for Web, бесплатный и открытый инструмент для разработчиков, который поможет вам оценить веб-сайт на соответствие WCAG 2.1 AA.
Примеры тестов доступности
Тесты доступности работают так же, как и любые другие тесты Playwright. Вы можете либо создать для них отдельные тестовые случаи, либо интегрировать сканирование доступности и утверждения в ваши существующие тестовые случаи.
Следующие примеры демонстрируют несколько базовых сценариев тестирования доступности.
Сканирование всей страницы
Этот пример демонстрирует, как протестировать всю страницу на наличие автоматически обнаруживаемых нарушений доступности. Тест:
- Импортирует пакет
@axe-core/playwright
- Использует обычный синтаксис теста Playwright для определения тестового случая
- Использует обычный синтаксис Playwright для навигации к тестируемой странице
- Ожидает выполнения
AxeBuilder.analyze()
, чтобы запустить сканирование доступности страницы - Использует обычные утверждения теста Playwright assertions, чтобы убедиться, что в возвращенных результатах сканирования нет нарушений
- TypeScript
- JavaScript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // 1
test.describe('homepage', () => { // 2
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/'); // 3
const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4
expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default; // 1
test.describe('homepage', () => { // 2
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/'); // 3
const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4
expect(accessibilityScanResults.violations).toEqual([]); // 5
});
});
Настройка axe для сканирования конкретной части страницы
@axe-core/playwright
поддерживает множество опций конфигурации для axe. Вы можете указать эти опции, используя паттерн Builder с классом AxeBuilder
.
Например, вы можете использовать AxeBuilder.include()
, чтобы ограничить сканирование доступности только одной конкретной частью страницы.
AxeBuilder.analyze()
будет сканировать страницу в её текущем состоянии в момент вызова. Чтобы сканировать части страницы, которые отображаются в результате взаимодействия с интерфейсом, используйте Locators для взаимодействия со страницей перед вызовом analyze()
:
test('navigation menu should not have automatically detectable accessibility violations', async ({
page,
}) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: 'Navigation Menu' }).click();
// Важно дождаться, чтобы страница была в нужном
// состоянии *перед* запуском analyze(). В противном случае axe может не
// найти все элементы, которые ваш тест ожидает сканировать.
await page.locator('#navigation-menu-flyout').waitFor();
const accessibilityScanResults = await new AxeBuilder({ page })
.include('#navigation-menu-flyout')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Сканирование на наличие нарушений WCAG
По умолчанию axe проверяет множество правил доступности. Некоторые из этих правил соответствуют конкретным критериям успеха из Руководства по доступности веб-контента (WCAG), а другие являются правилами "лучшей практики", которые не требуются ни одним критерием WCAG.
Вы можете ограничить сканирование доступности, чтобы запускать только те правила, которые "помечены" как соответствующие конкретным критериям успеха WCAG, используя AxeBuilder.withTags()
. Например, Автоматические проверки Accessibility Insights for Web включают только правила axe, которые проверяют нарушения критериев успеха WCAG A и AA; чтобы соответствовать этому поведению, вы бы использовали теги wcag2a
, wcag2aa
, wcag21a
и wcag21aa
.
Обратите внимание, что автоматическое тестирование не может обнаружить все типы нарушений WCAG.
test('should not have any automatically detectable WCAG A or AA violations', async ({ page }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Вы можете найти полный список тегов правил, поддерживаемых axe-core, в разделе "Теги Axe-core" документации по API axe.
Обработка известных проблем
Один из распространенных вопросов при добавлении тестов доступности в приложение: "как подавить известные нарушения?" Следующие примеры демонстрируют несколько техник, которые вы можете использовать.
Исключение отдельных элементов из сканирования
Если ваше приложение содержит несколько конкретных элементов с известными проблемами, вы можете использовать AxeBuilder.exclude()
, чтобы исключить их из сканирования, пока вы не сможете исправить проблемы.
Это обычно самый простой вариант, но у него есть некоторые важные недостатки:
exclude()
исключит указанные элементы и всех их потомков. Избегайте использования его с компонентами, содержащими много дочерних элементов.exclude()
предотвратит выполнение всех правил для указанных элементов, а не только правил, соответствующих известным проблемам.
Вот пример исключения одного элемента из сканирования в одном конкретном тесте:
test('should not have any accessibility violations outside of elements with known issues', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#element-with-known-issue')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Если рассматриваемый элемент используется многократно на многих страницах, рассмотрите возможность использования тестового фикстура для повторного использования одной и той же конфигурации AxeBuilder
в нескольких тестах.
Отключение отдельных правил сканирования
Если ваше приложение содержит множество различных существующих нарушений конкретного правила, вы можете использовать AxeBuilder.disableRules()
, чтобы временно отключить отдельные правила, пока вы не сможете исправить проблемы.
Вы можете найти идентификаторы правил, которые нужно передать в disableRules()
, в свойстве id
нарушений, которые вы хотите подавить. Полный список правил axe можно найти в документации axe-core
.
test('should not have any accessibility violations outside of rules with known issues', async ({
page,
}) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(['duplicate-id'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Использование снимков для разрешения конкретных известных проблем
Если вы хотите разрешить более детализированный набор известных проблем, вы можете использовать Снимки, чтобы убедиться, что набор существующих нарушений не изменился. Этот подход избегает недостатков использования AxeBuilder.exclude()
за счет небольшой дополнительной сложности и хрупкости.
Не используйте снимок всего массива accessibilityScanResults.violations
. Он содержит детали реализации рассматриваемых элементов, такие как фрагмент их отображаемого HTML; если вы включите их в свои снимки, это сделает ваши тесты склонными к поломке каждый раз, когда один из рассматриваемых компонентов изменяется по несвязанной причине:
// Не делайте этого! Это хрупко.
expect(accessibilityScanResults.violations).toMatchSnapshot();
Вместо этого создайте отпечаток нарушения(ий), содержащий только достаточно информации для уникальной идентификации проблемы, и используйте снимок отпечатка:
// Это менее хрупко, чем создание снимка всего массива нарушений.
expect(violationFingerprints(accessibilityScanResults)).toMatchSnapshot();
// my-test-utils.js
function violationFingerprints(accessibilityScanResults) {
const violationFingerprints = accessibilityScanResults.violations.map(violation => ({
rule: violation.id,
// Это CSS-селекторы, которые уникально идентифицируют каждый элемент с
// нарушением рассматриваемого правила.
targets: violation.nodes.map(node => node.target),
}));
return JSON.stringify(violationFingerprints, null, 2);
}
Экспорт результатов сканирования в качестве вложения к тесту
Большинство тестов доступности в первую очередь сосредоточены на свойстве violations
результатов сканирования axe. Однако результаты сканирования содержат не только violations
. Например, результаты также содержат информацию о правилах, которые прошли проверку, и об элементах, которые axe нашел с неокончательными результатами для некоторых правил. Эта информация может быть полезна для отладки тестов, которые не обнаруживают все ожидаемые нарушения.
Чтобы включить все результаты сканирования в ваши результаты тестов для целей отладки, вы можете добавить результаты сканирования в качестве вложения к тесту с помощью testInfo.attach()
. Отчеты затем могут встроить или связать полные результаты в качестве части вашего тестового вывода.
Следующий пример демонстрирует прикрепление результатов сканирования к тесту:
test('example with attachment', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
});
expect(accessibilityScanResults.violations).toEqual([]);
});
Использование тестового фикстура для общей конфигурации axe
Тестовые фикстуры — это хороший способ поделиться общей конфигурацией AxeBuilder
между многими тестами. Некоторые сценарии, в которых это может быть полезно, включают:
- Использование общего набора правил среди всех ваших тестов
- Подавление известного нарушения в общем элементе, который появляется на многих разных страницах
- Последовательное прикрепление автономных отчетов о доступности для многих сканирований
Следующий пример демонстрирует создание и использование тестового фикстура, который охватывает каждый из этих сценариев.
Создание фикстура
Этот пример фикстура создает объект AxeBuilder
, который предварительно настроен с общей конфигурацией withTags()
и exclude()
.
- TypeScript
- JavaScript
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type AxeFixture = {
makeAxeBuilder: () => AxeBuilder;
};
// Расширяем базовый тест, предоставляя "makeAxeBuilder"
//
// Этот новый "тест" можно использовать в нескольких тестовых файлах, и каждый из них получит
// последовательно настроенный экземпляр AxeBuilder.
export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use, testInfo) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
}
});
export { expect } from '@playwright/test';
// axe-test.js
const base = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
// Расширяем базовый тест, предоставляя "makeAxeBuilder"
//
// Этот новый "тест" можно использовать в нескольких тестовых файлах, и каждый из них получит
// последовательно настроенный экземпляр AxeBuilder.
exports.test = base.test.extend({
makeAxeBuilder: async ({ page }, use, testInfo) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
}
});
exports.expect = base.expect;
Использование фикстура
Чтобы использовать фикстур, замените new AxeBuilder({ page })
из предыдущих примеров на вновь определенный фикстур makeAxeBuilder
:
const { test, expect } = require('./axe-test');
test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await makeAxeBuilder()
// Автоматически использует общую конфигурацию AxeBuilder,
// но также поддерживает дополнительную конфигурацию, специфичную для теста
.include('#specific-element-under-test')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});