30 KiB
Executable file
File Upload and Download Recipes
When to use: You need to test file uploads (single, multiple, drag-and-drop), file downloads (verify content, filename, type), upload progress, or file type restrictions.
Recipe 1: Single File Upload via Input
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
import path from 'path';
test('uploads a single file via file input', async ({ page }) => {
await page.goto('/documents');
// Click the upload button which triggers the hidden file input
const fileInput = page.locator('input[type="file"]');
// Upload a file from the test fixtures directory
await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
// Verify the file name appears in the UI
await expect(page.getByText('report.pdf')).toBeVisible();
// Click the submit/upload button
await page.getByRole('button', { name: 'Upload' }).click();
// Wait for the upload to complete
await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
// Verify the file appears in the documents list
await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
});
test('uploads a file created in-memory (no fixture file needed)', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Create a file from a buffer -- no fixture file required
await fileInput.setInputFiles({
name: 'test-data.csv',
mimeType: 'text/csv',
buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin\nBob,bob@example.com,user'),
});
await expect(page.getByText('test-data.csv')).toBeVisible();
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
});
test('clears a selected file before uploading', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Select a file
await fileInput.setInputFiles({
name: 'wrong-file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('wrong content'),
});
await expect(page.getByText('wrong-file.txt')).toBeVisible();
// Clear the selection
await fileInput.setInputFiles([]);
// Or click a "Remove" button in the UI
// await page.getByRole('button', { name: 'Remove' }).click();
await expect(page.getByText('wrong-file.txt')).not.toBeVisible();
});
JavaScript
const { test, expect } = require('@playwright/test');
const path = require('path');
test('uploads a single file via file input', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
await expect(page.getByText('report.pdf')).toBeVisible();
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
});
test('uploads a file created in-memory', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'test-data.csv',
mimeType: 'text/csv',
buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin'),
});
await expect(page.getByText('test-data.csv')).toBeVisible();
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
});
Recipe 2: Multiple File Upload
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
import path from 'path';
test('uploads multiple files at once', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Pass an array to upload multiple files
await fileInput.setInputFiles([
path.resolve(__dirname, '../fixtures/report.pdf'),
path.resolve(__dirname, '../fixtures/photo.jpg'),
path.resolve(__dirname, '../fixtures/data.csv'),
]);
// Verify all file names appear
await expect(page.getByText('report.pdf')).toBeVisible();
await expect(page.getByText('photo.jpg')).toBeVisible();
await expect(page.getByText('data.csv')).toBeVisible();
// Verify the count indicator
await expect(page.getByText('3 files selected')).toBeVisible();
await page.getByRole('button', { name: 'Upload all' }).click();
await expect(page.getByRole('alert')).toContainText('3 files uploaded');
});
test('uploads multiple files with in-memory buffers', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles([
{
name: 'notes.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Meeting notes for Q4 planning'),
},
{
name: 'config.json',
mimeType: 'application/json',
buffer: Buffer.from(JSON.stringify({ theme: 'dark', lang: 'en' })),
},
]);
await expect(page.getByText('notes.txt')).toBeVisible();
await expect(page.getByText('config.json')).toBeVisible();
await page.getByRole('button', { name: 'Upload all' }).click();
await expect(page.getByRole('alert')).toContainText('2 files uploaded');
});
test('removes one file from a multi-file selection', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles([
{ name: 'keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
{ name: 'remove.txt', mimeType: 'text/plain', buffer: Buffer.from('remove') },
{ name: 'also-keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
]);
// Remove a specific file from the preview list
const removeItem = page.getByText('remove.txt').locator('..');
await removeItem.getByRole('button', { name: /remove|delete|×/i }).click();
await expect(page.getByText('remove.txt')).not.toBeVisible();
await expect(page.getByText('keep.txt')).toBeVisible();
await expect(page.getByText('also-keep.txt')).toBeVisible();
await expect(page.getByText('2 files selected')).toBeVisible();
});
JavaScript
const { test, expect } = require('@playwright/test');
const path = require('path');
test('uploads multiple files at once', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles([
path.resolve(__dirname, '../fixtures/report.pdf'),
path.resolve(__dirname, '../fixtures/photo.jpg'),
path.resolve(__dirname, '../fixtures/data.csv'),
]);
await expect(page.getByText('report.pdf')).toBeVisible();
await expect(page.getByText('photo.jpg')).toBeVisible();
await expect(page.getByText('data.csv')).toBeVisible();
await page.getByRole('button', { name: 'Upload all' }).click();
await expect(page.getByRole('alert')).toContainText('3 files uploaded');
});
Recipe 3: Drag-and-Drop File Upload
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
import path from 'path';
test('uploads a file via drag-and-drop zone', async ({ page }) => {
await page.goto('/documents');
const dropZone = page.locator('[data-testid="drop-zone"]');
await expect(dropZone).toContainText(/drag.*here|drop.*files/i);
// Drag-and-drop from the OS is not natively supported in Playwright,
// but drop zones always have an underlying input[type=file] -- use that
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
// File preview should appear in the drop zone
await expect(dropZone.getByText('report.pdf')).toBeVisible();
// Trigger upload
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});
test('shows visual feedback during drag-over', async ({ page }) => {
await page.goto('/documents');
const dropZone = page.locator('[data-testid="drop-zone"]');
// Simulate dragenter to show visual feedback
await dropZone.dispatchEvent('dragenter', {
dataTransfer: { types: ['Files'], files: [] },
});
// Drop zone should show an active state
await expect(dropZone).toHaveClass(/active|highlight|drag-over/);
await expect(dropZone).toContainText(/release|drop now/i);
// Simulate dragleave to reset
await dropZone.dispatchEvent('dragleave');
await expect(dropZone).not.toHaveClass(/active|highlight|drag-over/);
});
test('handles multiple files dropped at once', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles([
{ name: 'image1.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-1') },
{ name: 'image2.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-2') },
{ name: 'image3.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-3') },
]);
await expect(page.getByText('image1.png')).toBeVisible();
await expect(page.getByText('image2.png')).toBeVisible();
await expect(page.getByText('image3.png')).toBeVisible();
});
JavaScript
const { test, expect } = require('@playwright/test');
const path = require('path');
test('uploads a file via drag-and-drop zone', async ({ page }) => {
await page.goto('/documents');
const dropZone = page.locator('[data-testid="drop-zone"]');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
await expect(dropZone.getByText('report.pdf')).toBeVisible();
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});
Recipe 4: Download and Verify File Content
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
test('downloads a file and verifies its content', async ({ page }) => {
await page.goto('/documents');
// Start waiting for the download event BEFORE clicking
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'report.csv' }).click();
const download = await downloadPromise;
// Save to a temporary location
const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
await download.saveAs(downloadPath);
// Read and verify the content
const content = fs.readFileSync(downloadPath, 'utf-8');
expect(content).toContain('name,email,role');
expect(content).toContain('Jane,jane@example.com,admin');
// Verify line count
const lines = content.trim().split('\n');
expect(lines.length).toBeGreaterThan(1); // header + at least one data row
// Clean up
fs.unlinkSync(downloadPath);
});
test('downloads a JSON file and verifies structure', async ({ page }) => {
await page.goto('/api-docs');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export API spec' }).click();
const download = await downloadPromise;
const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
await download.saveAs(downloadPath);
const content = JSON.parse(fs.readFileSync(downloadPath, 'utf-8'));
expect(content).toHaveProperty('openapi');
expect(content.paths).toBeDefined();
expect(Object.keys(content.paths).length).toBeGreaterThan(0);
fs.unlinkSync(downloadPath);
});
test('downloads a file via the stream (no disk save needed)', async ({ page }) => {
await page.goto('/documents');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'report.csv' }).click();
const download = await downloadPromise;
// Read directly from the download stream without saving to disk
const readable = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const chunk of readable!) {
chunks.push(Buffer.from(chunk));
}
const content = Buffer.concat(chunks).toString('utf-8');
expect(content).toContain('name,email,role');
});
JavaScript
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
test('downloads a file and verifies its content', async ({ page }) => {
await page.goto('/documents');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'report.csv' }).click();
const download = await downloadPromise;
const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
await download.saveAs(downloadPath);
const content = fs.readFileSync(downloadPath, 'utf-8');
expect(content).toContain('name,email,role');
expect(content).toContain('Jane,jane@example.com,admin');
fs.unlinkSync(downloadPath);
});
test('downloads a file via the stream', async ({ page }) => {
await page.goto('/documents');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'report.csv' }).click();
const download = await downloadPromise;
const readable = await download.createReadStream();
const chunks = [];
for await (const chunk of readable) {
chunks.push(Buffer.from(chunk));
}
const content = Buffer.concat(chunks).toString('utf-8');
expect(content).toContain('name,email,role');
});
Recipe 5: Download and Verify Filename
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
test('download has correct filename and extension', async ({ page }) => {
await page.goto('/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export as PDF' }).click();
const download = await downloadPromise;
// Verify filename pattern
expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
});
test('download filename changes based on selected format', async ({ page }) => {
await page.goto('/reports');
// Select CSV format
await page.getByLabel('Export format').selectOption('csv');
const csvDownload = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const csv = await csvDownload;
expect(csv.suggestedFilename()).toMatch(/\.csv$/);
// Select Excel format
await page.getByLabel('Export format').selectOption('xlsx');
const xlsxDownload = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const xlsx = await xlsxDownload;
expect(xlsx.suggestedFilename()).toMatch(/\.xlsx$/);
});
test('download has correct MIME type via response headers', async ({ page }) => {
await page.goto('/reports');
// Intercept the download request to check response headers
const responsePromise = page.waitForResponse('**/api/reports/export**');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export as PDF' }).click();
const response = await responsePromise;
expect(response.headers()['content-type']).toContain('application/pdf');
expect(response.headers()['content-disposition']).toContain('attachment');
await downloadPromise; // Consume the download event
});
test('download failure shows error to user', async ({ page }) => {
// Mock the download endpoint to fail
await page.route('**/api/reports/export**', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Report generation failed' }),
});
});
await page.goto('/reports');
await page.getByRole('button', { name: 'Export as PDF' }).click();
await expect(page.getByRole('alert')).toContainText(/failed|error/i);
});
JavaScript
const { test, expect } = require('@playwright/test');
test('download has correct filename and extension', async ({ page }) => {
await page.goto('/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export as PDF' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
});
test('download filename changes based on selected format', async ({ page }) => {
await page.goto('/reports');
await page.getByLabel('Export format').selectOption('csv');
const csvDownload = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const csv = await csvDownload;
expect(csv.suggestedFilename()).toMatch(/\.csv$/);
});
Recipe 6: Large File Upload with Progress
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
test('shows upload progress for a large file', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Create a large in-memory file (5 MB)
const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
await fileInput.setInputFiles({
name: 'large-dataset.bin',
mimeType: 'application/octet-stream',
buffer: largeBuffer,
});
await page.getByRole('button', { name: 'Upload' }).click();
// Verify progress bar appears
const progressBar = page.getByRole('progressbar');
await expect(progressBar).toBeVisible();
// Verify progress percentage updates
// Use polling to check that progress increases
await expect(async () => {
const value = await progressBar.getAttribute('aria-valuenow');
expect(Number(value)).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Wait for upload to complete
await expect(progressBar).not.toBeVisible({ timeout: 60000 });
await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});
test('cancels an in-progress upload', async ({ page }) => {
// Slow down the upload API to simulate a long upload
await page.route('**/api/documents/upload', async (route) => {
// Delay for 10 seconds to give us time to cancel
await new Promise((resolve) => setTimeout(resolve, 10000));
await route.continue();
});
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
await fileInput.setInputFiles({
name: 'large-file.bin',
mimeType: 'application/octet-stream',
buffer: largeBuffer,
});
await page.getByRole('button', { name: 'Upload' }).click();
// Wait for upload to start
await expect(page.getByRole('progressbar')).toBeVisible();
// Cancel the upload
await page.getByRole('button', { name: 'Cancel upload' }).click();
// Verify upload was cancelled
await expect(page.getByRole('progressbar')).not.toBeVisible();
await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();
// Verify the file did not appear in the documents list
await expect(page.getByRole('link', { name: 'large-file.bin' })).not.toBeVisible();
});
test('retries a failed upload', async ({ page }) => {
let attempt = 0;
await page.route('**/api/documents/upload', async (route) => {
attempt++;
if (attempt === 1) {
// First attempt fails
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' }),
});
} else {
// Second attempt succeeds
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: '123', name: 'data.csv' }),
});
}
});
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'data.csv',
mimeType: 'text/csv',
buffer: Buffer.from('col1,col2\nval1,val2'),
});
await page.getByRole('button', { name: 'Upload' }).click();
// First attempt fails
await expect(page.getByText(/upload failed|error/i)).toBeVisible();
// Retry
await page.getByRole('button', { name: /retry/i }).click();
// Second attempt succeeds
await expect(page.getByRole('alert')).toContainText('uploaded successfully');
expect(attempt).toBe(2);
});
JavaScript
const { test, expect } = require('@playwright/test');
test('shows upload progress for a large file', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
await fileInput.setInputFiles({
name: 'large-dataset.bin',
mimeType: 'application/octet-stream',
buffer: largeBuffer,
});
await page.getByRole('button', { name: 'Upload' }).click();
const progressBar = page.getByRole('progressbar');
await expect(progressBar).toBeVisible();
await expect(progressBar).not.toBeVisible({ timeout: 60000 });
await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});
test('cancels an in-progress upload', async ({ page }) => {
await page.route('**/api/documents/upload', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 10000));
await route.continue();
});
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'large-file.bin',
mimeType: 'application/octet-stream',
buffer: Buffer.alloc(5 * 1024 * 1024, 'x'),
});
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByRole('progressbar')).toBeVisible();
await page.getByRole('button', { name: 'Cancel upload' }).click();
await expect(page.getByRole('progressbar')).not.toBeVisible();
await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();
});
Recipe 7: Testing File Type Restrictions
Complete Example
TypeScript
import { test, expect } from '@playwright/test';
test('accepts allowed file types', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Verify the accept attribute on the file input
await expect(fileInput).toHaveAttribute('accept', /\.pdf|\.doc|\.docx|\.txt/);
// Upload an allowed file type
await fileInput.setInputFiles({
name: 'valid-document.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('fake-pdf-content'),
});
// Should be accepted without error
await expect(page.getByText('valid-document.pdf')).toBeVisible();
await expect(page.getByText(/not allowed|invalid/i)).not.toBeVisible();
});
test('rejects disallowed file types with clear error message', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Upload a disallowed file type
// Note: setInputFiles bypasses the browser's accept attribute filter,
// so the app's JavaScript validation is what we are testing here
await fileInput.setInputFiles({
name: 'script.exe',
mimeType: 'application/x-msdownload',
buffer: Buffer.from('fake-exe-content'),
});
// App should show a rejection message
await expect(page.getByRole('alert')).toContainText(
/not allowed|unsupported file type|only .pdf, .doc/i
);
// File should not appear in the upload queue
await expect(page.getByText('script.exe')).not.toBeVisible();
});
test('enforces maximum file size', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Create a file that exceeds the max size (e.g., 11 MB when limit is 10 MB)
const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');
await fileInput.setInputFiles({
name: 'huge-file.pdf',
mimeType: 'application/pdf',
buffer: oversizedBuffer,
});
await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
await expect(page.getByText('huge-file.pdf')).not.toBeVisible();
});
test('enforces maximum number of files', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
// Try to upload more files than the limit (e.g., limit is 5)
const files = Array.from({ length: 6 }, (_, i) => ({
name: `file-${i + 1}.txt`,
mimeType: 'text/plain' as const,
buffer: Buffer.from(`content ${i + 1}`),
}));
await fileInput.setInputFiles(files);
await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
});
test('validates image dimensions for avatar upload', async ({ page }) => {
await page.goto('/settings/profile');
const fileInput = page.locator('input[type="file"]');
// Create a tiny 1x1 PNG (valid format but too small)
// Minimal PNG buffer
const tinyPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64'
);
await fileInput.setInputFiles({
name: 'tiny-avatar.png',
mimeType: 'image/png',
buffer: tinyPng,
});
await expect(page.getByRole('alert')).toContainText(/minimum.*dimensions|too small/i);
});
JavaScript
const { test, expect } = require('@playwright/test');
test('rejects disallowed file types with clear error message', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'script.exe',
mimeType: 'application/x-msdownload',
buffer: Buffer.from('fake-exe-content'),
});
await expect(page.getByRole('alert')).toContainText(
/not allowed|unsupported file type|only .pdf, .doc/i
);
await expect(page.getByText('script.exe')).not.toBeVisible();
});
test('enforces maximum file size', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');
await fileInput.setInputFiles({
name: 'huge-file.pdf',
mimeType: 'application/pdf',
buffer: oversizedBuffer,
});
await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
});
test('enforces maximum number of files', async ({ page }) => {
await page.goto('/documents');
const fileInput = page.locator('input[type="file"]');
const files = Array.from({ length: 6 }, (_, i) => ({
name: `file-${i + 1}.txt`,
mimeType: 'text/plain',
buffer: Buffer.from(`content ${i + 1}`),
}));
await fileInput.setInputFiles(files);
await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
});
Variations
Upload via File Chooser Dialog
test('uploads via the native file chooser dialog', async ({ page }) => {
await page.goto('/documents');
// Listen for the file chooser event before clicking the trigger
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose file' }).click();
const fileChooser = await fileChooserPromise;
// Verify it accepts only certain types
expect(fileChooser.isMultiple()).toBe(false);
await fileChooser.setFiles({
name: 'chosen-file.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('pdf-content'),
});
await expect(page.getByText('chosen-file.pdf')).toBeVisible();
});
Upload with Image Preview
test('shows image preview after selecting a file', async ({ page }) => {
await page.goto('/settings/profile');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/avatar.jpg'));
// Verify the image preview is displayed
const preview = page.getByRole('img', { name: /preview|avatar/i });
await expect(preview).toBeVisible();
// Verify the preview src is a blob or data URL
const src = await preview.getAttribute('src');
expect(src).toMatch(/^(blob:|data:image)/);
});
Download with Authentication
test('downloads a file that requires authentication', async ({ page, request }) => {
// Some downloads go through an API that needs auth cookies
await page.goto('/documents');
// The UI download works because the browser sends cookies
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'confidential-report.pdf' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('confidential-report.pdf');
// Alternatively, verify via API request (which carries the auth context)
const response = await request.get('/api/documents/123/download');
expect(response.ok()).toBeTruthy();
expect(response.headers()['content-type']).toContain('application/pdf');
});
Tips
-
Use
setInputFilesfor upload testing. Even if the UI uses a drag-and-drop zone, there is always an underlyinginput[type="file"]element. Target it directly withsetInputFiles()instead of trying to simulate OS-level drag events. -
Prefer in-memory buffers over fixture files. Creating files with
Buffer.from()keeps tests self-contained and eliminates dependencies on external fixture files. Use fixture files only when you need real file content (e.g., a valid PDF that your app parses). -
Always set up the download listener before clicking. Call
page.waitForEvent('download')before the click that triggers the download. If you click first, you may miss the event. -
Use
createReadStream()to verify content without disk I/O. Thedownload.createReadStream()method lets you read file content directly in memory, which is faster and avoids cleanup of temporary files. -
Test the accept attribute AND the JavaScript validation separately. The HTML
acceptattribute only filters the OS file dialog -- it does not prevent uploads via other means. Your app's JavaScript validation is the real gatekeeper, andsetInputFiles()bypasses theacceptfilter, which is exactly what you want to test.
Related
- Playwright Upload Docs
- Playwright Download Docs
recipes/drag-and-drop.md-- General drag and drop patternsrecipes/crud-testing.md-- File uploads as part of resource creationfoundations/actions.md-- Input interaction fundamentals