In Playwright, how do you attach a file when the page renders no file input to target?
answer
- No input to target, so listen instead
- The page asks the browser for a picker
- Start waiting before the click
- setFiles takes the same arguments
- isMultiple reports what the input allows
basics
~10 sWait for the page's filechooser event. Start waiting before clicking the control, then call setFiles on the FileChooser Playwright hands you; this works even when script creates the input and discards it.
solid answer
~40 sSome upload controls build an `<input type=file>` in JavaScript, click it, and throw it away, so there is nothing stable for `locator.setInputFiles()` to point at. Playwright emits a `filechooser` event whenever the page asks the browser to open a picker, and gives you a `FileChooser` with `setFiles()`, `isMultiple()` and `page()`. The pattern is to create `page.waitForEvent('filechooser')` **before** the click and await it afterwards -- the event fires during the click, so a promise created after the awaited click subscribes too late and times out. `fileChooser.setFiles()` takes the same arguments as `locator.setInputFiles()`: a path, an array, or `{ name, mimeType, buffer }`. Prefer `setInputFiles()` whenever a real input exists; it is one call with no event ordering to get wrong.
code
typescript · 18 linesimport { test, expect } from '@playwright/test';
test('attach a screenshot through a scripted picker', async ({ page }) => {
await page.goto('/issues/ISS-482');
const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Attach file' }).click();
const chooser = await chooserPromise;
expect(chooser.isMultiple()).toBe(false);
await chooser.setFiles({
name: 'repro.png',
mimeType: 'image/png',
buffer: Buffer.from('89504e470d0a1a0a', 'hex'),
});
await expect(page.getByText('repro.png')).toBeVisible();
});go deeper
Know that a second route exists: when there is no file input to target, Playwright surfaces the browser's picker request as a page event the test can answer with setFiles.
Explain the ordering rule and why it exists -- the event fires during the click, and waitForEvent only catches events that arrive after it starts listening, so the wait must be created first.
Diagnose from the timeout. A chooser event that never arrives is evidence about the control, not about timing, and you should be able to list what else it could have been doing.
Decide when a page that is this hard to drive is a testability problem worth fixing in the application rather than a puzzle to keep solving in the suite.
## Why there is sometimes nothing to target `locator.setInputFiles()` needs an `<input type=file>` in the DOM. Plenty of upload controls do not have one you can reach: a script creates the input on demand, calls `click()` on it, and discards it once a file is chosen. Others wrap the input in a component that re-renders it on every interaction. In both cases a locator either finds nothing or finds an element that is gone by the time the call runs. What those controls do have in common is that they ask the **browser** to open a file picker. Playwright intercepts that request and turns it into a page event instead of an operating-system dialog. ## The filechooser event `page.waitForEvent('filechooser')` resolves with a `FileChooser`: - `fileChooser.setFiles(...)` -- takes the same arguments as `locator.setInputFiles()`: a path, an array of paths, or `{ name, mimeType, buffer }` objects. - `fileChooser.isMultiple()` -- whether the underlying input accepts more than one file, which is a genuine assertion about the application's markup. - `fileChooser.page()` -- the page that opened it. - `fileChooser.element()` -- the input behind the chooser, rarely needed. Nothing native ever appears, so nothing blocks. The event simply says "the page would have shown a picker here" and lets the test answer it. ## Ordering is the whole trick 1. `const chooserPromise = page.waitForEvent('filechooser');` 2. Click the control that opens the picker. 3. `const chooser = await chooserPromise;` 4. `await chooser.setFiles(...)` and then assert on the application's response. The event fires while the click is being handled. `waitForEvent` only catches events that arrive **after** it starts listening, so a promise created on the line after an awaited click subscribes too late and times out with a message about waiting for the event -- which reads like the application never opened a picker, even though it did. This single ordering rule accounts for most of the flakiness people attribute to uploads. ## Choosing between the two routes | situation | use | | --- | --- | | a stable file input exists in the DOM | `locator.setInputFiles()` | | the input is built and discarded by script | the `filechooser` event | | the input exists but is re-rendered constantly | the `filechooser` event | | you need to assert whether multiple files are allowed | the `filechooser` event, via `isMultiple()` | Default to `setInputFiles()`. It is one call, it has no event to race, and it fails with an error that names the element rather than a timeout that names an event. The chooser route is the fallback for pages that leave you nothing to point at, not a general upgrade. ## When the event never arrives A timeout waiting for `filechooser` is information, not just a failure. It means the control the test clicked did not ask the browser for a picker. Common causes: - The zone accepts dropped files only, and the visible prompt is decorative text rather than a picker trigger. - The upload is a second step: the first click opens a modal, and only a control inside it opens the picker. - The click landed on a wrapper that swallows the event, so the real trigger was never activated. - The page uses a different browser capability for file access altogether, in which case no chooser request is made and no amount of waiting will produce one. The diagnosis is the same in each case: watch what the page does when a human clicks, and confirm that a picker is what it actually opens. ## Assert past the attachment Answering the chooser proves a file was handed over, not that the issue tracker accepted it. Follow it with an assertion the user would recognise -- the filename appearing in the comment composer's attachment list, a thumbnail rendering, the submit button enabling. `setFiles()` resolves once the file reaches the input; the upload request that follows is the application's business and needs its own assertion.
- Why create the waitForEvent promise before the click rather than after?The filechooser event fires while the click is being handled. `page.waitForEvent()` only catches events arriving after it starts listening, so a promise created below an awaited click subscribes too late and times out. Start the wait, click, then await -- the same ordering the download event needs.
- The click produces no filechooser event at all -- what does that tell you?That the control never asked the browser for a picker. A drop-only zone, an upload gated behind a second step, or a click landing on a wrapper that swallows it all produce this. Waiting longer cannot help; find out what the control actually does when a human clicks it.
saying these in an interview costs you the question
- Awaits the click first, then starts waiting for the chooser
- Claims Playwright types a path into the native OS dialog
- Adds a fixed sleep instead of awaiting the chooser event
- Uses the chooser event even when a stable input exists
- Thinks the event fires on page load rather than on the click