{ "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Первый слой — intent: пользователь ввёл текст или нажал submit. Второй — transition: state machine решила, что переход допустим, и записала request id. Третий — observation: экран получил status либо alert с доступным именем. Когда тест сразу читает внутренний флаг второго слоя, он перестаёт проверять пользовательский результат. Когда он ждёт только время, он не проверяет ни один слой. Нужен явный третий слой, связанный с переходом.
\n| Слой | Хранит | Не должен утверждать | Проверка |
|---|---|---|---|
| Intent | ввод и click | что сохранение принято | событие доступно пользователю |
| Transition | phase и request id | что браузер уже отрисовал результат | guard и matching acknowledgement |
| Observation | role, name, testState | что сеть работала | assertion пользовательского сценария |
| Транспорт | запрос и reply | смысл UI без mapping | отдельный API-contract |
Если 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 отрисовки, а компонент-автоматизация найдёт обходной путь.
Ниже нет page.click, locator или sleep. Вызовы создают намерение, планируют transition и вручную передают acknowledgement. Проверка не измеряет задержку: она смотрит на shape evidence и на testState. Если заменить order событий, stale acknowledgement не сдвигает сценарий; если сделать rollback, состояние возвращается к snapshot. Это удобный first gate перед настоящим UI.
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 за текущий успех.
После модели assertion должен выражать пользовательский факт, например: после действия есть status с именем «Отправка ожидает подтверждения», а после контролируемого ответа — status «Изменение сохранено». В Playwright-документации v1.27.0 retrying assertion повторяет проверку выбранного условия до timeout. Поэтому timeout — ограничитель ожидания условия, не само условие. Если condition выбран как «кнопка где-то исчезла», инструмент добросовестно ждёт неправильный факт.
\nСтабильный selector допустим, если он не подменяет семантику. Для чисто технического контейнера можно применять data-testid; для результата сценария лучше сначала искать роль и имя, которые уже нужны человеку. Это не абсолютное правило: составной виджет, локализация и визуально скрытый status потребуют отдельного решения. Важно зафиксировать, какой contract должен пережить рефакторинг, а какой selector является внутренней деталью.
Первая ошибка — назвать все состояния loading. Пользователь не отличит ожидание подтверждения от повторной попытки, а тест не отличит новый запрос от старого reply. Вторая — очищать форму сразу после click. Тогда rollback нечего восстанавливать. Третья — делать success индикатор побочным эффектом network callback без проверки request id. В такой схеме поздний callback способен сообщить о старой операции поверх нового редактирования.
Четвёртая ошибка — добавить aria-live или role только ради теста. WCAG 2.1, критерий 4.1.3, требует, чтобы сообщения о состоянии можно было определить программно и передать вспомогательным технологиям без перевода фокуса. Это не даёт универсального шаблона: роль должна соответствовать срочности и поведению интерфейса. Полный accessibility-аудит реализации остаётся отдельной задачей.
Учебная модель не знает 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Техническая граница построена на immutable Playwright documentation commit v1.27.0, WebDriver Working Draft от 25 октября 2022 года и WCAG 2.1 Recommendation. Ни один из документов не является доказательством того, что локальная fixture открывала страницу или что конкретный UI обладает заявленной доступностью.
\n