Перейти к содержанию

Ожидания и утверждения

Наш каталог получает данные асинхронно. Заголовок доступен сразу, но список появляется после ответа /api/courses. Если проверка прочитает количество карточек слишком рано, она увидит ноль. Если ждать несколько секунд без условия, пример станет медленнее, а на другом компьютере паузы всё равно может не хватить.

В этом уроке научимся ждать конкретное состояние. Для наглядности задержим ответ каталога управляемым обещанием и отдельно рассмотрим этап загрузки. Полная версия находится в lesson-04/expected/lesson.spec.js архива. Здесь не приводятся фактически измеренные задержки: сценарии пока не запускались.

Действие готово и приложение готово

Нажатие кнопки требует, чтобы подходящий элемент был доступен для действия. Playwright выполняет проверки пригодности элемента перед операцией. Однако возможность нажать кнопку не означает, что приложение закончило получение данных. Это разные условия: состояние элемента и результат программы.

В нашем HTML поле поиска присутствует до ответа сервера. Технически его можно заполнить сразу, но сценарий исходного каталога должен дождаться четырёх курсов. Условие «Найдено курсов: 4» описывает готовность нужных данных. Оно осмысленнее, чем просто завершившийся переход к /, потому что открытие документа и заполнение карточек происходят в разное время.

Автоматические проверки действий описаны в Auto-waiting. Повторяющиеся ожидания состояния представлены в Assertions. В нашей лаборатории применяем их к двум отдельным вопросам: можно ли взаимодействовать с элементом и появился ли требуемый результат. Не следует переносить обещание одного механизма на другой.

Управляемый ответ вместо секундомера

Создадим обещание gate, которое завершится, когда сценарий вызовет release. Перехваченный запрос каталога будет ждать это обещание:

let release;
const gate = new Promise(resolve => { release = resolve; });
await page.route('**/api/courses', async route => {
  await gate;
  await route.fulfill({ status: 200, json: { courses } });
});

Массив courses импортируется из public/data.js. Перехват устанавливается до перехода, иначе первый запрос успел бы уйти без подмены. Когда страница выполняет fetch, обработчик маршрута удерживает ответ. В результате мы получаем состояние загрузки без догадки о скорости машины, сети или браузера.

Это учебная подмена одного запроса. Остальные ресурсы и /api/me продолжают обращаться к локальному серверу. Мы не задерживаем весь интернет и не используем настоящий ProfessorWeb. Правило совпадает с путём каталога, поэтому объяснение относится к известной части приложения. В следующем разделе откроем документ и освободим ответ после нужного наблюдения.

try {
  await page.goto('/');
  await expect(page.getByTestId('results-status'))
    .toHaveText('Загрузка каталога');
  release();
  await expect(page.getByTestId('results-status'))
    .toHaveText('Найдено курсов: 4');
  await expect(page.getByRole('list', { name: 'Курсы' })
    .getByRole('listitem')).toHaveCount(4);
} finally {
  release();
}

Этот фрагмент находится внутри полного test из архива. После открытия страницы ожидается первоначальный текст. Вызов release разрешает ответ. Далее утверждение ожидает замену текста и появление четырёх элементов. Блок finally освобождает ожидание даже при неудаче первого утверждения, чтобы оставшийся обработчик не продолжал бесконечно ждать закрываемый контекст.

Повторный вызов функции завершения обещания не меняет его уже полученный результат. Поэтому мы можем вызвать её и в основном ходе, и при завершении. Такое оформление показывает важную связь: управление задержкой требует управления её окончанием. Иначе учебный механизм диагностики сам создаст зависшие операции.

Почему чтение значения бывает преждевременным

Рассмотрим другой способ проверки: получить await locator.textContent() и сравнить строку через обычное expect(value).toBe(...). Такой код читает значение в один момент. Он полезен, когда именно это значение уже гарантированно готово, но не выражает ожидание будущего изменения DOM. Читатель мог увидеть состояние «Загрузка каталога», хотя нужный текст появился через мгновение.

Вариант await expect(locator).toHaveText(...) выражает другое намерение: дождаться соответствующего состояния в пределах установленного таймаута. В нашей конфигурации таймаут утверждения равен пяти секундам. Это предел ожидания, а не обязательная пауза. Если состояние наступит быстро, дальнейший код продолжит работу быстро; если не наступит, проверка завершится неудачей.

Увеличение таймаута не исправляет неверные данные или неправильный локатор. Если приложение всегда пишет «Найдено курсов: 3», оно не выполнит ожидание четырёх ни за пять, ни за двадцать секунд. Сначала необходимо понять, может ли условие вообще наступить при этих входных данных. Продолжительность ожидания обсуждается после этого, а не заменяет диагностику.

Не стоит также ждать полного отсутствия сетевой активности вместо результата каталога. В настоящем приложении могут работать опросы, аналитика или фоновые запросы. Наш критерий готовности связан с нужной частью интерфейса. Он не требует, чтобы все возможные сетевые процессы прекратились навсегда, и поэтому лучше отражает пользовательскую задачу.

Ошибочное условие и полезная неудача

Для самостоятельного разбора можно временно ожидать пять карточек вместо четырёх. При будущем запуске это утверждение должно завершиться неудачей после отведённого времени. Оно не свидетельствует о сбое страницы: сценарий сформулировал неверное ожидание. Важна возможность отделить неправильный тест от неправильного приложения, сверяя оба с исходной договорённостью.

Если текст статуса равен четырём, но элементов три, проблема уже относится к рассогласованию интерфейса. Возможно, одна запись не была отрисована или ошибка остановила создание списка. Комбинация утверждений позволяет увидеть, что именно расходится. Нельзя объявлять страницу готовой только по одному сигналу, если результат включает ещё и содержимое карточек.

Управляемая задержка используется здесь для изучения ожиданий, а не для имитации реальной производительности сервера. Мы не заявляем время загрузки, пропускную способность или устойчивость под нагрузкой. Для этих свойств потребовались бы другие условия и отдельные измерения. В текущем уроке достаточно показать переход между двумя известными состояниями.

Теперь сценарий умеет дождаться результата независимо от произвольной паузы. Во многих дальнейших проверках подготовка повторится: открыть страницу и дождаться четырёх курсов. Чтобы эта договорённость не дублировалась в каждом файле, следующим шагом оформим её как собственную фикстуру с явными данными и границей жизненного цикла.

Два предела ожидания

В конфигурации у теста есть общий предел времени, а у повторяющегося утверждения — собственный. Наш тест ограничен пятнадцатью секундами, утверждение обычно получает пять. Это не означает, что каждый шаг обязан занимать пять секунд. Сценарий может завершиться гораздо раньше, если все условия уже выполнены.

Если ожидание удерживаемого ответа забыли освободить, проблема создана самим примером. Увеличение общего таймаута только продлит неудачу. Блок finally поэтому относится к корректности подготовки, а не к удобству оформления. Он делает намеренную задержку ограниченным управляемым состоянием.

Для обычного сценария загрузки такая искусственная задержка не нужна. Мы используем её именно для наблюдения промежуточного текста. После изучения механизма общая фикстура будет просто ждать известный результат, сохраняя ту же модель готовности без дополнительного воротного обещания.