Back to Playwright & UI Automation
Q4Question 4 of 7

How Playwright handles file uploads

Key Concepts
  • setInputFiles on a standard file input sets files directly without opening the OS dialog, making it reliable and headless-friendly
  • Pass one path for single file, an array for multiple files, or an in-memory buffer with name and MIME type for generated test files
  • For custom upload controls, capture the filechooser event before clicking, then call setFiles on the FileChooser object
  • Client-side verification: assert file name appears, thumbnail renders, progress completes, or success message shows
  • Server-side verification is stronger: assert the uploaded document appears in the account's list via API with correct name, size, and type
  • Negative cases: uploading a disallowed type or oversized file must be rejected with the correct error message

Definition

Use setInputFiles on a file input for single, multiple, or in-memory files, or capture the filechooser event and call setFiles for custom controls, then verify both the UI signal and the server-side result to confirm the correct file actually uploaded.
  • setInputFiles – Sets files directly on a standard HTML file input without opening the OS dialog. Pass one path, an array of paths, or an in-memory buffer.
  • filechooser event – For custom upload controls that open the native file dialog, register a handler before clicking, then call setFiles on the FileChooser object.
  • Client-side verification – Assert file name appears, thumbnail renders, progress completes, or success message shows.
  • Server-side verification – Assert the uploaded document appears via API or database with correct name, size, and type.
  • Negative cases – Disallowed file types and oversized files must be rejected with the correct error.

When Priya uploads her address-proof PDF to the National Bank of India onboarding screen, Playwright does not fight with the operating system's file-picker window. If the page uses a normal file box, Playwright hands the file straight to that box. If the page uses a fancy custom upload button, Playwright intercepts the dialog about to open and hands over the file programmatically. Then the tester checks two things: that the screen shows "address-proof.pdf uploaded," and that the back office actually received the right document.

Understanding the Concepts

How does setInputFiles work for standard file inputs?

For a standard HTML file input, locate the input and call setInputFiles with the file path. Playwright sets the file directly without opening the OS dialog.

TYPESCRIPT
// Single file upload
await page.getByLabel('Upload Document').setInputFiles('fixtures/address-proof.pdf');

// Multiple files (input must have 'multiple' attribute)
await page.getByLabel('Upload Documents').setInputFiles([
  'fixtures/id-proof.pdf',
  'fixtures/address-proof.pdf',
]);

// Clear a selection
await page.getByLabel('Upload Document').setInputFiles([]);

How do you upload from memory without a fixture file?

Pass an object with name, MIME type, and buffer to generate test files on the fly.

TYPESCRIPT
// In-memory file: no fixture needed
await page.getByLabel('Upload Statement').setInputFiles({
  name: 'statement-2024.csv',
  mimeType: 'text/csv',
  buffer: Buffer.from(
    'Date,Description,Amount\n2024-01-15,Salary Credit,50000.00\n'
  ),
});

// Generate a synthetic PDF
import { randomBytes } from 'crypto';
await page.getByLabel('Upload Document').setInputFiles({
  name: 'test-doc.pdf',
  mimeType: 'application/pdf',
  buffer: Buffer.from('%PDF-1.4 ' + randomBytes(100).toString('hex')),
});

How do you handle custom upload controls with filechooser?

For custom controls that open the native file dialog on click, capture the filechooser event.

TYPESCRIPT
// Custom upload: intercept the file chooser dialog
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose File' }).click();
const fileChooser = await fileChooserPromise;

// Set the file on the chooser
await fileChooser.setFiles('fixtures/address-proof.pdf');

// Check if multiple files are accepted
console.log('Accepts multiple:', fileChooser.isMultiple());

How do you verify the upload client-side?

Assert the UI reflects the successful upload using auto-waiting assertions.

TYPESCRIPT
// Client-side verification
await page.getByLabel('Upload Document').setInputFiles('fixtures/id-proof.pdf');

// File name appears
await expect(page.getByText('id-proof.pdf')).toBeVisible();

// Upload progress completes
await expect(page.getByTestId('upload-progress')).toHaveAttribute(
  'aria-valuenow', '100'
);

// Success message
await expect(page.getByText('Document uploaded successfully')).toBeVisible();

How do you verify the upload server-side?

Server-side verification is stronger. Assert the document exists via API with correct metadata.

TYPESCRIPT
// Server-side verification via API
const response = await page.request.get(
  '/api/accounts/ACC-001/documents'
);
const documents = await response.json();

const uploaded = documents.find(
  (d: any) => d.name === 'id-proof.pdf'
);
expect(uploaded).toBeDefined();
expect(uploaded.type).toBe('application/pdf');
expect(uploaded.size).toBeGreaterThan(0);

How do you test negative upload cases?

Disallowed types and oversized files must be rejected with the correct error.

TYPESCRIPT
// Negative: disallowed file type
await page.getByLabel('Upload Document').setInputFiles({
  name: 'malware.exe',
  mimeType: 'application/octet-stream',
  buffer: Buffer.from('fake executable'),
});
await expect(
  page.getByText('File type not allowed')
).toBeVisible();

// Negative: oversized file (simulate with large buffer)
await page.getByLabel('Upload Document').setInputFiles({
  name: 'huge-file.pdf',
  mimeType: 'application/pdf',
  buffer: Buffer.alloc(11 * 1024 * 1024), // 11MB
});
await expect(
  page.getByText('File exceeds maximum size')
).toBeVisible();

What is the complete upload test pattern?

A complete upload test covers the happy path, server verification, and negative cases.

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

test.describe('Document Upload', () => {
  test('Upload address proof and verify', async ({ page }) => {
    await page.goto('/onboarding/documents');
    await page.getByLabel('Address Proof').setInputFiles(
      'fixtures/address-proof.pdf'
    );
    await expect(page.getByText('address-proof.pdf')).toBeVisible();
    await page.getByRole('button', { name: 'Submit' }).click();
    await expect(page.getByText('Document uploaded')).toBeVisible();

    // Verify server-side
    const docs = await (await page.request.get(
      '/api/accounts/ACC-001/documents'
    )).json();
    expect(docs.some((d: any) => d.name === 'address-proof.pdf')).toBe(true);
  });

  test('Reject executable file', async ({ page }) => {
    await page.goto('/onboarding/documents');
    await page.getByLabel('Address Proof').setInputFiles({
      name: 'script.exe',
      mimeType: 'application/octet-stream',
      buffer: Buffer.from('fake'),
    });
    await expect(page.getByText('File type not allowed')).toBeVisible();
  });
});

What are common interview follow-up questions?

  • "When should you use setInputFiles vs filechooser?" Prefer setInputFiles when a real file input exists. Use filechooser only for custom controls that open the OS dialog.
  • "How do you test drag-and-drop uploads?" Playwright's dragTo or dispatchEvent can simulate drag-and-drop, but setInputFiles on the underlying input is often simpler and more reliable.
  • "Should you commit binary test fixtures?" Prefer in-memory buffers for small files. Only commit binary fixtures when the file content matters for the test.

Visual Overview

DIAGRAM
  File Upload: Two Mechanisms
  ───────────────────────────

  Standard file input:            Custom upload control:
  ┌─────────────────────┐         ┌─────────────────────┐
  │ <input type="file"> │         │ <button>Choose</button>
  │                     │         │   (opens OS dialog)  │
  │ setInputFiles(path) │         │                     │
  │ - Single file       │         │ 1. waitForEvent      │
  │ - Multiple files    │         │    ('filechooser')   │
  │ - In-memory buffer  │         │ 2. click the button  │
  │                     │         │ 3. setFiles(path)    │
  │ No OS dialog opens  │         │ Dialog intercepted   │
  └─────────────────────┘         └─────────────────────┘
           │                               │
           └───────────┬───────────────────┘
                       │
           ┌───────────▼───────────┐
           │  VERIFY               │
           │  Client: file name,   │
           │    thumbnail, message │
           │  Server: API check    │
           │    name, size, type   │
           │  Negative: reject     │
           │    bad type, oversize │
           └───────────────────────┘

Key Takeaway

Playwright handles file uploads in two ways. For a standard file input I use setInputFiles: one path for a single file, an array for multiple files, or an in-memory buffer for generated test data. For custom upload controls I capture the filechooser event before clicking, then call setFiles on the FileChooser object. Both bypass the OS dialog, making tests reliable and headless-friendly.

The key emphasis is verification. Client-side, I assert the file name appears and a success message shows. Server-side, which is stronger, I assert the document exists via API with correct name, size, and type. And I always include negative cases: disallowed types and oversized files must be rejected with the correct error.

At the National Bank of India our onboarding let customers upload compliance documents, so proving the correct file truly arrived was a real requirement. I used setInputFiles for standard inputs, generated synthetic PDFs as in-memory buffers, and verified uploads through the API.

Remember one sentence: use setInputFiles for standard inputs and filechooser for custom controls, then verify both client-side and server-side to confirm the correct file actually uploaded.

---

Share
Use ← → arrow keys to navigate