{ "index": 188, "slug": "editorial-2022-10-mechanism-ui-tests", "title": "Механика UI-проверки: сделать состояние наблюдаемым", "excerpt": "Как отделить transition, наблюдение и assertion, чтобы не лечить флак произвольным ожиданием.", "contentHtml": "

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

\n

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

\n

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

\n

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

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

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

\n

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

\n

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

\n

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

\n

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

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

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

\n
\"Схема
Рисунок отделяет модель переходов от инструмента автоматизации и не показывает настоящий DOM или WebDriver-сеанс.
\n

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

\n

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

\n

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

\n

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

\n
  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 как постоянный патч.
\n

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

\n

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

\n

Четвёртая ошибка — добавить aria-live или role только ради теста. WCAG 2.1, критерий 4.1.3, требует, чтобы сообщения о состоянии можно было определить программно и передать вспомогательным технологиям без перевода фокуса. Это не даёт универсального шаблона: роль должна соответствовать срочности и поведению интерфейса. Полный accessibility-аудит реализации остаётся отдельной задачей.

\n

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

\n

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

\n

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

\n

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

\n

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

\n

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

" }