Files
progcode/editorial/agent-rewrites/188.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 188,
"slug": "editorial-2022-10-mechanism-ui-tests",
"title": "Механика UI-теста: наблюдаемое состояние вместо случайной паузы",
"excerpt": "Как связать действие пользователя, переход состояния и проверяемый результат, чтобы UI-тест объяснял сбой, а не маскировал его ожиданием.",
"contentHtml": "<p>UI-тест нажимает «Сохранить», ждёт секунду и иногда падает на проверке результата. На быстрой машине он успевает увидеть новый экран. На занятой машине проверка срабатывает раньше. Если увеличить паузу, тест станет медленнее, но не умнее. Цена ошибки — флак в CI, повторный запуск и потеря доверия к зелёному результату. При настоящем дефекте команда получает сигнал поздно, потому что тест не знает, какого состояния он ждёт.</p>\n<p>Тезис простой: UI-тест должен ждать не время, а наблюдаемое состояние, которое означает завершение пользовательского действия. Страница должна назвать переход. Тест должен проверить это имя через доступную семантику или другой устойчивый контракт. Переход, наблюдение и assertion остаются отдельными слоями. Если их смешать, timeout превращается в замену модели.</p>\n<h2>Механизм: от действия к результату</h2>\n<p>Рассмотрим форму с текстовым полем и кнопкой отправки. Пользователь вводит текст. Состояние формы остаётся <code>editing</code>. После отправки приложение переходит в <code>submitting</code> и связывает попытку с идентификатором запроса. Только подтверждение этой попытки переводит форму в <code>saved</code>. Если подтверждение не пришло или относится к старой попытке, интерфейс показывает <code>recovery-required</code>, а не объявляет успех.</p>\n<p>В этой схеме есть четыре разных факта. Клик сообщает о намерении. Переход меняет модель. UI делает переход видимым. Assertion проверяет видимый результат. Клик не доказывает отправку. Исчезновение кнопки не доказывает сохранение. Истёкшая секунда не доказывает ни один из этих фактов.</p>\n<div class=\"table-scroll\"><table><caption>Что ждёт проверка после отправки формы</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Пример</th><th scope=\"col\">Чего он не доказывает</th><th scope=\"col\">Нужная проверка</th></tr></thead><tbody><tr><td>Действие</td><td>Пользователь нажал «Сохранить»</td><td>Ответ принят</td><td>Проверить переход в pending</td></tr><tr><td>Переход</td><td><code>submitting</code> и <code>request-01</code></td><td>Результат виден человеку</td><td>Проверить guard и связь ответа с запросом</td></tr><tr><td>Наблюдение</td><td><code>role=status</code>, имя «Сохранение выполняется»</td><td>Сеть работала без ошибок</td><td>Проверить доступный результат сценария</td></tr><tr><td>Ответ</td><td>Подтверждение с тем же идентификатором</td><td>Любой экран с текстом «Готово» корректен</td><td>Проверить переход в <code>saved</code></td></tr></tbody></table></div>\n<h2>Почему одной переменной loading недостаточно</h2>\n<p>Флаг <code>isLoading</code> отвечает только на вопрос о занятом состоянии. Он не различает первую и вторую попытку, не защищает от двойной отправки и не объясняет, что делать после ошибки. Для асинхронной формы нужен корреляционный признак. В учебном примере это <code>requestId</code>. Первый submit получает <code>request-01</code>. Ответ с <code>request-00</code> считается устаревшим и не меняет экран.</p>\n<p>Тот же guard закрывает повторный submit. Пока состояние равно <code>submitting</code>, второй клик не создаёт новый запрос. В реальном интерфейсе кнопка может стать disabled, но смысл правила должен жить в переходе состояния, а не только в DOM. Иначе другой обработчик или быстрый повторный клик обойдёт визуальную блокировку.</p>\n<p>Отрицательный путь важен не меньше happy path. Если сервер не подтвердил запрос, форма не должна показывать «Сохранено». Она должна сохранить черновик, показать понятное восстановление и дать действие, предусмотренное продуктом: повторить, проверить результат или вернуться к редактированию. Конкретная политика зависит от системы. Тест обязан проверить, что ложного успеха нет.</p>\n<h2>Учебный пример: переходы без браузера</h2>\n<p>Следующий код ограничен локальной моделью. Он не открывает страницу, не отправляет HTTP-запрос и не показывает результат реального продукта. Его задача — сделать инварианты видимыми до написания browser-теста: повторная отправка блокируется, старый ответ игнорируется, отсутствие подтверждения ведёт к восстановлению.</p>\n<pre><code>const state = { phase: 'editing', text: 'Согласовать условия', requestId: null };\n\nfunction submit(current) {\n if (current.phase === 'submitting') {\n return { ...current, event: 'submit-blocked' };\n }\n return { ...current, phase: 'submitting', requestId: 'request-01' };\n}\n\nfunction acknowledge(current, id) {\n if (current.phase !== 'submitting' || id !== current.requestId) {\n return { ...current, event: 'stale-acknowledgement' };\n }\n return { ...current, phase: 'saved', event: 'saved' };\n}\n\nfunction fail(current, id) {\n if (current.phase !== 'submitting' || id !== current.requestId) {\n return { ...current, event: 'stale-failure' };\n }\n return { ...current, phase: 'recovery-required', event: 'recovery' };\n}</code></pre>\n<p>Код намеренно не решает transport, retry и доступность. Он фиксирует границу переходов. После него browser-тест может проверять факты интерфейса: появился статус отправки, второй submit не изменил попытку, корректный ответ показал сохранение, а устаревший ответ не перекрыл новую форму.</p>\n<figure><img src=\"/assets/editorial/2022/ui-tests-2022-observation-route.svg\" alt=\"Схема переходов UI-теста: intent переходит в submitting, подтверждение с совпадающим request id ведёт в saved, устаревший ответ и повторная отправка блокируются, отсутствие подтверждения ведёт в recovery-required\" loading=\"lazy\" /><figcaption>Схема отделяет действие, переход, наблюдение и отрицательный путь. Это учебная модель, а не trace браузера и не результат production-прогона.</figcaption></figure>\n<h2>Как выглядит browser assertion</h2>\n<p>После того как страница получила понятный контракт, assertion выражает пользовательский факт. В Playwright пример может выглядеть так:</p>\n<pre><code>await page.getByRole('button', { name: 'Сохранить' }).click();\nawait expect(page.getByRole('status'))\n .toHaveText('Сохранение выполняется');\n\n// Контролируемый ответ тестового окружения приходит здесь.\nawait expect(page.getByRole('status'))\n .toHaveText('Изменение сохранено');</code></pre>\n<p>Вызов <code>toHaveText</code> ждёт условие до установленного timeout. Это полезно только тогда, когда условие связано с переходом, который важен пользователю. Проверка «кнопка исчезла» может пройти из-за закрытия модального окна, ошибки рендера или смены маршрута. Она не заменяет статус результата.</p>\n<p>Выбирайте locator по смыслу. Роль и имя подходят для состояния, которое должен распознать пользователь. <code>data-testid</code> уместен для технического узла, у которого нет пользовательской семантики. Ни один locator не исправит отсутствующий contract. Если экран не различает pending, success и recovery, автоматизация будет угадывать состояние по косвенным признакам.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Карта диагностики нестабильной UI-проверки</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После click стоит <code>wait(1000)</code></td><td>Нет названного pending-состояния</td><td>Найти видимый результат до и после отправки</td><td>Добавить status и assertion на него</td></tr><tr><td>Тест ждёт исчезновения кнопки</td><td>DOM-признак подменяет бизнес-результат</td><td>Проверить, что будет при server error</td><td>Утвердить success и recovery отдельно</td></tr><tr><td>Два быстрых click создают два эффекта</td><td>Guard живёт только в UI</td><td>Повторить submit в состоянии <code>submitting</code></td><td>Запретить переход и проверить один request id</td></tr><tr><td>Поздний ответ показывает старый успех</td><td>Ответ не связан с попыткой</td><td>Передать устаревший <code>requestId</code></td><td>Игнорировать ответ или отправить его в согласованный recovery-путь</td></tr><tr><td>После ошибки нечего повторить</td><td>Форма очистила черновик до подтверждения</td><td>Проверить текст и snapshot после failure</td><td>Сохранить черновик и назвать действие восстановления</td></tr></tbody></table></div>\n<h2>Порядок действий</h2>\n<ol><li><strong>Запишите симптом.</strong> Сохраните текст падения, действие перед ним и текущий assertion. Не увеличивайте timeout до диагностики.</li><li><strong>Назовите состояния.</strong> Опишите pending, success и recovery словами, которые понимает пользователь.</li><li><strong>Назначьте владельца перехода.</strong> Укажите, какой обработчик создаёт request id, кто принимает ответ и кто меняет видимый статус.</li><li><strong>Проверьте отрицательный путь.</strong> Подайте пустой ввод, повторный submit, устаревший ответ и отсутствие подтверждения.</li><li><strong>Добавьте локальные проверки модели.</strong> Убедитесь, что guard, correlation и rollback не зависят от времени и DOM.</li><li><strong>Настройте контролируемый browser-вход.</strong> В тестовом окружении зафиксируйте разрешённый способ получить success и failure. Не выдавайте локальную модель за e2e.</li><li><strong>Поставьте assertion на observable state.</strong> Проверяйте role, имя и текст результата. Timeout оставьте ограничителем, а не условием успеха.</li><li><strong>Проверьте обратимость.</strong> Если новый contract расходится с UX, откатите его вместе с assertion. Не оставляйте паузу как постоянный обход.</li></ol>\n<h2>Ограничения</h2>\n<p>Модель не знает о re-render, планировщике фреймворка, локализации, авторизации, нескольких вкладках и фактической доставке ответа. <code>request-01</code> — учебное имя, а не требование к production-протоколу. В реальной системе поздний ответ может требовать журнала, повторного чтения данных или server reconciliation, а не молчаливого игнорирования.</p>\n<p>Роль <code>status</code> или <code>alert</code> нельзя добавлять только ради теста. Доступное сообщение должно соответствовать срочности и поведению интерфейса. Источник текста, локализация и фокус требуют отдельной проверки. UI-тест подтверждает выбранный contract, но не сертифицирует всю доступность страницы.</p>\n<p>Учебный пример не доказывает, что флак исчез, не измеряет длительность CI и не сообщает production-результаты. Для такого вывода нужны воспроизводимые запуски, версия браузера и runner, окружение, история падений и правило сравнения. Без этих данных корректно утверждать только то, что assertion теперь ждёт названное состояние.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, если один сценарий проходит по четырём наблюдаемым веткам: pending появляется после действия, корректное подтверждение показывает success, повторная отправка не создаёт второй переход, а устаревший или отсутствующий ответ не показывает ложный успех. Для каждой ветки есть assertion на public UI contract. В коде нет произвольной паузы, которая заменяет отсутствующее состояние. Локальные проверки модели и browser-тест имеют явно описанные границы.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://playwright.dev/docs/test-assertions\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright: Assertions</a> — официальная документация по auto-retrying assertions и условиям, которые проверяются до timeout.</li><li><a href=\"https://www.w3.org/TR/webdriver/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WebDriver</a> — действующая спецификация интерфейса управления браузером; она не определяет смысл прикладного состояния страницы.</li><li><a href=\"https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html\" target=\"_blank\" rel=\"noopener noreferrer\">W3C WCAG 2.2: Status Messages</a> — официальное описание границы для сообщений о результате, которые не должны незаметно менять контекст пользователя.</li></ul>"
}