mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
750 lines
25 KiB
Markdown
750 lines
25 KiB
Markdown
|
|
# File Operations
|
||
|
|
|
||
|
|
> **When to use**: Testing file uploads, downloads, drag-and-drop file interactions, file type validation, and download verification.
|
||
|
|
> **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Upload — single file
|
||
|
|
await page.getByLabel('Upload').setInputFiles('fixtures/resume.pdf');
|
||
|
|
|
||
|
|
// Upload — multiple files
|
||
|
|
await page.getByLabel('Upload').setInputFiles(['fixtures/a.png', 'fixtures/b.png']);
|
||
|
|
|
||
|
|
// Upload — clear selection
|
||
|
|
await page.getByLabel('Upload').setInputFiles([]);
|
||
|
|
|
||
|
|
// Download — wait and save
|
||
|
|
const download = await page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export CSV' }).click();
|
||
|
|
const path = await download.path(); // temp path
|
||
|
|
await download.saveAs('test-results/export.csv'); // permanent path
|
||
|
|
|
||
|
|
// File chooser dialog — non-input uploads
|
||
|
|
const fileChooser = await page.waitForEvent('filechooser');
|
||
|
|
await page.getByRole('button', { name: 'Choose file' }).click();
|
||
|
|
await fileChooser.setFiles('fixtures/photo.jpg');
|
||
|
|
```
|
||
|
|
|
||
|
|
## Patterns
|
||
|
|
|
||
|
|
### Single File Upload
|
||
|
|
|
||
|
|
**Use when**: A form has a standard `<input type="file">` element.
|
||
|
|
**Avoid when**: The upload uses a drag-and-drop zone with no underlying file input.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
test('upload a single document', async ({ page }) => {
|
||
|
|
await page.goto('/settings/profile');
|
||
|
|
|
||
|
|
const filePath = path.join(__dirname, '../fixtures/avatar.png');
|
||
|
|
await page.getByLabel('Profile picture').setInputFiles(filePath);
|
||
|
|
|
||
|
|
// Verify the file name appears in the UI
|
||
|
|
await expect(page.getByText('avatar.png')).toBeVisible();
|
||
|
|
|
||
|
|
await page.getByRole('button', { name: 'Save' }).click();
|
||
|
|
await expect(page.getByText('Profile updated')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
test('upload a single document', async ({ page }) => {
|
||
|
|
await page.goto('/settings/profile');
|
||
|
|
|
||
|
|
const filePath = path.join(__dirname, '../fixtures/avatar.png');
|
||
|
|
await page.getByLabel('Profile picture').setInputFiles(filePath);
|
||
|
|
|
||
|
|
await expect(page.getByText('avatar.png')).toBeVisible();
|
||
|
|
|
||
|
|
await page.getByRole('button', { name: 'Save' }).click();
|
||
|
|
await expect(page.getByText('Profile updated')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Multiple File Upload
|
||
|
|
|
||
|
|
**Use when**: The file input accepts `multiple` and you need to attach several files at once.
|
||
|
|
**Avoid when**: The UI only allows one file. Use single file upload.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
test('upload multiple attachments', async ({ page }) => {
|
||
|
|
await page.goto('/tickets/new');
|
||
|
|
|
||
|
|
const fixtures = ['doc1.pdf', 'doc2.pdf', 'screenshot.png'].map(
|
||
|
|
(f) => path.join(__dirname, '../fixtures', f)
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.getByLabel('Attachments').setInputFiles(fixtures);
|
||
|
|
|
||
|
|
// Verify all files are listed
|
||
|
|
await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(3);
|
||
|
|
|
||
|
|
// Remove one file by clearing and re-setting
|
||
|
|
await page.getByLabel('Attachments').setInputFiles(fixtures.slice(0, 2));
|
||
|
|
await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(2);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
test('upload multiple attachments', async ({ page }) => {
|
||
|
|
await page.goto('/tickets/new');
|
||
|
|
|
||
|
|
const fixtures = ['doc1.pdf', 'doc2.pdf', 'screenshot.png'].map(
|
||
|
|
(f) => path.join(__dirname, '../fixtures', f)
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.getByLabel('Attachments').setInputFiles(fixtures);
|
||
|
|
await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(3);
|
||
|
|
|
||
|
|
await page.getByLabel('Attachments').setInputFiles(fixtures.slice(0, 2));
|
||
|
|
await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(2);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### File Chooser Dialog
|
||
|
|
|
||
|
|
**Use when**: The upload is triggered by a button click that opens the native file picker, not by a visible `<input type="file">`. Common with drag-and-drop libraries and custom upload components.
|
||
|
|
**Avoid when**: There is a visible `<input type="file">` — use `setInputFiles` directly.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
|
||
|
|
test('upload via file chooser dialog', async ({ page }) => {
|
||
|
|
await page.goto('/documents');
|
||
|
|
|
||
|
|
// Register the listener BEFORE the click that opens the dialog
|
||
|
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||
|
|
await page.getByRole('button', { name: 'Upload document' }).click();
|
||
|
|
const fileChooser = await fileChooserPromise;
|
||
|
|
|
||
|
|
// Verify dialog properties
|
||
|
|
expect(fileChooser.isMultiple()).toBe(false);
|
||
|
|
|
||
|
|
await fileChooser.setFiles('fixtures/report.pdf');
|
||
|
|
await expect(page.getByText('report.pdf')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('upload multiple via file chooser', async ({ page }) => {
|
||
|
|
await page.goto('/gallery');
|
||
|
|
|
||
|
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||
|
|
await page.getByRole('button', { name: 'Add photos' }).click();
|
||
|
|
const fileChooser = await fileChooserPromise;
|
||
|
|
|
||
|
|
expect(fileChooser.isMultiple()).toBe(true);
|
||
|
|
await fileChooser.setFiles(['fixtures/photo1.jpg', 'fixtures/photo2.jpg']);
|
||
|
|
|
||
|
|
await expect(page.getByRole('img')).toHaveCount(2);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
|
||
|
|
test('upload via file chooser dialog', async ({ page }) => {
|
||
|
|
await page.goto('/documents');
|
||
|
|
|
||
|
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||
|
|
await page.getByRole('button', { name: 'Upload document' }).click();
|
||
|
|
const fileChooser = await fileChooserPromise;
|
||
|
|
|
||
|
|
expect(fileChooser.isMultiple()).toBe(false);
|
||
|
|
|
||
|
|
await fileChooser.setFiles('fixtures/report.pdf');
|
||
|
|
await expect(page.getByText('report.pdf')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('upload multiple via file chooser', async ({ page }) => {
|
||
|
|
await page.goto('/gallery');
|
||
|
|
|
||
|
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||
|
|
await page.getByRole('button', { name: 'Add photos' }).click();
|
||
|
|
const fileChooser = await fileChooserPromise;
|
||
|
|
|
||
|
|
expect(fileChooser.isMultiple()).toBe(true);
|
||
|
|
await fileChooser.setFiles(['fixtures/photo1.jpg', 'fixtures/photo2.jpg']);
|
||
|
|
|
||
|
|
await expect(page.getByRole('img')).toHaveCount(2);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Drag-and-Drop File Upload
|
||
|
|
|
||
|
|
**Use when**: The UI has a drop zone that accepts files via the HTML5 Drag and Drop API and has no `<input type="file">` fallback.
|
||
|
|
**Avoid when**: A file input exists — even hidden ones work with `setInputFiles`.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
test('drop file onto upload zone', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
// Read the file into a buffer
|
||
|
|
const filePath = path.join(__dirname, '../fixtures/data.csv');
|
||
|
|
const buffer = fs.readFileSync(filePath);
|
||
|
|
|
||
|
|
// Create a DataTransfer with the file and dispatch drop event
|
||
|
|
const dropZone = page.getByTestId('drop-zone');
|
||
|
|
|
||
|
|
await dropZone.dispatchEvent('drop', {
|
||
|
|
dataTransfer: {
|
||
|
|
files: [
|
||
|
|
{ name: 'data.csv', mimeType: 'text/csv', buffer },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('data.csv')).toBeVisible();
|
||
|
|
await expect(page.getByText('Upload complete')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('drag-and-drop with hidden input fallback', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
// Many drag-and-drop libraries still use a hidden <input type="file">
|
||
|
|
// Check for it first — this is more reliable than simulating DnD events
|
||
|
|
const hiddenInput = page.locator('input[type="file"]');
|
||
|
|
|
||
|
|
if (await hiddenInput.count() > 0) {
|
||
|
|
await hiddenInput.setInputFiles('fixtures/data.csv');
|
||
|
|
}
|
||
|
|
|
||
|
|
await expect(page.getByText('data.csv')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
test('drop file onto upload zone', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
const filePath = path.join(__dirname, '../fixtures/data.csv');
|
||
|
|
const buffer = fs.readFileSync(filePath);
|
||
|
|
|
||
|
|
const dropZone = page.getByTestId('drop-zone');
|
||
|
|
|
||
|
|
await dropZone.dispatchEvent('drop', {
|
||
|
|
dataTransfer: {
|
||
|
|
files: [
|
||
|
|
{ name: 'data.csv', mimeType: 'text/csv', buffer },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('data.csv')).toBeVisible();
|
||
|
|
await expect(page.getByText('Upload complete')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('drag-and-drop with hidden input fallback', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
const hiddenInput = page.locator('input[type="file"]');
|
||
|
|
|
||
|
|
if (await hiddenInput.count() > 0) {
|
||
|
|
await hiddenInput.setInputFiles('fixtures/data.csv');
|
||
|
|
}
|
||
|
|
|
||
|
|
await expect(page.getByText('data.csv')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### File Download — Wait and Verify
|
||
|
|
|
||
|
|
**Use when**: Testing export buttons, report generation, or any action that triggers a browser download.
|
||
|
|
**Avoid when**: The file is served as a page navigation (opens in a new tab). Use multi-tab patterns instead.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import fs from 'fs';
|
||
|
|
|
||
|
|
test('download and verify file content', async ({ page }) => {
|
||
|
|
await page.goto('/reports');
|
||
|
|
|
||
|
|
// Register BEFORE the click that triggers the download
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export CSV' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
// Verify download metadata
|
||
|
|
expect(download.suggestedFilename()).toBe('report-2025.csv');
|
||
|
|
|
||
|
|
// Save to a known location
|
||
|
|
const savePath = 'test-results/report.csv';
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
// Read and verify content
|
||
|
|
const content = fs.readFileSync(savePath, 'utf-8');
|
||
|
|
expect(content).toContain('Name,Email,Status');
|
||
|
|
expect(content).toContain('Jane Doe,jane@example.com,Active');
|
||
|
|
|
||
|
|
// Verify file size is reasonable
|
||
|
|
const stats = fs.statSync(savePath);
|
||
|
|
expect(stats.size).toBeGreaterThan(100);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('download triggered by a link', async ({ page }) => {
|
||
|
|
await page.goto('/files');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('link', { name: 'Download invoice' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
expect(download.suggestedFilename()).toMatch(/invoice-\d+\.pdf/);
|
||
|
|
|
||
|
|
// Use download.path() for the temp file path
|
||
|
|
const tempPath = await download.path();
|
||
|
|
expect(tempPath).toBeTruthy();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const fs = require('fs');
|
||
|
|
|
||
|
|
test('download and verify file content', async ({ page }) => {
|
||
|
|
await page.goto('/reports');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export CSV' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
expect(download.suggestedFilename()).toBe('report-2025.csv');
|
||
|
|
|
||
|
|
const savePath = 'test-results/report.csv';
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
const content = fs.readFileSync(savePath, 'utf-8');
|
||
|
|
expect(content).toContain('Name,Email,Status');
|
||
|
|
expect(content).toContain('Jane Doe,jane@example.com,Active');
|
||
|
|
|
||
|
|
const stats = fs.statSync(savePath);
|
||
|
|
expect(stats.size).toBeGreaterThan(100);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('download triggered by a link', async ({ page }) => {
|
||
|
|
await page.goto('/files');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('link', { name: 'Download invoice' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
expect(download.suggestedFilename()).toMatch(/invoice-\d+\.pdf/);
|
||
|
|
|
||
|
|
const tempPath = await download.path();
|
||
|
|
expect(tempPath).toBeTruthy();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Configuring Download Paths
|
||
|
|
|
||
|
|
**Use when**: You need downloads to go to a specific directory, or you need to disable the download dialog prompt.
|
||
|
|
**Avoid when**: Default temp paths via `download.path()` or `download.saveAs()` are sufficient.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
// playwright.config.ts — global download behavior
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
use: {
|
||
|
|
// Accept all downloads without prompting
|
||
|
|
acceptDownloads: true, // default is true
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
// Per-test download directory via a custom fixture
|
||
|
|
const downloadTest = test.extend<{ downloadDir: string }>({
|
||
|
|
downloadDir: async ({}, use, testInfo) => {
|
||
|
|
const dir = path.join('test-results', 'downloads', testInfo.title.replace(/\s+/g, '-'));
|
||
|
|
fs.mkdirSync(dir, { recursive: true });
|
||
|
|
await use(dir);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
downloadTest('save downloads to organized directories', async ({ page, downloadDir }) => {
|
||
|
|
await page.goto('/exports');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
const savePath = path.join(downloadDir, download.suggestedFilename());
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
expect(fs.existsSync(savePath)).toBe(true);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig } = require('@playwright/test');
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
use: {
|
||
|
|
acceptDownloads: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const downloadTest = test.extend({
|
||
|
|
downloadDir: async ({}, use, testInfo) => {
|
||
|
|
const dir = path.join('test-results', 'downloads', testInfo.title.replace(/\s+/g, '-'));
|
||
|
|
fs.mkdirSync(dir, { recursive: true });
|
||
|
|
await use(dir);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
downloadTest('save downloads to organized directories', async ({ page, downloadDir }) => {
|
||
|
|
await page.goto('/exports');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
const savePath = path.join(downloadDir, download.suggestedFilename());
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
expect(fs.existsSync(savePath)).toBe(true);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### File Type Validation
|
||
|
|
|
||
|
|
**Use when**: Testing that the application rejects invalid file types and accepts valid ones.
|
||
|
|
**Avoid when**: The application does no client-side validation and relies entirely on server-side checks (test via API instead).
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
|
||
|
|
test('rejects unsupported file types', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
// Upload an invalid file type
|
||
|
|
await page.getByLabel('Upload image').setInputFiles({
|
||
|
|
name: 'malware.exe',
|
||
|
|
mimeType: 'application/octet-stream',
|
||
|
|
buffer: Buffer.from('fake-exe-content'),
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('Only JPG, PNG, and GIF files are allowed')).toBeVisible();
|
||
|
|
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('accepts valid file types', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
await page.getByLabel('Upload image').setInputFiles({
|
||
|
|
name: 'photo.jpg',
|
||
|
|
mimeType: 'image/jpeg',
|
||
|
|
buffer: Buffer.from('fake-jpg-content'),
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('photo.jpg')).toBeVisible();
|
||
|
|
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('validates file size limits', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
// Create a buffer that exceeds the 5MB limit
|
||
|
|
const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'x');
|
||
|
|
|
||
|
|
await page.getByLabel('Upload document').setInputFiles({
|
||
|
|
name: 'huge-file.pdf',
|
||
|
|
mimeType: 'application/pdf',
|
||
|
|
buffer: largeBuffer,
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('File size must be under 5MB')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
|
||
|
|
test('rejects unsupported file types', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
await page.getByLabel('Upload image').setInputFiles({
|
||
|
|
name: 'malware.exe',
|
||
|
|
mimeType: 'application/octet-stream',
|
||
|
|
buffer: Buffer.from('fake-exe-content'),
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('Only JPG, PNG, and GIF files are allowed')).toBeVisible();
|
||
|
|
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('accepts valid file types', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
await page.getByLabel('Upload image').setInputFiles({
|
||
|
|
name: 'photo.jpg',
|
||
|
|
mimeType: 'image/jpeg',
|
||
|
|
buffer: Buffer.from('fake-jpg-content'),
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('photo.jpg')).toBeVisible();
|
||
|
|
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('validates file size limits', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'x');
|
||
|
|
|
||
|
|
await page.getByLabel('Upload document').setInputFiles({
|
||
|
|
name: 'huge-file.pdf',
|
||
|
|
mimeType: 'application/pdf',
|
||
|
|
buffer: largeBuffer,
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(page.getByText('File size must be under 5MB')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Large File Handling
|
||
|
|
|
||
|
|
**Use when**: Testing uploads or downloads of large files where timeouts and progress indicators matter.
|
||
|
|
**Avoid when**: Every test. Large file tests are slow. Run them in a separate suite or tag them for nightly runs.
|
||
|
|
|
||
|
|
**TypeScript**
|
||
|
|
```typescript
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
test.describe('large file operations', () => {
|
||
|
|
// Increase timeout for the entire describe block
|
||
|
|
test.slow(); // Triples the default timeout
|
||
|
|
|
||
|
|
test('upload large file with progress tracking', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
// Create a test file on disk (avoid Buffer.alloc for very large files)
|
||
|
|
const largePath = path.join('test-results', 'large-test-file.bin');
|
||
|
|
const stream = fs.createWriteStream(largePath);
|
||
|
|
for (let i = 0; i < 100; i++) {
|
||
|
|
stream.write(Buffer.alloc(1024 * 1024, 'a')); // 100MB total
|
||
|
|
}
|
||
|
|
stream.end();
|
||
|
|
await new Promise((resolve) => stream.on('finish', resolve));
|
||
|
|
|
||
|
|
await page.getByLabel('Upload file').setInputFiles(largePath);
|
||
|
|
|
||
|
|
// Wait for progress indicator
|
||
|
|
await expect(page.getByRole('progressbar')).toBeVisible();
|
||
|
|
|
||
|
|
// Wait for completion — extended timeout
|
||
|
|
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 120_000 });
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
fs.unlinkSync(largePath);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('download large file', async ({ page }) => {
|
||
|
|
await page.goto('/exports');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export full dataset' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
const savePath = 'test-results/large-export.zip';
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
// Verify file size is reasonable (at least 10MB)
|
||
|
|
const stats = fs.statSync(savePath);
|
||
|
|
expect(stats.size).toBeGreaterThan(10 * 1024 * 1024);
|
||
|
|
|
||
|
|
fs.unlinkSync(savePath);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**JavaScript**
|
||
|
|
```javascript
|
||
|
|
const { test, expect } = require('@playwright/test');
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
test.describe('large file operations', () => {
|
||
|
|
test.slow();
|
||
|
|
|
||
|
|
test('upload large file with progress tracking', async ({ page }) => {
|
||
|
|
await page.goto('/upload');
|
||
|
|
|
||
|
|
const largePath = path.join('test-results', 'large-test-file.bin');
|
||
|
|
const stream = fs.createWriteStream(largePath);
|
||
|
|
for (let i = 0; i < 100; i++) {
|
||
|
|
stream.write(Buffer.alloc(1024 * 1024, 'a'));
|
||
|
|
}
|
||
|
|
stream.end();
|
||
|
|
await new Promise((resolve) => stream.on('finish', resolve));
|
||
|
|
|
||
|
|
await page.getByLabel('Upload file').setInputFiles(largePath);
|
||
|
|
|
||
|
|
await expect(page.getByRole('progressbar')).toBeVisible();
|
||
|
|
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 120_000 });
|
||
|
|
|
||
|
|
fs.unlinkSync(largePath);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('download large file', async ({ page }) => {
|
||
|
|
await page.goto('/exports');
|
||
|
|
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await page.getByRole('button', { name: 'Export full dataset' }).click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
|
||
|
|
const savePath = 'test-results/large-export.zip';
|
||
|
|
await download.saveAs(savePath);
|
||
|
|
|
||
|
|
const stats = fs.statSync(savePath);
|
||
|
|
expect(stats.size).toBeGreaterThan(10 * 1024 * 1024);
|
||
|
|
|
||
|
|
fs.unlinkSync(savePath);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Decision Guide
|
||
|
|
|
||
|
|
| Scenario | Approach | Key API |
|
||
|
|
|---|---|---|
|
||
|
|
| Standard `<input type="file">` | `setInputFiles()` on the locator | `locator.setInputFiles(path)` |
|
||
|
|
| Hidden file input | Find the input even if hidden, call `setInputFiles()` | `page.locator('input[type="file"]').setInputFiles()` |
|
||
|
|
| Custom button opens file picker | Listen for `filechooser` event before clicking | `page.waitForEvent('filechooser')` |
|
||
|
|
| Drag-and-drop zone with no input | Dispatch `drop` event with `DataTransfer` | `locator.dispatchEvent('drop', ...)` |
|
||
|
|
| DnD zone with hidden input fallback | Prefer `setInputFiles()` on the hidden input | Check `input[type="file"]` count first |
|
||
|
|
| Multiple files at once | Pass array to `setInputFiles()` | `setInputFiles([path1, path2])` |
|
||
|
|
| In-memory test file (no disk) | Pass object with `name`, `mimeType`, `buffer` | `setInputFiles({ name, mimeType, buffer })` |
|
||
|
|
| Download — verify filename | `download.suggestedFilename()` | `page.waitForEvent('download')` |
|
||
|
|
| Download — verify content | `download.saveAs()` then read with `fs` | `fs.readFileSync()` |
|
||
|
|
| Download — temp file only | `download.path()` returns temp location | Auto-deleted after test |
|
||
|
|
| Large file upload/download | Use `test.slow()`, increase assertion timeouts | `{ timeout: 120_000 }` |
|
||
|
|
|
||
|
|
## Anti-Patterns
|
||
|
|
|
||
|
|
| Don't Do This | Problem | Do This Instead |
|
||
|
|
|---|---|---|
|
||
|
|
| `await page.waitForTimeout(3000)` after upload | Arbitrary delay; flaky | `await expect(page.getByText('Upload complete')).toBeVisible()` |
|
||
|
|
| `await page.setInputFiles('#file', path)` using CSS selector | Fragile selector; breaks on ID changes | `await page.getByLabel('Upload').setInputFiles(path)` |
|
||
|
|
| Clicking download then immediately reading the file | Race condition; file may not be written yet | Use `page.waitForEvent('download')` then `download.saveAs()` |
|
||
|
|
| Creating large test files in `beforeAll` and never cleaning up | Fills disk in CI, slows down subsequent runs | Clean up in `afterAll` or use fixture teardown |
|
||
|
|
| Using `page.on('filechooser')` for every upload | Unnecessary complexity when `<input type="file">` exists | Use `setInputFiles()` directly |
|
||
|
|
| Hardcoding absolute file paths | Breaks across machines and CI | Use `path.join(__dirname, ...)` or relative to project root |
|
||
|
|
| Testing file upload with empty buffer | Does not test real validation behavior | Use realistic file content or minimum valid file size |
|
||
|
|
| Using `download.path()` for permanent storage | Temp files are cleaned up after test context closes | Use `download.saveAs()` for permanent paths |
|
||
|
|
| Uploading real 100MB files in every test run | Slows entire suite, wastes CI resources | Tag large file tests separately; run on schedule, not every PR |
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### "FileChooser event was not emitted"
|
||
|
|
|
||
|
|
**Cause**: The click did not open a native file picker dialog. The upload component may use a different mechanism.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Debug: check if there is a hidden <input type="file"> you can target directly
|
||
|
|
const fileInputCount = await page.locator('input[type="file"]').count();
|
||
|
|
console.log(`Found ${fileInputCount} file inputs`);
|
||
|
|
|
||
|
|
// If input exists, skip the file chooser approach entirely
|
||
|
|
if (fileInputCount > 0) {
|
||
|
|
await page.locator('input[type="file"]').setInputFiles('fixtures/file.pdf');
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### "Download event was not emitted"
|
||
|
|
|
||
|
|
**Cause**: The link opens in a new tab or navigates to the file URL instead of triggering a download.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Fix 1: Ensure acceptDownloads is true in config
|
||
|
|
// Fix 2: Check if the link opens a new tab — handle it as a new page
|
||
|
|
const pagePromise = page.context().waitForEvent('page');
|
||
|
|
await page.getByRole('link', { name: 'Download' }).click();
|
||
|
|
const newPage = await pagePromise;
|
||
|
|
// Then wait for the download event on the new page
|
||
|
|
const download = await newPage.waitForEvent('download');
|
||
|
|
```
|
||
|
|
|
||
|
|
### Upload works locally but fails in CI
|
||
|
|
|
||
|
|
**Cause**: File paths are wrong in CI, or the fixture files are not included in the repo/build.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Fix: always resolve paths relative to the test file
|
||
|
|
import path from 'path';
|
||
|
|
const fixturePath = path.join(__dirname, '..', 'fixtures', 'test-file.pdf');
|
||
|
|
|
||
|
|
// Verify the file exists before uploading
|
||
|
|
import fs from 'fs';
|
||
|
|
if (!fs.existsSync(fixturePath)) {
|
||
|
|
throw new Error(`Fixture file missing: ${fixturePath}`);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `setInputFiles` does nothing — no file appears
|
||
|
|
|
||
|
|
**Cause**: The input element is detached, inside a Shadow DOM, or in an iframe.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Check for iframe
|
||
|
|
const frame = page.frameLocator('iframe[title="Upload"]');
|
||
|
|
await frame.locator('input[type="file"]').setInputFiles('fixtures/file.pdf');
|
||
|
|
|
||
|
|
// Check for Shadow DOM — Playwright pierces open shadow roots automatically
|
||
|
|
// but the input may be in a closed shadow root (rare). Use the file chooser approach instead.
|
||
|
|
```
|
||
|
|
|
||
|
|
## Related
|
||
|
|
|
||
|
|
- [core/locators.md](locators.md) -- locator strategies for finding upload/download elements
|
||
|
|
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- assertion patterns for verifying upload/download results
|
||
|
|
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- creating reusable download directory fixtures
|
||
|
|
- [core/network-mocking.md](network-mocking.md) -- mocking upload endpoints for faster tests
|
||
|
|
- [core/error-and-edge-cases.md](error-and-edge-cases.md) -- testing upload failure states and error handling
|