Тестирование через accessibility
Я обычно смотрю на accessibility-разметку не только как на способ сделать интерфейс более доступным, но и как на способ сделать его более пригодным для тестирования.
Когда я проектирую интерфейс, я стараюсь разбивать его на связные логические области и описывать эти области плотной, осмысленной a11y-разметкой. Это дает сразу два эффекта: интерфейс становится удобнее для вспомогательных технологий, а тесты начинают опираться не на случайные детали верстки, а на намерение, выраженное в самом интерфейсе.
Рабочая идея
Чем точнее интерфейс выражает свою структуру через роли, имена и связи между элементами, тем проще писать тесты, которые описывают то, что пользователь действительно делает.
Обычно это приводит к тестам, которые:
- слабее связаны с внутренней разметкой
- проще читаются и поддерживаются
- лучше отражают продуктовый смысл
- устойчивее к рефакторингу, который не меняет поведение интерфейса и его намерение
- чаще ломаются именно тогда, когда должны ломаться, то есть когда реально меняется намерение интерфейса
Почему это важно для меня
Мне ближе тесты, которые работают со смыслом, а не со случайной структурой.
Если тесту нужны хрупкие селекторы, безымянные обертки или layout-specific hooks, это часто означает, что сам интерфейс недостаточно ясно выражает свое намерение. Хорошая accessibility-разметка помогает сократить этот разрыв.
То же самое часто относится и к селекторам на базе class, data-* и похожим тестовым опорам. Нередко это либо детали конкретной реализации, либо искусственные маркеры, добавленные только ради QA. Хорошая accessibility-семантика, в отличие от этого, приносит продукту самостоятельную и вполне ощутимую ценность, а не служит лишь удобством для тестового стека.
Мне также нравится опираться на accessibility потому, что эта область уже достаточно хорошо стандартизирована и дает командам общий язык. Когда люди опираются на роли, имена и связи между элементами, определенные стандартами, они обычно и ищут элементы похожим образом, и строят селекторы вокруг одних и тех же понятий. За счет этого тесты проще обсуждать, ревьюить и развивать внутри команды.
Типичное направление мысли
На практике этот подход обычно начинается с нескольких простых вопросов:
- какие логические области есть на экране
- у каких контролов есть устойчивые доступные имена
- какие связи между элементами нужно сделать явными
- какое пользовательское намерение должен описывать тест
Инструменты, которые уже помогают
Некоторые инструменты уже хорошо поддерживают такой стиль тестирования.
JavaScript и TypeScript
DOM Testing Library- это базовый строительный блок. Он подталкивает искать элементы так, как их воспринимает пользователь, а не так, как они случайно устроены внутри.user-eventпомогает выражать взаимодействия как реальные пользовательские действия, а не как низкоуровневую отправку событий.Playwrightособенно полезен на browser-уровне, потому что его locator API поддерживаетgetByRole,getByLabel,getByAltTextи другие запросы, близкие к пользовательскому восприятию.axe-coreполезен как дополнительный автоматизированный слой. Он не заменяет тесты, выражающие намерение, но помогает систематически находить accessibility-нарушения.
React и Vue
React Testing Libraryпереносит подход Testing Library в React и удерживает тесты ближе к DOM-семантике, а не к внутренностям компонентов.Vue Testing Libraryделает то же самое для Vue и особенно полезен там, где хочется тестировать отрендеренное поведение, а не механику фреймворка.
PHP
В PHP-экосистеме меньше инструментов, которые изначально построены вокруг accessibility-first запросов.
Для PHP-проектов я бы обычно смотрел в сторону browser-level инструментов вроде Symfony Panther, когда нужен настоящий end-to-end слой. Он не дает из коробки той же a11y-ориентированной модели запросов, что Testing Library или Playwright, но все равно хорошо работает как исполнительный слой поверх интерфейса, который уже семантически организован.
Это еще одна причина, по которой для меня так важен browser и DOM-слой: если интерфейс достаточно ясно выражает свой смысл, от этого выигрывают многие тестовые стеки, даже если их API по-разному поддерживают accessibility-понятия.
Пример: тестирование кастомного combobox через намерение
Ниже пример того паттерна, который мне обычно нравится. Если интерфейс уже выражает понятный accessibility-контракт, то и тесты, на мой взгляд, должны переиспользовать именно этот контракт, а не заново изобретать его через хрупкие селекторы в каждом файле.
В этом случае переиспользуемый примитив - это не просто "найди popup". Это "разреши элемент по семантическим критериям и при необходимости пройди по явным accessibility-связям вроде aria-controls".
import { computeAccessibleName } from 'dom-accessibility-api'
import { buildQueries } from '@testing-library/dom'
import { within } from '@testing-library/dom'
type Criteria = {
role?: string
name?: string
controlledby?: HTMLElement
}
function queryAllByCriteria(container: HTMLElement, criteria: Criteria): HTMLElement[] {
let candidate: HTMLElement | null = null
if (criteria.controlledby) {
const controlledId = criteria.controlledby.getAttribute('aria-controls')
if (!controlledId) {
throw new Error('Expected control to expose aria-controls')
}
candidate = criteria.controlledby.ownerDocument.getElementById(controlledId)
if (!candidate) {
throw new Error(
`Expected aria-controls="${controlledId}" to reference an existing element`
)
}
}
if (!candidate && criteria.role) {
candidate = within(container).getByRole(criteria.role, {
name: criteria.name
})
}
if (!candidate) {
return []
}
if (criteria.role && candidate.getAttribute('role') !== criteria.role) {
return []
}
if (
criteria.name &&
computeAccessibleName(candidate) !== criteria.name
) {
return []
}
return [candidate]
}
const getMultipleError = (_container: HTMLElement, criteria: Criteria) =>
`Found multiple elements matching criteria ${JSON.stringify(criteria)}`
const getMissingError = (_container: HTMLElement, criteria: Criteria) =>
`Unable to find an element matching criteria ${JSON.stringify(criteria)}`
export const [
queryByCriteria,
getAllByCriteria,
getByCriteria,
findAllByCriteria,
findByCriteria
] = buildQueries(queryAllByCriteria, getMultipleError, getMissingError)Тогда сам тест становится короче и при этом яснее в том, что он делает. В этом примере я исхожу из того, что getByCriteria уже зарегистрирован в общем test setup, поэтому within(...) может использовать его напрямую:
import userEvent from '@testing-library/user-event'
import { render, screen, within } from '@testing-library/vue'
import ProjectMembersDialog from './ProjectMembersDialog.vue'
test('adds a reviewer through the members dialog', async () => {
const user = userEvent.setup()
render(ProjectMembersDialog, {
props: {
open: true,
projectName: 'Omnica',
availableReviewers: [
{ id: 'anna-case', name: 'Anna Case' },
{ id: 'kirill-zaitsev', name: 'Kirill Zaitsev' }
]
}
})
const workspace = screen.getByRole('region', { name: 'Project members' })
const dialog = within(workspace).getByRole('dialog', {
name: 'Manage project members'
})
const reviewerForm = within(dialog).getByRole('group', {
name: 'Add reviewer'
})
const reviewerCombobox = within(reviewerForm).getByRole('combobox', {
name: 'Reviewer'
})
await user.click(reviewerCombobox)
const reviewerOptions = within(reviewerForm).getByCriteria({
role: 'listbox',
name: 'Reviewer suggestions',
controlledby: reviewerCombobox
})
await user.click(
within(reviewerOptions).getByRole('option', { name: 'Kirill Zaitsev' })
)
await user.click(
within(reviewerForm).getByRole('button', { name: 'Add reviewer' })
)
expect(
within(dialog).getByRole('listitem', { name: 'Kirill Zaitsev' })
).toBeVisible()
})Что мне здесь нравится - тест по-прежнему проходит по интерфейсу через те же понятия, которые важны и для дизайна интерфейса, и для accessibility-review:
- region
- dialog
- group
- combobox
- listbox
- option
- button
Но сам контракт aria-controls уже не размазан по тестам вручную. Он превращается в переиспользуемый примитив, который команда может применять по всей дизайн-системе или продуктовой линейке. За счет этого тесты становятся короче, но не скатываются обратно к деталям реализации.
В более крупной кодовой базе такой helper можно затем развить в полноценный semantic query layer. Но даже в таком компактном виде он уже превращает accessibility-контракт в переиспользуемый и обсуждаемый командой инструмент.
Если такой query действительно часто нужен, я бы обычно выносил его из локального примера в общий test setup проекта. В этот момент он перестает быть разовой утилитой и становится частью общего тестового языка команды, поэтому snippet выше уже использует его так, как будто он встроен из коробки.
Мне также кажется, что текущая многословность accessibility-ориентированных тестов часто является проблемой инструментария, а не самой семантики. Даже в этом небольшом примере, чтобы получить действительно удобную форму, пришлось писать custom query. Сам паттерн при этом вовсе не выглядит экзотическим; скорее, ему просто пока не хватает более зрелого слоя инструментов, который относился бы к таким контрактам как к гражданам первого класса.
Разбиение экранов на семантические области
Когда я думаю про accessibility-структуру, я обычно начинаю с самого крупного масштаба и стараюсь не проваливаться в мелкие детали слишком рано.
Первый проход касается общей компоновки:
- на какие глобальные зоны поделен экран
- где находится навигация
- где находится основной контент
- есть ли header или footer
- как эти части расположены относительно друг друга
Такая первая классификация уже дает мне начальную семантическую карту интерфейса. Она помогает отделить то, что относится к навигации уровня страницы, от того, что относится к контенту, и от того, что должно остаться локальным внутри меньшего интерактивного контекста.
Только после этого я начинаю спускаться по иерархии ниже и разбирать внутренности каждого блока. На этом уровне я обычно смотрю:
- какие подзоны достаточно осмысленны, чтобы получить имя
- какие контролы принадлежат одной группе взаимодействия
- какие связи между labels, descriptions и controlled elements нужно сделать явными
- где блок достаточно связен, чтобы стать самостоятельной семантической единицей
Такой проход сверху вниз важен для меня потому, что он заранее снижает хаос. Вместо того чтобы реактивно раскидывать роли и имена по странице, я сначала получаю более ясную структуру, а уже затем уточняю ее по уровням. Обычно это приводит и к тестам, которые спускаются по интерфейсу в том же порядке: сначала layout, затем section, затем локальная группа контролов, затем конкретный виджет.
Использование доступных имен как устойчивых опор
Когда я использую доступные имена в тестах, я стараюсь сначала опираться на семантику, которая уже существует в обычном HTML, и не добавлять ничего лишнего раньше времени.
Если в интерфейсе уже есть нативный элемент с достаточным смыслом, этого часто более чем достаточно. Например, если в UI есть:
<button>Сохранить</button>то видимый текст уже сам по себе является хорошей опорой и для accessibility, и для тестирования. В такой ситуации я не хочу добавлять ARIA просто ради тестов. Нативная семантика и существующий контент уже выполняют свою работу.
Для меня это важно потому, что я не воспринимаю ARIA как декоративный слой. Я использую ее тогда, когда она действительно закрывает реальный семантический дефицит:
- когда нативный HTML-элемент недостаточно ясно выражает характер взаимодействия
- когда кастомному виджету нужны роли и связи, которых plain markup сам по себе не дает
- когда labels, descriptions или controlled elements нужно сделать явными
Я стараюсь избегать ситуации, когда дополнительные accessibility-атрибуты добавляются только ради производства селекторов для QA. Обычно это создает второй искусственный слой поверх интерфейса вместо того, чтобы реально улучшать сам интерфейс.
При этом моя практика показывает, что действительно перегрузить интерфейс полезными accessibility-связями не так просто, если они отражают реальную структуру. Во многих случаях более богатая ARIA-разметка не усложняет, а наоборот снимает неоднозначность. Если на странице есть несколько похожих структур в разных местах, хорошие роли, имена и связи помогают сначала локализовать нужную область, а потом окончательно отсечь лишние совпадения.
Это особенно полезно для атрибутов вроде aria-controls или aria-owns. В простом примере выше контрол указывает на один элемент, но стандарт допускает и более сложные случаи. Даже если такие ситуации не самые частые, они достаточно реалистичны, чтобы более сильная relationship-aware модель тестирования себя оправдывала.
Предпочитаемый мной порядок довольно простой:
- Сначала использовать нативную HTML-семантику и видимый контент.
- Затем опираться на доступные имена, которые уже естественно получаются из разметки.
- И только потом добавлять ARIA там, где возможностей HTML уже недостаточно.
- После этого уже опирать тесты на получившийся контракт.
Такой подход держит тесты ближе к реальному интерфейсу продукта. Те же имена, которые помогают пользователю понять интерфейс, начинают помогать и тесту находить нужный элемент и описывать его корректно.
Примечания
- Тестирование через accessibility не означает, что один и тот же UI-сценарий нужно прогонять под всеми доступными локалями. Если цель теста не связана именно с локализацией, одной стабильной локали обычно достаточно. Я сам предпочитаю английскую (
en,en-GBилиen-US, в зависимости от того, что доступно), потому что так доступные имена остаются стабильнее, а корпус тестов проще поддерживать и анализировать.
Полезные ссылки
Вот короткий набор официальных материалов, которые полезно держать под рукой:
- W3C WAI Home как основная точка входа в экосистему accessibility-стандартов, туториалов и материалов по оценке
- WAI-ARIA Overview для понимания роли ARIA в web-accessibility
- ARIA Authoring Practices Guide как практическое руководство по семантике, keyboard behavior и доступным виджетам
- APG Patterns с конкретными паттернами компонентов и взаимодействий
- Accessible Name and Description Computation 1.2 про то, как на самом деле вычисляются доступные имена и описания
- WCAG 2 Overview как точка входа в основной accessibility-стандарт и связанные материалы
- WAI Page Structure Tutorial про landmarks, headings и структурную навигацию
- Evaluating Web Accessibility Overview с материалами по тестированию и оценке accessibility