Files Users Actually Send — Upload, Download, and Content
Test the files users upload and download: type, size, name, and a look inside. An enabled button is not proof the file survived.
This post is part of the Playwright Essentials series. If you missed the previous post, read Part 13 — Browser Permissions Users Refuse first.
Introduction — The Button Was Enabled
The test clicks Upload, the button is enabled, the test passes. The file the user chose was a 40 MB CSV. The server rejected it. The UI said nothing. The suite never attached a file.
File tests are about the bytes, the name, and the refusal. They are not about the control’s disabled attribute.
Upload the File You Mean
setInputFiles works on input[type=file], including inputs that are visually hidden.
import path from 'node:path';
test('accepts a small CSV and shows the row count', async ({ page }) => {
await page.goto('/import');
await page.getByLabel('Spreadsheet').setInputFiles(
path.join(__dirname, 'fixtures', 'three-rows.csv'),
);
await page.getByRole('button', { name: 'Import' }).click();
await expect(page.getByRole('status')).toHaveText('3 rows ready');
});
Keep fixtures tiny and committed. A test that depends on a file on someone’s desktop is not a test.
Assert something that can only be true if the contents were read: a row count, a column name, a total. “File selected” is the same class of assertion as “button enabled.”
Refuse the File You Do Not Want
Add one refusal for type and one for size, if the product has those rules.
test('refuses an executable', async ({ page }) => {
await page.goto('/import');
await page.getByLabel('Spreadsheet').setInputFiles(
path.join(__dirname, 'fixtures', 'not-a-sheet.bin'),
);
await expect(page.getByRole('alert')).toHaveText('Use a CSV file');
await expect(page.getByRole('button', { name: 'Import' })).toBeDisabled();
});
If the check happens only on the server, submit and assert the server’s message. A client-side accept attribute is not the rule. Users can bypass it.
Download
Wait for the download, then read it. Do not assert that a click happened.
test('downloads the invoice with the order number in the name', async ({ page }) => {
await page.goto('/orders/1001');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('link', { name: 'Download invoice' }).click(),
]);
expect(download.suggestedFilename()).toBe('invoice-1001.pdf');
const file = await download.path();
expect(file).toBeTruthy();
});
Open the file when the format is simple. A CSV fixture comparison, or a PDF text extract of the order number, catches a download that is an HTML error page named .pdf. Comparing every byte of a generated PDF will fail on timestamps. Compare the fields the user paid for.
Conclusion
Attach a real file. Assert a fact from inside it. Refuse one illegal file. On the way out, wait for the download and check the name and one field.
Action for this week: Replace one upload test that never calls setInputFiles with a fixture of two or three rows. Assert a value that is inside the file.
Next in the series: Part 15 — Assert on the Page the Click Opened.