# 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

> **Playwright 1.60+**: `locator.drop({ files })` simulates a real external file drop (native `dragenter`/`dragover`/`drop` with synthetic `DataTransfer`). Use it for zones that listen for `drop` events and have no usable `input[type=file]`. On Playwright < 1.60, target the underlying file input with `setInputFiles` (shown after).

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';
import path from 'path';

test('uploads a file via a native drop (Playwright 1.60+)', async ({ page }) => {
  await page.goto('/documents');

  const dropZone = page.locator('[data-testid="drop-zone"]');
  await expect(dropZone).toContainText(/drag.*here|drop.*files/i);

  // Native external file drop — fires the drop events the zone listens for
  await dropZone.drop({ files: 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');
});

test('drops multiple files at once (Playwright 1.60+)', async ({ page }) => {
  await page.goto('/documents');

  await page.locator('[data-testid="drop-zone"]').drop({
    files: [
      { name: 'image1.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-1') },
      { name: 'image2.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-2') },
    ],
  });

  await expect(page.getByText('image1.png')).toBeVisible();
  await expect(page.getByText('image2.png')).toBeVisible();
});

test('uploads a file via the underlying input (Playwright < 1.60)', async ({ page }) => {
  await page.goto('/documents');

  const dropZone = page.locator('[data-testid="drop-zone"]');
  await expect(dropZone).toContainText(/drag.*here|drop.*files/i);

  // Older Playwright can't simulate OS file drops — drive the input the zone falls back to
  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. **Pick the right upload primitive for your Playwright version**. For a plain `input[type="file"]`, `setInputFiles()` is the most reliable path. For drag-and-drop zones that listen for `drop` events, prefer `locator.drop({ files })` on Playwright 1.60+ (it fires native `dragenter`/`dragover`/`drop`); on older versions, fall back to `setInputFiles()` on the zone's underlying `input[type="file"]`.

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
