Скачивание файла, всплывающее окно, ответ сервера - всё это события, которые случаются в ответ на действие, и здесь легко создать гонку. Наивный код нажимает кнопку, а потом пытается получить событие - но между нажатием и попыткой событие уже могло произойти и пройти незамеченным. Правильный порядок противоположен интуиции: сначала начать ждать событие, потом выполнить действие, и только затем дождаться результата. Этот сдвиг снимает целый класс нестабильности.
Скачивание - канонический пример. Сначала создают промис ожидания через page.waitForEvent('download'), не дожидаясь его, затем нажимают кнопку, запускающую загрузку, и только после этого делают await промиса. Так между действием и ожиданием нет щели, в которую событие могло бы проскользнуть. Полученный объект download несёт метаданные - предложенное имя файла - и умеет сохранить содержимое туда, куда скажет тест.
Проверять скачанный файл удобно через его свойства и содержимое. suggestedFilename() возвращает имя, которое сервер предложил браузеру, - его сверяют с ожидаемым. saveAs сохраняет файл, причём в путь, выданный testInfo.outputPath, - это каталог артефактов конкретного теста, изолированный от других. Так скачанный файл и проверяется, и остаётся приложенным к результату теста для последующего разбора, не мешая параллельным сценариям.
Загрузка файла проще и не требует настоящего файла на диске. setInputFiles принимает описание файла прямо в коде - имя, mime-тип и содержимое буфером, - и передаёт его полю загрузки. Это удобно и детерминированно: тест не зависит от наличия файла в репозитории и его пути, а формирует нужный файл на месте. Для аватара, документа, изображения достаточно собрать буфер и отдать его полю по его подписи.
Полезно один раз увидеть оба паттерна рядом - скачивание с ожиданием события до действия и загрузку через буфер. Ниже - download без гонки и upload аватара. К этой паре возвращаются, когда в сценарии появляется файловое взаимодействие: сначала решают, кто инициирует событие и когда начать его ждать, а уже потом пишут само действие.
Тот же принцип "ждать до действия" распространяется на другие события. Всплывающее окно (новая вкладка) ловят через ожидание события 'page' на контексте до клика, открывающего его. Конкретный сетевой ответ - через waitForResponse с предикатом до действия, вызывающего запрос. Диалог выбора файла, попап платёжки, новое окно OAuth - у всех одна форма: промис ожидания, затем действие, затем await. Разная природа события, одна структура кода.
Причина, по которой порядок именно такой, - в природе событий. Событие не хранится и не ждёт, пока его спросят: оно происходит один раз в свой момент. Если начать слушать после того, как оно уже случилось, слушатель не увидит ничего и будет ждать до timeout. Поэтому подписку ставят заранее - до действия, которое событие породит, - чтобы гарантированно поймать его, когда оно наступит, а не гоняться за уже прошедшим.
Типичные провалы событийных сценариев сводятся к перевёрнутому порядку. Нажать кнопку скачивания, а потом начать ждать download - гонка, дающая случайные таймауты. Ждать popup или response после клика, который их уже вызвал, - тот же пропущенный момент. И проверять файл по пути в репозитории вместо буфера и outputPath - хрупкая зависимость от окружения. Начинайте ждать событие до действия, порождающего его, - и файловые и оконные сценарии перестанут флакать.
// Скачивание без гонки: ждать событие ДО действия
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Скачать отчёт' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('report.csv')
await download.saveAs(testInfo.outputPath('report.csv'))// Загрузка без файла на диске: содержимое буфером
await page.getByLabel('Загрузить аватар').setInputFiles({
name: 'avatar.png',
mimeType: 'image/png',
buffer: testPngBuffer,
})