15 Days of Playwright - Day 5

File handling is a crucial aspect of modern web applications. From uploading profile pictures to downloading reports, users constantly interact with files.

15 Days of Playwright - Day 5
Jhonatas Matos

Jhonatas Matos

File handling is a crucial aspect of modern web applications. From uploading profile pictures to downloading reports, users constantly interact with files. Playwright provides robust APIs to handle both file uploads and downloads seamlessly in your tests.

Today, I will explore how to work with file operations, covering everything from simple uploads to complex download verification.

File Uploads

File uploads are a critical user interaction that require careful testing. Playwright provides multiple approaches to handle different upload scenarios, from simple file inputs to complex drag-and-drop interfaces.

Method 1: Direct Input Setting (Most Common)

Ideal for standard file input elements, when I see a basic <input type="file">, this is my go-to approach:

// Single file upload
await page.locator('input[type="file"]').setInputFiles('path/to/file.pdf');

// Multiple files upload
await page.locator('input[type="file"]').setInputFiles([
  'file1.pdf',
  'file2.jpg',
  'file3.png'
]);

// Clear selected files
await page.locator('input[type="file"]').setInputFiles([]);

Method 2: File Chooser Dialog Handling

Use this when dealing with custom upload buttons that trigger the native file chooser dialog. This approach is essential for:

  • Custom-designed upload buttons (not standard <input type="file">)
  • Applications that use JavaScript to trigger file selection
  • Testing the complete user flow including dialog interaction
// Wait for and handle native file chooser
const [fileChooser] = await Promise.all([
  page.waitForEvent('filechooser'),
  page.locator('#upload-button').click() // Triggers file chooser
]);

await fileChooser.setFiles('my-file.pdf');

Method 3: Dynamic File Creation

Buffer handling examples

Perfect for those times when you need to test without physical files or generate content on the fly. I use this for:

  • Creating test files on the fly without maintaining fixture files
  • Testing specific file content scenarios
  • Avoiding file system dependencies in tests
// Create and upload a file without physical file
await page.locator('input[type="file"]').setInputFiles({
  name: 'report.pdf',
  mimeType: 'application/pdf',
  buffer: Buffer.from('file content')
});

⚠️ File Path Warning: Always use absolute paths or paths relative to your test directory. Relative paths might work differently depending on where tests are executed from. Use path.join(__dirname, 'file.pdf') for reliability.

Want to dive deeper? Check out the File Uploads Guide for advanced techniques and best practices.

Handling File Downloads

Playwright makes download testing straightforward with event-based handling:

Basic Download Capture

// Wait for download and save it
const [download] = await Promise.all([
  page.waitForEvent('download'),
  page.locator('#download-button').click()
]);

// Save to specific location
await download.saveAs('/path/to/save/report.pdf');

Download Verification

test('should download and verify report content', async ({ page }) => {
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('#generate-report')
  ]);

  // Verify download properties
  expect(download.suggestedFilename()).toBe('financial-report.pdf');
  
  // Read and verify file content
  const filePath = await download.path();
  const fileContent = await fs.promises.readFile(filePath, 'utf8');
  
  expect(fileContent).toContain('Transaction Summary');
  expect(fileContent).toContain('Balance: $1,500.00');
});

Explore more: The Download Handling documentation covers advanced scenarios like progress tracking and error handling.

Real-World Scenarios

Scenario 1: Profile Picture Upload

test('should upload profile picture successfully', async ({ page }) => {
  await page.goto('/profile-settings');
  
  // Upload image file
  await page.locator('input[type="file"]').setInputFiles('tests/fixtures/avatar.jpg');
  
  // Verify upload preview
  await expect(page.locator('.image-preview')).toBeVisible();
  await expect(page.locator('.file-name')).toHaveText('avatar.jpg');
  
  // Submit form
  await page.click('#save-changes');
  
  // Verify success message
  await expect(page.locator('.alert-success'))
    .toHaveText('Profile picture updated successfully');
});

Scenario 2: Bulk Document Upload

test('should handle multiple file uploads', async ({ page }) => {
  await page.goto('/document-upload');
  
  // Upload multiple documents
  await page.locator('#documents-input').setInputFiles([
    'tests/fixtures/doc1.pdf',
    'tests/fixtures/doc2.pdf',
    'tests/fixtures/photo-id.jpg'
  ]);

  // Verify all files are listed
  const fileItems = page.locator('.file-list li');
  await expect(fileItems).toHaveCount(3);
  
  // Verify individual file names
  await expect(fileItems.nth(0)).toContainText('doc1.pdf');
  await expect(fileItems.nth(1)).toContainText('doc2.pdf');
  await expect(fileItems.nth(2)).toContainText('photo-id.jpg');
});

Scenario 3: Download Verification with Mocking

test('should download report with mocked data', async ({ page }) => {
  // Mock API to control download content
  await page.route('**/api/report', async route => {
    await route.fulfill({
      status: 200,
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="report.pdf"'
      },
      body: Buffer.from('Mock PDF content for testing')
    });
  });

  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('#download-report')
  ]);

  // Verify download metadata
  expect(download.suggestedFilename()).toBe('report.pdf');
  
  // Verify file content
  const filePath = await download.path();
  const content = await fs.promises.readFile(filePath, 'utf8');
  expect(content).toBe('Mock PDF content for testing');
});

Advanced Techniques

1. Handling Drag-and-Drop Uploads

// Simulate drag-and-drop file upload
await page.dispatchEvent('.drop-zone', 'dragover');
await page.locator('.drop-zone').setInputFiles('file-to-upload.pdf');
await page.dispatchEvent('.drop-zone', 'drop');

2. File Type Validation Testing

test('should reject invalid file types', async ({ page }) => {
  await page.goto('/upload');
  
  // Try uploading invalid file type
  await page.locator('input[type="file"]').setInputFiles('script.exe');
  
  // Verify error message
  await expect(page.locator('.error-message'))
    .toHaveText('Executable files are not allowed');
  
  // Verify no files were actually uploaded
  await expect(page.locator('.file-list li')).toHaveCount(0);
});

3. Large File Upload Testing

test('should handle large file upload with progress', async ({ page }) => {
  // Create a large test file (10MB)
  const largeFile = await createTestFile('large-file.bin', 10 * 1024 * 1024);
  
  await page.goto('/upload');
  await page.locator('input[type="file"]').setInputFiles(largeFile);
  
  // Verify progress indicator
  await expect(page.locator('.progress-bar')).toBeVisible();
  
  // Wait for upload completion
  await expect(page.locator('.upload-status'))
    .toHaveText('Upload completed', { timeout: 30000 });
});

Practical BugBank Examples

Bank Statement Upload

test('should upload bank statement for verification', async ({ page }) => {
  await page.goto('https://bugbank.netlify.app/statement-upload');
  
  // Upload statement PDF
  await page.locator('#statement-file').setInputFiles('tests/fixtures/bank-statement.pdf');
  
  // Verify file preview
  await expect(page.locator('.file-preview')).toBeVisible();
  await expect(page.locator('.file-size')).toContainText('PDF');
  
  // Submit for processing
  await page.click('#process-statement');
  
  // Verify processing completion
  await expect(page.locator('.verification-status'))
    .toHaveText('Statement verified successfully', { timeout: 10000 });
});

Transaction Receipt Download

test('should download transaction receipt', async ({ page }) => {
  await page.goto('https://bugbank.netlify.app/transactions');
  
  // Click download button for a specific transaction
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.locator('tr:has-text("Transfer") .download-btn').click()
  ]);

  // Verify download properties
  expect(download.suggestedFilename()).toMatch(/receipt-\d+\.pdf/);
  
  // Save download
  const downloadPath = `./downloads/${download.suggestedFilename()}`;
  await download.saveAs(downloadPath);
  
  // Verify file exists and has content
  expect(fs.existsSync(downloadPath)).toBe(true);
  expect(fs.statSync(downloadPath).size).toBeGreaterThan(0);
});

Best Practices

1. File Management

// Use test-specific directories
const testUploadDir = './test-uploads';
const testDownloadDir = './test-downloads';

// Clean up before tests
beforeEach(async () => {
  if (fs.existsSync(testUploadDir)) {
    fs.rmSync(testUploadDir, { recursive: true });
  }
  fs.mkdirSync(testUploadDir, { recursive: true });
});

2. File Path Management

// Use path utilities for cross-platform compatibility
import path from 'path';

const filePath = path.join(__dirname, 'fixtures', 'test-file.pdf');
await page.locator('input[type="file"]').setInputFiles(filePath);

3. Mocking File Responses

// Mock file downloads for consistent testing
await page.route('**/api/export', async route => {
  await route.fulfill({
    status: 200,
    headers: {
      'Content-Type': 'application/vnd.ms-excel',
      'Content-Disposition': 'attachment; filename="report.xlsx"'
    },
    body: Buffer.from('mock,excel,content\n1,2,3')
  });
});

Debugging Tips

// Enable download logging
page.on('download', download => {
  console.log(`Download started: ${download.suggestedFilename()}`);
});

// Monitor file inputs
await page.locator('input[type="file"]').evaluate(input => {
  input.addEventListener('change', (e) => {
    console.log('Files selected:', e.target.files);
  });
});

The best way to learn is by doing! I've included a full test file with all the examples from this article. Clone it, run it, and experiment:

Link to GitHub projectGitHub Octocat

Conclusion

In this guide, I've explored various strategies for testing file uploads and downloads using Playwright. By leveraging the power of Playwright's API, we can create robust tests that simulate real user interactions with file inputs and ensure our applications handle files correctly.

Key takeaways include:

  • Utilizing fixtures for consistent test data
  • Implementing thorough error handling and validation
  • Mocking file responses for reliable testing
  • Employing debugging techniques to troubleshoot issues effectively

With these practices in place, you can enhance the reliability and maintainability of your file upload and download tests, ultimately leading to a better user experience.


Thank you for reading and see you in the next lesson! ☕💜