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

30 KiB
Executable file
Raw Blame History

File Upload and Download Recipes

When to use: You need to test file uploads (single, multiple, drag-and-drop), file downloads (verify content, filename, type), upload progress, or file type restrictions.


Recipe 1: Single File Upload via Input

Complete Example

TypeScript

import { test, expect } from '@playwright/test';
import path from 'path';

test('uploads a single file via file input', async ({ page }) => {
  await page.goto('/documents');

  // Click the upload button which triggers the hidden file input
  const fileInput = page.locator('input[type="file"]');

  // Upload a file from the test fixtures directory
  await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));

  // Verify the file name appears in the UI
  await expect(page.getByText('report.pdf')).toBeVisible();

  // Click the submit/upload button
  await page.getByRole('button', { name: 'Upload' }).click();

  // Wait for the upload to complete
  await expect(page.getByRole('alert')).toContainText('File uploaded successfully');

  // Verify the file appears in the documents list
  await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
});

test('uploads a file created in-memory (no fixture file needed)', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Create a file from a buffer -- no fixture file required
  await fileInput.setInputFiles({
    name: 'test-data.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin\nBob,bob@example.com,user'),
  });

  await expect(page.getByText('test-data.csv')).toBeVisible();

  await page.getByRole('button', { name: 'Upload' }).click();

  await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
});

test('clears a selected file before uploading', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Select a file
  await fileInput.setInputFiles({
    name: 'wrong-file.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('wrong content'),
  });

  await expect(page.getByText('wrong-file.txt')).toBeVisible();

  // Clear the selection
  await fileInput.setInputFiles([]);

  // Or click a "Remove" button in the UI
  // await page.getByRole('button', { name: 'Remove' }).click();

  await expect(page.getByText('wrong-file.txt')).not.toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');
const path = require('path');

test('uploads a single file via file input', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));

  await expect(page.getByText('report.pdf')).toBeVisible();

  await page.getByRole('button', { name: 'Upload' }).click();

  await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
  await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
});

test('uploads a file created in-memory', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles({
    name: 'test-data.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin'),
  });

  await expect(page.getByText('test-data.csv')).toBeVisible();
  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
});

Recipe 2: Multiple File Upload

Complete Example

TypeScript

import { test, expect } from '@playwright/test';
import path from 'path';

test('uploads multiple files at once', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Pass an array to upload multiple files
  await fileInput.setInputFiles([
    path.resolve(__dirname, '../fixtures/report.pdf'),
    path.resolve(__dirname, '../fixtures/photo.jpg'),
    path.resolve(__dirname, '../fixtures/data.csv'),
  ]);

  // Verify all file names appear
  await expect(page.getByText('report.pdf')).toBeVisible();
  await expect(page.getByText('photo.jpg')).toBeVisible();
  await expect(page.getByText('data.csv')).toBeVisible();

  // Verify the count indicator
  await expect(page.getByText('3 files selected')).toBeVisible();

  await page.getByRole('button', { name: 'Upload all' }).click();

  await expect(page.getByRole('alert')).toContainText('3 files uploaded');
});

test('uploads multiple files with in-memory buffers', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles([
    {
      name: 'notes.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('Meeting notes for Q4 planning'),
    },
    {
      name: 'config.json',
      mimeType: 'application/json',
      buffer: Buffer.from(JSON.stringify({ theme: 'dark', lang: 'en' })),
    },
  ]);

  await expect(page.getByText('notes.txt')).toBeVisible();
  await expect(page.getByText('config.json')).toBeVisible();

  await page.getByRole('button', { name: 'Upload all' }).click();
  await expect(page.getByRole('alert')).toContainText('2 files uploaded');
});

test('removes one file from a multi-file selection', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles([
    { name: 'keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
    { name: 'remove.txt', mimeType: 'text/plain', buffer: Buffer.from('remove') },
    { name: 'also-keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
  ]);

  // Remove a specific file from the preview list
  const removeItem = page.getByText('remove.txt').locator('..');
  await removeItem.getByRole('button', { name: /remove|delete|×/i }).click();

  await expect(page.getByText('remove.txt')).not.toBeVisible();
  await expect(page.getByText('keep.txt')).toBeVisible();
  await expect(page.getByText('also-keep.txt')).toBeVisible();
  await expect(page.getByText('2 files selected')).toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');
const path = require('path');

test('uploads multiple files at once', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles([
    path.resolve(__dirname, '../fixtures/report.pdf'),
    path.resolve(__dirname, '../fixtures/photo.jpg'),
    path.resolve(__dirname, '../fixtures/data.csv'),
  ]);

  await expect(page.getByText('report.pdf')).toBeVisible();
  await expect(page.getByText('photo.jpg')).toBeVisible();
  await expect(page.getByText('data.csv')).toBeVisible();

  await page.getByRole('button', { name: 'Upload all' }).click();
  await expect(page.getByRole('alert')).toContainText('3 files uploaded');
});

Recipe 3: Drag-and-Drop File Upload

Complete Example

TypeScript

import { test, expect } from '@playwright/test';
import path from 'path';

test('uploads a file via drag-and-drop zone', async ({ page }) => {
  await page.goto('/documents');

  const dropZone = page.locator('[data-testid="drop-zone"]');
  await expect(dropZone).toContainText(/drag.*here|drop.*files/i);

  // Drag-and-drop from the OS is not natively supported in Playwright,
  // but drop zones always have an underlying input[type=file] -- use that
  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));

  // File preview should appear in the drop zone
  await expect(dropZone.getByText('report.pdf')).toBeVisible();

  // Trigger upload
  await page.getByRole('button', { name: 'Upload' }).click();

  await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});

test('shows visual feedback during drag-over', async ({ page }) => {
  await page.goto('/documents');

  const dropZone = page.locator('[data-testid="drop-zone"]');

  // Simulate dragenter to show visual feedback
  await dropZone.dispatchEvent('dragenter', {
    dataTransfer: { types: ['Files'], files: [] },
  });

  // Drop zone should show an active state
  await expect(dropZone).toHaveClass(/active|highlight|drag-over/);
  await expect(dropZone).toContainText(/release|drop now/i);

  // Simulate dragleave to reset
  await dropZone.dispatchEvent('dragleave');

  await expect(dropZone).not.toHaveClass(/active|highlight|drag-over/);
});

test('handles multiple files dropped at once', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles([
    { name: 'image1.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-1') },
    { name: 'image2.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-2') },
    { name: 'image3.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-3') },
  ]);

  await expect(page.getByText('image1.png')).toBeVisible();
  await expect(page.getByText('image2.png')).toBeVisible();
  await expect(page.getByText('image3.png')).toBeVisible();
});

JavaScript

const { test, expect } = require('@playwright/test');
const path = require('path');

test('uploads a file via drag-and-drop zone', async ({ page }) => {
  await page.goto('/documents');

  const dropZone = page.locator('[data-testid="drop-zone"]');
  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));

  await expect(dropZone.getByText('report.pdf')).toBeVisible();

  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});

Recipe 4: Download and Verify File Content

Complete Example

TypeScript

import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';

test('downloads a file and verifies its content', async ({ page }) => {
  await page.goto('/documents');

  // Start waiting for the download event BEFORE clicking
  const downloadPromise = page.waitForEvent('download');

  await page.getByRole('link', { name: 'report.csv' }).click();

  const download = await downloadPromise;

  // Save to a temporary location
  const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
  await download.saveAs(downloadPath);

  // Read and verify the content
  const content = fs.readFileSync(downloadPath, 'utf-8');

  expect(content).toContain('name,email,role');
  expect(content).toContain('Jane,jane@example.com,admin');

  // Verify line count
  const lines = content.trim().split('\n');
  expect(lines.length).toBeGreaterThan(1); // header + at least one data row

  // Clean up
  fs.unlinkSync(downloadPath);
});

test('downloads a JSON file and verifies structure', async ({ page }) => {
  await page.goto('/api-docs');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Export API spec' }).click();

  const download = await downloadPromise;
  const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
  await download.saveAs(downloadPath);

  const content = JSON.parse(fs.readFileSync(downloadPath, 'utf-8'));

  expect(content).toHaveProperty('openapi');
  expect(content.paths).toBeDefined();
  expect(Object.keys(content.paths).length).toBeGreaterThan(0);

  fs.unlinkSync(downloadPath);
});

test('downloads a file via the stream (no disk save needed)', async ({ page }) => {
  await page.goto('/documents');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'report.csv' }).click();

  const download = await downloadPromise;

  // Read directly from the download stream without saving to disk
  const readable = await download.createReadStream();
  const chunks: Buffer[] = [];

  for await (const chunk of readable!) {
    chunks.push(Buffer.from(chunk));
  }

  const content = Buffer.concat(chunks).toString('utf-8');
  expect(content).toContain('name,email,role');
});

JavaScript

const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');

test('downloads a file and verifies its content', async ({ page }) => {
  await page.goto('/documents');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'report.csv' }).click();

  const download = await downloadPromise;

  const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
  await download.saveAs(downloadPath);

  const content = fs.readFileSync(downloadPath, 'utf-8');
  expect(content).toContain('name,email,role');
  expect(content).toContain('Jane,jane@example.com,admin');

  fs.unlinkSync(downloadPath);
});

test('downloads a file via the stream', async ({ page }) => {
  await page.goto('/documents');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'report.csv' }).click();

  const download = await downloadPromise;

  const readable = await download.createReadStream();
  const chunks = [];

  for await (const chunk of readable) {
    chunks.push(Buffer.from(chunk));
  }

  const content = Buffer.concat(chunks).toString('utf-8');
  expect(content).toContain('name,email,role');
});

Recipe 5: Download and Verify Filename

Complete Example

TypeScript

import { test, expect } from '@playwright/test';

test('download has correct filename and extension', async ({ page }) => {
  await page.goto('/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Export as PDF' }).click();

  const download = await downloadPromise;

  // Verify filename pattern
  expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
});

test('download filename changes based on selected format', async ({ page }) => {
  await page.goto('/reports');

  // Select CSV format
  await page.getByLabel('Export format').selectOption('csv');

  const csvDownload = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Download' }).click();
  const csv = await csvDownload;
  expect(csv.suggestedFilename()).toMatch(/\.csv$/);

  // Select Excel format
  await page.getByLabel('Export format').selectOption('xlsx');

  const xlsxDownload = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Download' }).click();
  const xlsx = await xlsxDownload;
  expect(xlsx.suggestedFilename()).toMatch(/\.xlsx$/);
});

test('download has correct MIME type via response headers', async ({ page }) => {
  await page.goto('/reports');

  // Intercept the download request to check response headers
  const responsePromise = page.waitForResponse('**/api/reports/export**');
  const downloadPromise = page.waitForEvent('download');

  await page.getByRole('button', { name: 'Export as PDF' }).click();

  const response = await responsePromise;
  expect(response.headers()['content-type']).toContain('application/pdf');
  expect(response.headers()['content-disposition']).toContain('attachment');

  await downloadPromise; // Consume the download event
});

test('download failure shows error to user', async ({ page }) => {
  // Mock the download endpoint to fail
  await page.route('**/api/reports/export**', async (route) => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Report generation failed' }),
    });
  });

  await page.goto('/reports');
  await page.getByRole('button', { name: 'Export as PDF' }).click();

  await expect(page.getByRole('alert')).toContainText(/failed|error/i);
});

JavaScript

const { test, expect } = require('@playwright/test');

test('download has correct filename and extension', async ({ page }) => {
  await page.goto('/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Export as PDF' }).click();

  const download = await downloadPromise;
  expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
});

test('download filename changes based on selected format', async ({ page }) => {
  await page.goto('/reports');

  await page.getByLabel('Export format').selectOption('csv');

  const csvDownload = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Download' }).click();
  const csv = await csvDownload;
  expect(csv.suggestedFilename()).toMatch(/\.csv$/);
});

Recipe 6: Large File Upload with Progress

Complete Example

TypeScript

import { test, expect } from '@playwright/test';

test('shows upload progress for a large file', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Create a large in-memory file (5 MB)
  const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');

  await fileInput.setInputFiles({
    name: 'large-dataset.bin',
    mimeType: 'application/octet-stream',
    buffer: largeBuffer,
  });

  await page.getByRole('button', { name: 'Upload' }).click();

  // Verify progress bar appears
  const progressBar = page.getByRole('progressbar');
  await expect(progressBar).toBeVisible();

  // Verify progress percentage updates
  // Use polling to check that progress increases
  await expect(async () => {
    const value = await progressBar.getAttribute('aria-valuenow');
    expect(Number(value)).toBeGreaterThan(0);
  }).toPass({ timeout: 10000 });

  // Wait for upload to complete
  await expect(progressBar).not.toBeVisible({ timeout: 60000 });
  await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});

test('cancels an in-progress upload', async ({ page }) => {
  // Slow down the upload API to simulate a long upload
  await page.route('**/api/documents/upload', async (route) => {
    // Delay for 10 seconds to give us time to cancel
    await new Promise((resolve) => setTimeout(resolve, 10000));
    await route.continue();
  });

  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');
  const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');

  await fileInput.setInputFiles({
    name: 'large-file.bin',
    mimeType: 'application/octet-stream',
    buffer: largeBuffer,
  });

  await page.getByRole('button', { name: 'Upload' }).click();

  // Wait for upload to start
  await expect(page.getByRole('progressbar')).toBeVisible();

  // Cancel the upload
  await page.getByRole('button', { name: 'Cancel upload' }).click();

  // Verify upload was cancelled
  await expect(page.getByRole('progressbar')).not.toBeVisible();
  await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();

  // Verify the file did not appear in the documents list
  await expect(page.getByRole('link', { name: 'large-file.bin' })).not.toBeVisible();
});

test('retries a failed upload', async ({ page }) => {
  let attempt = 0;

  await page.route('**/api/documents/upload', async (route) => {
    attempt++;
    if (attempt === 1) {
      // First attempt fails
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Server error' }),
      });
    } else {
      // Second attempt succeeds
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ id: '123', name: 'data.csv' }),
      });
    }
  });

  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles({
    name: 'data.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from('col1,col2\nval1,val2'),
  });

  await page.getByRole('button', { name: 'Upload' }).click();

  // First attempt fails
  await expect(page.getByText(/upload failed|error/i)).toBeVisible();

  // Retry
  await page.getByRole('button', { name: /retry/i }).click();

  // Second attempt succeeds
  await expect(page.getByRole('alert')).toContainText('uploaded successfully');
  expect(attempt).toBe(2);
});

JavaScript

const { test, expect } = require('@playwright/test');

test('shows upload progress for a large file', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');

  await fileInput.setInputFiles({
    name: 'large-dataset.bin',
    mimeType: 'application/octet-stream',
    buffer: largeBuffer,
  });

  await page.getByRole('button', { name: 'Upload' }).click();

  const progressBar = page.getByRole('progressbar');
  await expect(progressBar).toBeVisible();

  await expect(progressBar).not.toBeVisible({ timeout: 60000 });
  await expect(page.getByRole('alert')).toContainText('uploaded successfully');
});

test('cancels an in-progress upload', async ({ page }) => {
  await page.route('**/api/documents/upload', async (route) => {
    await new Promise((resolve) => setTimeout(resolve, 10000));
    await route.continue();
  });

  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles({
    name: 'large-file.bin',
    mimeType: 'application/octet-stream',
    buffer: Buffer.alloc(5 * 1024 * 1024, 'x'),
  });

  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.getByRole('progressbar')).toBeVisible();

  await page.getByRole('button', { name: 'Cancel upload' }).click();

  await expect(page.getByRole('progressbar')).not.toBeVisible();
  await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();
});

Recipe 7: Testing File Type Restrictions

Complete Example

TypeScript

import { test, expect } from '@playwright/test';

test('accepts allowed file types', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Verify the accept attribute on the file input
  await expect(fileInput).toHaveAttribute('accept', /\.pdf|\.doc|\.docx|\.txt/);

  // Upload an allowed file type
  await fileInput.setInputFiles({
    name: 'valid-document.pdf',
    mimeType: 'application/pdf',
    buffer: Buffer.from('fake-pdf-content'),
  });

  // Should be accepted without error
  await expect(page.getByText('valid-document.pdf')).toBeVisible();
  await expect(page.getByText(/not allowed|invalid/i)).not.toBeVisible();
});

test('rejects disallowed file types with clear error message', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Upload a disallowed file type
  // Note: setInputFiles bypasses the browser's accept attribute filter,
  // so the app's JavaScript validation is what we are testing here
  await fileInput.setInputFiles({
    name: 'script.exe',
    mimeType: 'application/x-msdownload',
    buffer: Buffer.from('fake-exe-content'),
  });

  // App should show a rejection message
  await expect(page.getByRole('alert')).toContainText(
    /not allowed|unsupported file type|only .pdf, .doc/i
  );

  // File should not appear in the upload queue
  await expect(page.getByText('script.exe')).not.toBeVisible();
});

test('enforces maximum file size', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Create a file that exceeds the max size (e.g., 11 MB when limit is 10 MB)
  const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');

  await fileInput.setInputFiles({
    name: 'huge-file.pdf',
    mimeType: 'application/pdf',
    buffer: oversizedBuffer,
  });

  await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
  await expect(page.getByText('huge-file.pdf')).not.toBeVisible();
});

test('enforces maximum number of files', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  // Try to upload more files than the limit (e.g., limit is 5)
  const files = Array.from({ length: 6 }, (_, i) => ({
    name: `file-${i + 1}.txt`,
    mimeType: 'text/plain' as const,
    buffer: Buffer.from(`content ${i + 1}`),
  }));

  await fileInput.setInputFiles(files);

  await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
});

test('validates image dimensions for avatar upload', async ({ page }) => {
  await page.goto('/settings/profile');

  const fileInput = page.locator('input[type="file"]');

  // Create a tiny 1x1 PNG (valid format but too small)
  // Minimal PNG buffer
  const tinyPng = Buffer.from(
    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
    'base64'
  );

  await fileInput.setInputFiles({
    name: 'tiny-avatar.png',
    mimeType: 'image/png',
    buffer: tinyPng,
  });

  await expect(page.getByRole('alert')).toContainText(/minimum.*dimensions|too small/i);
});

JavaScript

const { test, expect } = require('@playwright/test');

test('rejects disallowed file types with clear error message', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles({
    name: 'script.exe',
    mimeType: 'application/x-msdownload',
    buffer: Buffer.from('fake-exe-content'),
  });

  await expect(page.getByRole('alert')).toContainText(
    /not allowed|unsupported file type|only .pdf, .doc/i
  );
  await expect(page.getByText('script.exe')).not.toBeVisible();
});

test('enforces maximum file size', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');

  await fileInput.setInputFiles({
    name: 'huge-file.pdf',
    mimeType: 'application/pdf',
    buffer: oversizedBuffer,
  });

  await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
});

test('enforces maximum number of files', async ({ page }) => {
  await page.goto('/documents');

  const fileInput = page.locator('input[type="file"]');

  const files = Array.from({ length: 6 }, (_, i) => ({
    name: `file-${i + 1}.txt`,
    mimeType: 'text/plain',
    buffer: Buffer.from(`content ${i + 1}`),
  }));

  await fileInput.setInputFiles(files);

  await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
});

Variations

Upload via File Chooser Dialog

test('uploads via the native file chooser dialog', async ({ page }) => {
  await page.goto('/documents');

  // Listen for the file chooser event before clicking the trigger
  const fileChooserPromise = page.waitForEvent('filechooser');
  await page.getByRole('button', { name: 'Choose file' }).click();

  const fileChooser = await fileChooserPromise;

  // Verify it accepts only certain types
  expect(fileChooser.isMultiple()).toBe(false);

  await fileChooser.setFiles({
    name: 'chosen-file.pdf',
    mimeType: 'application/pdf',
    buffer: Buffer.from('pdf-content'),
  });

  await expect(page.getByText('chosen-file.pdf')).toBeVisible();
});

Upload with Image Preview

test('shows image preview after selecting a file', async ({ page }) => {
  await page.goto('/settings/profile');

  const fileInput = page.locator('input[type="file"]');

  await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/avatar.jpg'));

  // Verify the image preview is displayed
  const preview = page.getByRole('img', { name: /preview|avatar/i });
  await expect(preview).toBeVisible();

  // Verify the preview src is a blob or data URL
  const src = await preview.getAttribute('src');
  expect(src).toMatch(/^(blob:|data:image)/);
});

Download with Authentication

test('downloads a file that requires authentication', async ({ page, request }) => {
  // Some downloads go through an API that needs auth cookies
  await page.goto('/documents');

  // The UI download works because the browser sends cookies
  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('link', { name: 'confidential-report.pdf' }).click();

  const download = await downloadPromise;
  expect(download.suggestedFilename()).toBe('confidential-report.pdf');

  // Alternatively, verify via API request (which carries the auth context)
  const response = await request.get('/api/documents/123/download');
  expect(response.ok()).toBeTruthy();
  expect(response.headers()['content-type']).toContain('application/pdf');
});

Tips

  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.


  • Playwright Upload Docs
  • Playwright Download Docs
  • 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