Downloading a file, a popup window, a server response - all of these are events that happen in reply to an action, and here it is easy to create a race. Naive code presses a button and then tries to get the event - but between the press and the attempt the event could already have happened and passed unnoticed. The right order is the opposite of intuition: first start waiting for the event, then perform the action, and only then await the result. This shift removes a whole class of instability.
Downloading is the canonical example. First you create a wait promise via page.waitForEvent('download') without awaiting it, then press the button that starts the download, and only after that await the promise. So there is no gap between the action and the wait through which the event could slip. The resulting download object carries metadata - the suggested filename - and can save the content wherever the test says.
Checking a downloaded file is convenient through its properties and content. suggestedFilename() returns the name the server suggested to the browser - you compare it with the expected. saveAs saves the file, into the path given by testInfo.outputPath - the artifact directory of this specific test, isolated from others. So the downloaded file is both checked and stays attached to the test result for later analysis, without interfering with parallel scenarios.
Uploading a file is simpler and does not require a real file on disk. setInputFiles takes a file description right in the code - a name, a mime type and content as a buffer - and passes it to the upload field. This is convenient and deterministic: the test does not depend on a file being in the repository and its path but forms the needed file on the spot. For an avatar, a document, an image it is enough to assemble a buffer and hand it to the field by its label.
It helps to see both patterns side by side once - downloading with the wait before the action and uploading via a buffer. Below is a download without a race and an avatar upload. You return to this pair when a file interaction appears in a scenario: first you decide who initiates the event and when to start waiting for it, and only then write the action itself.
The same "wait before the action" principle extends to other events. A popup window (a new tab) is caught by waiting for the 'page' event on the context before the click that opens it. A specific network response - via waitForResponse with a predicate before the action that triggers the request. A file chooser dialog, a payment popup, a new OAuth window - all share one shape: a wait promise, then the action, then await. Different event nature, the same code structure.
The reason the order is exactly this lies in the nature of events. An event is not stored and does not wait to be asked: it happens once at its moment. If you start listening after it has already happened, the listener sees nothing and waits until timeout. So the subscription is set in advance - before the action that will produce the event - to catch it for sure when it occurs, rather than chasing one already past.
The typical event-scenario failures come down to a reversed order. Pressing the download button and then starting to wait for the download - a race giving random timeouts. Waiting for a popup or response after the click that already triggered them - the same missed moment. And checking a file by a path in the repository instead of a buffer and outputPath - a fragile dependence on the environment. Start waiting for the event before the action that produces it - and file and window scenarios will stop flaking.
// Download without a race: wait for the event BEFORE the action
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download report' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('report.csv')
await download.saveAs(testInfo.outputPath('report.csv'))// Upload with no file on disk: content as a buffer
await page.getByLabel('Upload avatar').setInputFiles({
name: 'avatar.png',
mimeType: 'image/png',
buffer: testPngBuffer,
})