SurfSense/.cursor/skills/playwright-testing/file-upload-download.md
2026-05-04 13:54:13 +05:30

982 lines
30 KiB
Markdown
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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**
```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
```typescript
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
```typescript
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
```typescript
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
1. **Use `setInputFiles` for upload testing**. Even if the UI uses a drag-and-drop zone, there is always an underlying `input[type="file"]` element. Target it directly with `setInputFiles()` instead of trying to simulate OS-level drag events.
2. **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).
3. **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.
4. **Use `createReadStream()` to verify content without disk I/O**. The `download.createReadStream()` method lets you read file content directly in memory, which is faster and avoids cleanup of temporary files.
5. **Test the accept attribute AND the JavaScript validation separately**. The HTML `accept` attribute only filters the OS file dialog -- it does not prevent uploads via other means. Your app's JavaScript validation is the real gatekeeper, and `setInputFiles()` bypasses the `accept` filter, which is exactly what you want to test.
---
## Related
- [Playwright Upload Docs](https://playwright.dev/docs/input#upload-files)
- [Playwright Download Docs](https://playwright.dev/docs/downloads)
- `recipes/drag-and-drop.md` -- General drag and drop patterns
- `recipes/crud-testing.md` -- File uploads as part of resource creation
- `foundations/actions.md` -- Input interaction fundamentals