DarkRiDDeR13 мин

Когда доступный control ломается после правки: диагностика и безопасный откат

FrontendТестирование

После небольшой правки control может выглядеть лучше и стать менее доступным. Например, новая панель красиво закрывается после save, но focused element удаляется; статус виден в toast, но код не оставляет проверяемого состояния; invalid choice подсвечивается, но уже перезаписала сохранённое значение. Симптомы похожи на «мелкие UI-детали», пока человек не пытается выполнить путь без мыши. Цена — не только один дефект: следующий рефакторинг получает неявное состояние, которое невозможно безопасно отменить.

Диагностику полезно начать не с перебора aria-атрибутов, а с вопроса, в какой момент расходятся два представления одного действия. Есть draft value, committed value, visual marker, declared semantic state, focus route и сообщение результата. Если все шесть меняются в разных местах, «поправить доступность» превращается в широкий рискованный diff. Эта статья сужает проверку до учебной панели. В ней нет production-метрик, ручного screen-reader результата или реального browser trace; всё, что похоже на фокус и объявление, помечено как модельное значение.

Пять причин похожего симптома

Диагностика разрыва control contract
НаблюдениеВероятная причинаМинимальный фактОбратимое действие
После закрытия нет понятной точки продолженияreturn target не определён до unmountnamed focus route не содержит triggerсохранить trigger как return target и проверить его до close
Ошибка подсвечена, но сохранённый выбор изменилсяdraft и committed state склееныinvalid branch меняет saved valueразделить значения и остановить commit до valid branch
Видна галочка, но semantic state старыйCSS и declared state имеют разные ownerодна операция меняет только classвывести оба значения из одного source of truth
Есть toast, но нет проверяемого статусатекст создаётся вне contractнет named result stateсформировать declared status до platform validation
Undo возвращает не то или работает бесконечноprior state не сохранён либо не очищенpending undo не связан с commitсохранить один prior value и очистить его после undo

Не начинайте с «проверим всё»

Большой accessibility audit нужен, но он плохо отвечает на локальный вопрос «почему после save исчез фокус». Сначала нужно построить временную последовательность одного действия. До save есть trigger, открытая панель и draft. Во время save выбирается valid branch, фиксируется prior saved value, формируется declared status, панель закрывается, route возвращается на trigger. В invalid branch порядок другой: commit не происходит, панель остаётся открытой, route идёт к полю исправления. Если код не может показать эту последовательность, проверять версию aria-атрибута ещё рано.

Такой порядок переносит привычную практику 2022 года из производительности и SSR: не считать два исхода одинаковыми только потому, что оба показывают новый экран. Здесь valid save и invalid input могут иметь одинаковую раскладку, но отличаются полномочием менять saved value. У valid save есть обратимый снимок. У invalid branch его быть не должно, потому что ничего не было сохранено. Это маленький контракт, который удобно защитить fixture до запуска ручных сценариев.

Fixture проверяет rollback, но не симулирует браузер

Модель открывает панель, выбирает sms, применяет выбор и вызывает undoLastSave. После undo оба значения снова email, а pendingUndo очищен. Повторный undo получает nothing-to-undo. Это важно для безопасной правки: тест не говорит, что в продукте есть кнопка «Отменить» или что она доступна с клавиатуры. Он фиксирует более узкое утверждение — сохранённое изменение не теряет предыдущее значение и не откатывается бесконечно.

const panel = createTeachingPanel();
openPanel(panel);
chooseChannel(panel, "sms");
applyChoice(panel);
undoLastSave(panel);

console.log(panel.state.savedChannel); // "email"
console.log(panel.state.pendingUndo); // null

// Undo restores the teaching value once; it is not a browser rollback.

Можно запустить fixture целиком командой из первой статьи. Она не импортирует React, не создаёт element, не шлёт key event и не записывает звук. Поля focus и declaredAnnouncement не надо называть результатом accessibility tree. В них записаны ожидаемые точки проекта. Это делает тест строгим в том, что он умеет проверить, и честным в том, чего в нём нет.

Диагностическая схема учебной панели: invalid value останавливает commit и ведёт к notification-email; valid save фиксирует prior value, возвращает маршрут к trigger и создаёт declared status; undo один раз восстанавливает prior value. Отдельной рамкой отмечено, что DOM и фактическое объявление не наблюдались.
Диаграмма разделяет две ветки, которые не следует объединять ради одного обработчика: исправление ошибки и подтверждённое обратимое сохранение.

Проверка реального компонента: собрать факты в правильном порядке

  1. Симптом. Зафиксируйте один наблюдаемый сбой: фокус потерян после close, state не соответствует визуальному выбору или ошибка не даёт следующего шага. Не называйте причину заранее.
  2. Контракт. Выпишите name, role, selected/expanded state, draft value, committed value, trigger, invalid target и return target. Это минимальный список, а не полный audit страницы.
  3. Проверка кода. Найдите место, где каждая часть меняется. Особенно проверьте, что invalid branch выполняется до commit и что return target не удаляется до записи маршрута.
  4. Проверка модели. Запустите node web/scripts/upgrade-2022-04.mjs --verify-fixture. Она должна сохранить invalid branch без изменения saved value и вернуть prior value одним undo.
  5. Проверка платформы. В реальном DOM пройдите согласованный keyboard scenario, затем проверьте семантику и выбранные assistive-technology combinations. Запишите среду и результат отдельно; модель не может заменить эту работу.
  6. Действие и откат. Вносите одно изменение: native host, единый state owner, return target или status boundary. Перед rollout оставьте способ вернуть прежний state/markup и повторите тот же сценарий.

Откуда берётся ложная уверенность

Ложная уверенность появляется, когда один артефакт получает чужую работу. Snapshot DOM не доказывает клавиатурную последовательность. Скринкаст с мышью не доказывает name/role. Валидный aria-live не доказывает конкретную фразу, произнесённую конкретной технологией. И наоборот, успешный ручной проход одного сочетания не доказывает, что state ownership не сломается после следующего refactor. WCAG разделяет проверяемые критерии, а практики ARIA описывают поведение паттернов; проекту всё равно нужно выбрать доказательство для каждого риска.

Полезная запись review выглядит скромно: «в fixture invalid branch не меняет committed value; в таком-то браузере команда вручную проверила такой-то сценарий; остальные среды не проверялись». Она не так эффектна, как фраза «полная доступность», зато следующему разработчику ясно, что можно воспроизвести и где находится неизвестное. Для автора 2022 года это продолжение технической дисциплины: измерение или тест имеют ценность ровно в пределах своих входов.

Undo-safe изменение не равно глобальному rollback

Учебный undo хранит одну строку prior value. Он не учитывает серверные конфликты, несколько вкладок, историю браузера, права, offline и долгоживущие черновики. Если реальный save вызывает сеть, отмена должна иметь отдельный контракт: отменяем ли мы запрос, создаём ли compensating change, как показываем конфликт и кто владеет retry. Нельзя переносить маленькую fixture в эти условия без новых тестов. Её роль — не решить распределённую задачу, а не дать локальному control потерять собственный предыдущий state.

Точно так же return focus не всегда обязан идти на trigger. Если после save появляется новый результат, пользователь может ожидаемо перейти к нему; если trigger исчезает по изменённому route, нужен другой named target. Важно не конкретное слово preferences-trigger, а решение до unmount и проверка его причины. В модели trigger выбран потому, что панель закрывается без смены страницы. Другая структура должна назвать свою предпосылку, а не копировать route как ритуал.

Следующий шаг для команды

Начните с одного компонента, который часто открывает и закрывает состояние: menu button, dialog trigger, selection control или save panel. Составьте короткую матрицу из этой статьи и добавьте к PR два правила: invalid branch не коммитит state; успешный branch имеет named focus target и обратимое значение, если продукт обещает отмену. Затем выберите одну платформенную проверку, которую реально можно повторить. Не нужно ждать общей a11y-платформы, чтобы перестать выпускать control без контракта.

После того как один сценарий стабилен, расширяйте его осторожно. Следующая статья может связать этот contract с ошибками формы: там появятся асинхронный ответ, несколько полей и приоритет сообщений. Но сначала нужно, чтобы текущий элемент различал draft, commit и отмену. Без этой границы новая сложность лишь добавит больше уведомлений вокруг того же скрытого состояния.

Историческая граница апреля 2022

Ссылки ведут на датированные документы, доступные до апреля 2022 года: WCAG 2.1 Recommendation 2018 года, APG 1.2 Group Note ноября 2021 года и WAI-ARIA 1.2 Candidate Recommendation Draft от 8 декабря 2021 года. В тексте нет отчёта о проведённом исследовании, реальном дефекте или произнесённой фразе screen reader. Все numbers, labels и ветки fixture относятся только к учебному контракту этого пакета.

Проверяемые источники