DarkRiDDeR13 мин

Механика UI-проверки: сделать состояние наблюдаемым

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

Проблема теста, который ждёт секунду потому, что после клика ему нечего наблюдать, обычно скрыта в самой странице. Внутри меняется несколько флагов, затем кто-то закрывает модалку, а итог пользователю виден только случайно. Цена — флак и дорогая диагностика: лог показывает click, но не показывает, какой transition не состоялся и почему.

Механика ниже строит один узкий мост: domain transition создаёт named visible state, а assertion проверяет именно его. Это не настоящий Playwright run и не реализация WebDriver. Локальная fixture специально не содержит таймеров, selector-ов, DOM или HTTP, чтобы можно было проверить reason перехода и rollback без истории конкретного браузера.

Три слоя, которые нельзя склеивать

Первый слой — intent: пользователь ввёл текст или нажал submit. Второй — transition: state machine решила, что переход допустим, и записала request id. Третий — observation: экран получил status либо alert с доступным именем. Когда тест сразу читает внутренний флаг второго слоя, он перестаёт проверять пользовательский результат. Когда он ждёт только время, он не проверяет ни один слой. Нужен явный третий слой, связанный с переходом.

Разделение ответственности
СлойХранитНе должен утверждатьПроверка
Intentввод и clickчто сохранение принятособытие доступно пользователю
Transitionphase и request idчто браузер уже отрисовал результатguard и matching acknowledgement
Observationrole, name, testStateчто сеть работалаassertion пользовательского сценария
Транспортзапрос и replyсмысл UI без mappingотдельный API-contract

Почему request id входит в учебную модель

Если confirmation приходит после submit, модели недостаточно boolean `isLoading`. Ей нужно знать, какую попытку она ждёт. Первый `submit` создаёт `request-01`; acknowledgement с `request-00` отвергается. После rollback счётчик не возвращается назад: повторный submit получает `request-02`, а запоздалый reply от первой попытки остаётся чужим. Это не утверждение о формате production ID и не требование передавать именно такое поле по сети. Это способ записать invariант: старый reply не может признать текущий transition.

Дальше работает guard. Повторный submit во время `submitting` возвращает `submit-guard-active` и не создаёт второй request id. Такой исход важен именно как наблюдаемая граница: кнопка в реальном UI может стать disabled, но смысл должен жить не только в button. Иначе пользовательский сценарий будет зависеть от timing отрисовки, а компонент-автоматизация найдёт обходной путь.

Пример: local fixture проверяет owner перехода

Ниже нет `page.click`, `locator` или sleep. Вызовы создают намерение, планируют transition и вручную передают acknowledgement. Проверка не измеряет задержку: она смотрит на shape evidence и на `testState`. Если заменить order событий, stale acknowledgement не сдвигает сценарий; если сделать rollback, состояние возвращается к snapshot. Это удобный first gate перед настоящим UI.

import { runUiTestsFixture } from './upgrade-2022-10.mjs';
const report = runUiTestsFixture();
if (!Object.values(report.assertions).every(Boolean)) throw new Error('fixture contract failed');
console.log(report.evidence.planned.observation.testState); // 'submitting'
console.log(report.evidence.accepted.observation.name); // 'Изменение сохранено'

После failure fixture возвращает pre-submit snapshot: `editing` с черновиком, без request id и без сохранённого результата. При этом счётчик запросов не откатывается. Поэтому повторный submit получает новый id, а поздний `request-01` не может подтвердить вторую попытку. Это не метрика надёжности и не готовая политика production rollback. Это узкая защита от модели, которая после возврата случайно принимает старый reply за текущий успех.

Схема разделения UI-проверки: intent submit создаёт transition submitting с request id; accessible status становится наблюдением; acknowledgement с совпадающим id переводит в saved; stale reply и повторный submit не меняют переход; recovery выводится отдельным alert.
Рисунок отделяет модель переходов от инструмента автоматизации и не показывает настоящий DOM или WebDriver-сеанс.

Как писать browser assertion после контракта

После модели assertion должен выражать пользовательский факт, например: после действия есть status с именем «Отправка ожидает подтверждения», а после контролируемого ответа — status «Изменение сохранено». В Playwright-документации v1.27.0 retrying assertion повторяет проверку выбранного условия до timeout. Поэтому timeout — ограничитель ожидания условия, не само условие. Если condition выбран как «кнопка где-то исчезла», инструмент добросовестно ждёт неправильный факт.

Стабильный selector допустим, если он не подменяет семантику. Для чисто технического контейнера можно применять `data-testid`; для результата сценария лучше сначала искать роль и имя, которые уже нужны человеку. Это не абсолютное правило: составной виджет, локализация и визуально скрытый status потребуют отдельного решения. Важно зафиксировать, какой contract должен пережить рефакторинг, а какой selector является внутренней деталью.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. В отчёте есть timeout либо sleep между click и проверкой, но не видно нужного состояния.
  2. Причина. Intent, transition и observation склеены: тест знает click, но не знает owner результата.
  3. Проверка. Найдите phase, request id и текст/role результата. Если одного нет, тесту нечего ждать корректно.
  4. Действие. Добавьте named status для pending/saved и alert для recovery; свяжите их с transition в одном месте.
  5. Проверка guard. Локально подтвердите duplicate submit и stale reply. Fixture не заменяет browser тест.
  6. Rollback. Откатывайте весь новый contract вместе с тестом, если доступное состояние расходится с утверждённой UX-копией; не возвращайте sleep как постоянный патч.

Ошибки внедрения

Первая ошибка — назвать все состояния `loading`. Пользователь не отличит ожидание подтверждения от повторной попытки, а тест не отличит новый запрос от старого reply. Вторая — очищать форму сразу после click. Тогда rollback нечего восстанавливать. Третья — делать success индикатор побочным эффектом network callback без проверки request id. В такой схеме поздний callback способен сообщить о старой операции поверх нового редактирования.

Четвёртая ошибка — добавить `aria-live` или role только ради теста. WCAG не даёт универсальный шаблон для всех сообщений, поэтому роль должна соответствовать срочности и поведению интерфейса. Если команда ещё не выбрала доступную подачу результата, сначала нужно решить UX, а затем автоматизацию. Тест не должен легализовать сообщение, которое пользователю непонятно или чрезмерно навязчиво.

Ограничение и следующий шаг

Учебная модель не знает re-render, framework scheduler, локализацию, авторизацию, несколько вкладок и фактическую доставку reply. Она не говорит, что любой stale reply следует молча отбросить: production может потребовать журнал, server reconciliation или повторное чтение данных. Фиксированная строка request id и ручной acknowledgement существуют только для детерминированной fixture.

Следующий шаг — взять один настоящий тест и нарисовать его timeline без времени: intent, pending observation, controlled response, final observation. Отдельно обозначьте fail path. Если на этой схеме нет наблюдаемого условия после click, сначала добавьте contract страницы. Только потом заменяйте sleep на auto-retrying assertion выбранного инструмента.

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

Техническая граница построена на immutable Playwright documentation commit v1.27.0, WebDriver Working Draft от 25 октября 2022 года и WCAG 2.1 Recommendation. Ни один из документов не является доказательством того, что локальная fixture открывала страницу или что конкретный UI обладает заявленной доступностью.

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

  • Playwright: test assertions, commit af0d2936 (7 октября 2022) — неизменяемый исходный документ версии v1.27.0, опубликованной 7 октября 2022 года. Описывает retrying assertion до условия или timeout; статья не запускает Playwright.
  • W3C WebDriver, Working Draft 25 октября 2022 года — датированный Working Draft, доступный в октябре 2022 года. Он задаёт remote-control interface, но не определяет смысл прикладного состояния страницы.
  • W3C WCAG 2.1, Recommendation 5 июня 2018 года — нормативная Recommendation, доступная к октябрю 2022 года. Используется только для границы: видимое состояние нуждается в доступной семантике, а не в одном визуальном эффекте.