Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/file-upload-download.md
1# File Upload and Download Testing23> **When to use**: Testing file uploads (single, multiple, drag-and-drop), downloads (content verification, filename, type), upload progress indicators, or file type/size restrictions.45## Table of Contents671. [Downloading Files](#downloading-files)82. [Single File Upload](#single-file-upload)93. [Multiple File Upload](#multiple-file-upload)104. [Drag-and-Drop Zones](#drag-and-drop-zones)115. [File Chooser Dialog](#file-chooser-dialog)126. [Upload Progress and Cancellation](#upload-progress-and-cancellation)137. [Retry After Failure](#retry-after-failure)148. [File Type and Size Restrictions](#file-type-and-size-restrictions)159. [Image Preview](#image-preview)1610. [Authenticated Downloads](#authenticated-downloads)1711. [Tips](#tips)1819---2021## Downloading Files2223### Capturing Downloads and Verifying Content2425```typescript26import { test, expect } from '@playwright/test';27import fs from 'fs';28import path from 'path';2930test('verifies downloaded CSV content', async ({ page }) => {31await page.goto('/exports');3233// Set up download listener BEFORE triggering the download34const downloadPromise = page.waitForEvent('download');35await page.getByRole('link', { name: 'transactions.csv' }).click();3637const download = await downloadPromise;38const savePath = path.join(__dirname, '../tmp', download.suggestedFilename());39await download.saveAs(savePath);4041const content = fs.readFileSync(savePath, 'utf-8');42expect(content).toContain('id,amount,date');43expect(content).toContain('1001,250.00,2025-01-15');4445const rows = content.trim().split('\n');46expect(rows.length).toBeGreaterThan(1);4748fs.unlinkSync(savePath);49});5051test('reads download via stream without disk I/O', async ({ page }) => {52await page.goto('/exports');5354const downloadPromise = page.waitForEvent('download');55await page.getByRole('link', { name: 'transactions.csv' }).click();5657const download = await downloadPromise;58const readable = await download.createReadStream();59const chunks: Buffer[] = [];6061for await (const chunk of readable!) {62chunks.push(Buffer.from(chunk));63}6465const content = Buffer.concat(chunks).toString('utf-8');66expect(content).toContain('id,amount,date');67});68```6970### Verifying Filename and Format7172```typescript73test('export filename matches selected format', async ({ page }) => {74await page.goto('/analytics');7576const downloadPromise = page.waitForEvent('download');77await page.getByRole('button', { name: 'Export PDF' }).click();7879const download = await downloadPromise;80expect(download.suggestedFilename()).toMatch(/^analytics-\d{4}-\d{2}-\d{2}\.pdf$/);81});8283test('format selector changes output extension', async ({ page }) => {84await page.goto('/analytics');8586await page.getByLabel('Format').selectOption('csv');87const csvDownload = page.waitForEvent('download');88await page.getByRole('button', { name: 'Download' }).click();89expect((await csvDownload).suggestedFilename()).toMatch(/\.csv$/);9091await page.getByLabel('Format').selectOption('xlsx');92const xlsxDownload = page.waitForEvent('download');93await page.getByRole('button', { name: 'Download' }).click();94expect((await xlsxDownload).suggestedFilename()).toMatch(/\.xlsx$/);95});96```9798### Checking Response Headers99100```typescript101test('download response has correct MIME type', async ({ page }) => {102await page.goto('/analytics');103104const responsePromise = page.waitForResponse('**/api/analytics/export**');105const downloadPromise = page.waitForEvent('download');106107await page.getByRole('button', { name: 'Export PDF' }).click();108109const response = await responsePromise;110expect(response.headers()['content-type']).toContain('application/pdf');111expect(response.headers()['content-disposition']).toContain('attachment');112113await downloadPromise;114});115```116117### Handling Download Failures118119```typescript120test('shows error when download fails', async ({ page }) => {121await page.route('**/api/analytics/export**', async (route) => {122await route.fulfill({123status: 500,124contentType: 'application/json',125body: JSON.stringify({ error: 'Generation failed' }),126});127});128129await page.goto('/analytics');130await page.getByRole('button', { name: 'Export PDF' }).click();131132await expect(page.getByRole('alert')).toContainText(/failed|error/i);133});134```135136---137138## Single File Upload139140### From Fixture File141142```typescript143import path from 'path';144145test('uploads document from fixture', async ({ page }) => {146await page.goto('/attachments');147148const fileInput = page.locator('input[type="file"]');149await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/invoice.pdf'));150151await expect(page.getByText('invoice.pdf')).toBeVisible();152153await page.getByRole('button', { name: 'Upload' }).click();154await expect(page.getByRole('alert')).toContainText('uploaded successfully');155await expect(page.getByRole('link', { name: 'invoice.pdf' })).toBeVisible();156});157```158159### From In-Memory Buffer160161```typescript162test('uploads in-memory CSV', async ({ page }) => {163await page.goto('/attachments');164165const fileInput = page.locator('input[type="file"]');166await fileInput.setInputFiles({167name: 'contacts.csv',168mimeType: 'text/csv',169buffer: Buffer.from('name,email\nAlice,[email protected]\nBob,[email protected]'),170});171172await expect(page.getByText('contacts.csv')).toBeVisible();173await page.getByRole('button', { name: 'Upload' }).click();174await expect(page.getByRole('alert')).toContainText('uploaded successfully');175});176```177178### Clearing Selection179180```typescript181test('clears selected file', async ({ page }) => {182await page.goto('/attachments');183184const fileInput = page.locator('input[type="file"]');185await fileInput.setInputFiles({186name: 'draft.txt',187mimeType: 'text/plain',188buffer: Buffer.from('draft content'),189});190191await expect(page.getByText('draft.txt')).toBeVisible();192193// Clear via API194await fileInput.setInputFiles([]);195await expect(page.getByText('draft.txt')).not.toBeVisible();196});197```198199---200201## Multiple File Upload202203```typescript204test('uploads multiple files at once', async ({ page }) => {205await page.goto('/attachments');206207const fileInput = page.locator('input[type="file"]');208await fileInput.setInputFiles([209{ name: 'doc1.pdf', mimeType: 'application/pdf', buffer: Buffer.from('pdf1') },210{ name: 'doc2.pdf', mimeType: 'application/pdf', buffer: Buffer.from('pdf2') },211{ name: 'doc3.pdf', mimeType: 'application/pdf', buffer: Buffer.from('pdf3') },212]);213214await expect(page.getByText('doc1.pdf')).toBeVisible();215await expect(page.getByText('doc2.pdf')).toBeVisible();216await expect(page.getByText('doc3.pdf')).toBeVisible();217await expect(page.getByText('3 files selected')).toBeVisible();218219await page.getByRole('button', { name: 'Upload all' }).click();220await expect(page.getByRole('alert')).toContainText('3 files uploaded');221});222223test('removes one file from selection', async ({ page }) => {224await page.goto('/attachments');225226const fileInput = page.locator('input[type="file"]');227await fileInput.setInputFiles([228{ name: 'keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },229{ name: 'discard.txt', mimeType: 'text/plain', buffer: Buffer.from('discard') },230]);231232const discardRow = page.getByText('discard.txt').locator('..');233await discardRow.getByRole('button', { name: /remove|delete|×/i }).click();234235await expect(page.getByText('discard.txt')).not.toBeVisible();236await expect(page.getByText('keep.txt')).toBeVisible();237});238```239240---241242## Drag-and-Drop Zones243244Drop zones always have an underlying `input[type="file"]`—target it directly instead of simulating OS-level drag events.245246```typescript247test('uploads via drop zone', async ({ page }) => {248await page.goto('/attachments');249250const dropZone = page.locator('[data-testid="drop-zone"]');251await expect(dropZone).toContainText(/drag.*here|drop.*files/i);252253const fileInput = page.locator('input[type="file"]');254await fileInput.setInputFiles({255name: 'dropped.pdf',256mimeType: 'application/pdf',257buffer: Buffer.from('pdf-content'),258});259260await expect(dropZone.getByText('dropped.pdf')).toBeVisible();261await page.getByRole('button', { name: 'Upload' }).click();262await expect(page.getByRole('alert')).toContainText('uploaded successfully');263});264265test('shows visual feedback on drag-over', async ({ page }) => {266await page.goto('/attachments');267268const dropZone = page.locator('[data-testid="drop-zone"]');269270await dropZone.dispatchEvent('dragenter', {271dataTransfer: { types: ['Files'], files: [] },272});273274await expect(dropZone).toHaveClass(/active|highlight|drag-over/);275await expect(dropZone).toContainText(/release|drop now/i);276277await dropZone.dispatchEvent('dragleave');278await expect(dropZone).not.toHaveClass(/active|highlight|drag-over/);279});280```281282---283284## File Chooser Dialog285286```typescript287test('uploads via native file chooser', async ({ page }) => {288await page.goto('/attachments');289290const fileChooserPromise = page.waitForEvent('filechooser');291await page.getByRole('button', { name: 'Choose file' }).click();292293const fileChooser = await fileChooserPromise;294expect(fileChooser.isMultiple()).toBe(false);295296await fileChooser.setFiles({297name: 'selected.pdf',298mimeType: 'application/pdf',299buffer: Buffer.from('pdf-content'),300});301302await expect(page.getByText('selected.pdf')).toBeVisible();303});304```305306---307308## Upload Progress and Cancellation309310```typescript311test('displays upload progress for large file', async ({ page }) => {312await page.goto('/attachments');313314const fileInput = page.locator('input[type="file"]');315const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');316317await fileInput.setInputFiles({318name: 'dataset.bin',319mimeType: 'application/octet-stream',320buffer: largeBuffer,321});322323await page.getByRole('button', { name: 'Upload' }).click();324325const progressBar = page.getByRole('progressbar');326await expect(progressBar).toBeVisible();327328await expect(async () => {329const value = await progressBar.getAttribute('aria-valuenow');330expect(Number(value)).toBeGreaterThan(0);331}).toPass({ timeout: 10000 });332333await expect(progressBar).not.toBeVisible({ timeout: 60000 });334await expect(page.getByRole('alert')).toContainText('uploaded successfully');335});336337test('cancels in-progress upload', async ({ page }) => {338await page.route('**/api/attachments/upload', async (route) => {339await new Promise((resolve) => setTimeout(resolve, 10000));340await route.continue();341});342343await page.goto('/attachments');344345const fileInput = page.locator('input[type="file"]');346await fileInput.setInputFiles({347name: 'large.bin',348mimeType: 'application/octet-stream',349buffer: Buffer.alloc(5 * 1024 * 1024, 'x'),350});351352await page.getByRole('button', { name: 'Upload' }).click();353await expect(page.getByRole('progressbar')).toBeVisible();354355await page.getByRole('button', { name: 'Cancel upload' }).click();356357await expect(page.getByRole('progressbar')).not.toBeVisible();358await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();359await expect(page.getByRole('link', { name: 'large.bin' })).not.toBeVisible();360});361```362363---364365## Retry After Failure366367```typescript368test('retries failed upload', async ({ page }) => {369let attempt = 0;370371await page.route('**/api/attachments/upload', async (route) => {372attempt++;373if (attempt === 1) {374await route.fulfill({375status: 500,376contentType: 'application/json',377body: JSON.stringify({ error: 'Server error' }),378});379} else {380await route.fulfill({381status: 200,382contentType: 'application/json',383body: JSON.stringify({ id: 'abc', name: 'data.csv' }),384});385}386});387388await page.goto('/attachments');389390const fileInput = page.locator('input[type="file"]');391await fileInput.setInputFiles({392name: 'data.csv',393mimeType: 'text/csv',394buffer: Buffer.from('col1,col2\nval1,val2'),395});396397await page.getByRole('button', { name: 'Upload' }).click();398await expect(page.getByText(/upload failed|error/i)).toBeVisible();399400await page.getByRole('button', { name: /retry/i }).click();401await expect(page.getByRole('alert')).toContainText('uploaded successfully');402expect(attempt).toBe(2);403});404```405406---407408## File Type and Size Restrictions409410### Validating Allowed Types411412```typescript413test('accepts allowed file types', async ({ page }) => {414await page.goto('/attachments');415416const fileInput = page.locator('input[type="file"]');417await expect(fileInput).toHaveAttribute('accept', /\.pdf|\.doc|\.docx|\.txt/);418419await fileInput.setInputFiles({420name: 'report.pdf',421mimeType: 'application/pdf',422buffer: Buffer.from('pdf-content'),423});424425await expect(page.getByText('report.pdf')).toBeVisible();426await expect(page.getByText(/not allowed|invalid/i)).not.toBeVisible();427});428429test('rejects disallowed file types', async ({ page }) => {430await page.goto('/attachments');431432const fileInput = page.locator('input[type="file"]');433// setInputFiles bypasses the accept attribute—tests JavaScript validation434await fileInput.setInputFiles({435name: 'malware.exe',436mimeType: 'application/x-msdownload',437buffer: Buffer.from('exe-content'),438});439440await expect(page.getByRole('alert')).toContainText(441/not allowed|unsupported file type|only .pdf, .doc/i442);443await expect(page.getByText('malware.exe')).not.toBeVisible();444});445```446447### Enforcing Size Limits448449```typescript450test('rejects oversized file', async ({ page }) => {451await page.goto('/attachments');452453const fileInput = page.locator('input[type="file"]');454const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');455456await fileInput.setInputFiles({457name: 'huge.pdf',458mimeType: 'application/pdf',459buffer: oversizedBuffer,460});461462await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);463await expect(page.getByText('huge.pdf')).not.toBeVisible();464});465```466467### Enforcing File Count Limits468469```typescript470test('rejects too many files', async ({ page }) => {471await page.goto('/attachments');472473const fileInput = page.locator('input[type="file"]');474const files = Array.from({ length: 6 }, (_, i) => ({475name: `file-${i + 1}.txt`,476mimeType: 'text/plain' as const,477buffer: Buffer.from(`content ${i + 1}`),478}));479480await fileInput.setInputFiles(files);481482await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);483});484```485486### Validating Image Dimensions487488```typescript489test('rejects image below minimum dimensions', async ({ page }) => {490await page.goto('/profile/avatar');491492const fileInput = page.locator('input[type="file"]');493// Minimal 1x1 PNG494const tinyPng = Buffer.from(495'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',496'base64'497);498499await fileInput.setInputFiles({500name: 'tiny.png',501mimeType: 'image/png',502buffer: tinyPng,503});504505await expect(page.getByRole('alert')).toContainText(/minimum.*dimensions|too small/i);506});507```508509---510511## Image Preview512513```typescript514test('shows image preview after selection', async ({ page }) => {515await page.goto('/profile/avatar');516517const fileInput = page.locator('input[type="file"]');518await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/photo.jpg'));519520const preview = page.getByRole('img', { name: /preview|avatar/i });521await expect(preview).toBeVisible();522523const src = await preview.getAttribute('src');524expect(src).toMatch(/^(blob:|data:image)/);525});526```527528---529530## Authenticated Downloads531532```typescript533test('downloads file requiring authentication', async ({ page, request }) => {534await page.goto('/attachments');535536// Browser download works because cookies are sent537const downloadPromise = page.waitForEvent('download');538await page.getByRole('link', { name: 'confidential.pdf' }).click();539540const download = await downloadPromise;541expect(download.suggestedFilename()).toBe('confidential.pdf');542543// Verify via API request (carries auth context)544const response = await request.get('/api/attachments/456/download');545expect(response.ok()).toBeTruthy();546expect(response.headers()['content-type']).toContain('application/pdf');547});548```549550---551552## Tips5535541. **Use `setInputFiles` for uploads**. Even drag-and-drop zones have an underlying `input[type="file"]`. Target it directly instead of simulating OS-level drag events.5555562. **Prefer in-memory buffers**. Creating files with `Buffer.from()` keeps tests self-contained. Use fixture files only when you need real content (e.g., a valid PDF your app parses).5575583. **Set up download listener before clicking**. Call `page.waitForEvent('download')` before the click that triggers the download—otherwise you may miss the event.5595604. **Use `createReadStream()` for content verification**. Reading directly from the stream avoids disk I/O and cleanup of temporary files.5615625. **Test both `accept` attribute and JavaScript validation**. The HTML `accept` attribute only filters the OS file dialog. `setInputFiles()` bypasses it, which is exactly what you need to test your app's JavaScript validation.563