Import a CSV, export an invoice, attach a document. Almost every business app does both, and both are awkward to test for the same reason: the file picker is an operating system window, not part of the page. No browser automation tool can click it.
Uploads.
You attach the file to the input directly:
await page.getByTestId('import-input').setInputFiles('tests/fixtures/contacts.csv');
Playwright attaches it exactly as if the user had chosen it, and the app's upload handler runs normally.
If the button opens a picker rather than exposing an input, catch the chooser event. The ordering matters, the same way it did for new tabs:
const fileChooserPromise = page.waitForEvent('filechooser'); // listen first
await page.getByRole('button', { name: 'Import' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('tests/fixtures/contacts.csv');
To clear a selection, pass an empty array: setInputFiles([]).
Now the part that matters more than the syntax: what do you assert?
Not that the filename appears. That only proves the app read a name. Assert the effect:
await page.getByTestId('import-input').setInputFiles('tests/fixtures/contacts.csv');
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('contact-row')).toHaveCount(12);
await expect(page.getByText('Dana Fox')).toBeVisible();
Twelve rows, and a name that was inside the file. That fails if the parser breaks, which is the bug you actually fear.
Test the bad files too: a missing column, a file that is not a CSV, an empty file. That is where import bugs live and where tests rarely go.
Downloads.
Same ordering rule: listen, then click.
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('contacts.csv');
const path = await download.path();
download.path() gives you the file on disk, so you can read it and check the contents. Checking the filename alone proves very little; checking that the CSV contains the right rows proves the export works.
Two warnings from real projects.
Downloads behave differently headless across browsers. This is a common "passes locally, fails in CI" cause. If a download test is fragile on your build server, calling the export endpoint with request instead is often steadier and tests the same logic.
Do not test the file dialog itself. It belongs to the operating system. Your job stops at what your app does with the file.
Remember this: attach files directly, listen before you click, and assert on what the file did.
Reference: Downloads.