AI Tools Compared

Claude Code excels at creating Playwright tests for file uploads and downloads because it understands Playwright’s file chooser APIs and download handling mechanisms. When prompted with your upload/download flow, Claude generates tests using setInputFiles(), download event handling, and blob management that require deep API knowledge and proper async handling.

This guide explores how AI assistants can help you create effective Playwright tests for file upload and download flows, with practical examples you can apply immediately.

What Makes an AI Assistant Effective for Playwright Test Generation

Not all AI assistants handle code generation equally. When evaluating AI tools for creating Playwright tests, several capabilities matter most.

An effective AI assistant should understand Playwright’s API thoroughly, including the file chooser APIs, download handling, and blob management. It should generate tests that follow Playwright best practices, such as using proper selectors, handling async operations correctly, and implementing reliable assertions.

Context awareness matters significantly. The best AI assistants can maintain conversation context across multiple turns, allowing you to refine tests iteratively. They should also understand your specific testing framework setup, whether you use Jest, Mocha, or another test runner.

File Upload Testing with Playwright

Playwright provides strong APIs for handling file uploads. The key is using setInputFiles() to programmatically select files for upload input elements.

Here is a practical example of testing a file upload flow:

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

test('should upload a file successfully', async ({ page }) => {
  // Navigate to the upload page
  await page.goto('/upload');

  // Upload a test file
  await page.setInputFiles('input[type="file"]', {
    name: 'test-document.pdf',
    mimeType: 'application/pdf',
    buffer: Buffer.from('mock pdf content')
  });

  // Submit the upload form
  await page.click('button[type="submit"]');

  // Verify upload success
  await expect(page.locator('.upload-success')).toBeVisible();
  await expect(page.locator('.file-name')).toContainText('test-document.pdf');
});

AI assistants can generate variations of this test for different scenarios, such as multiple file uploads or different file types. You can ask an AI to modify the test to handle drag-and-drop uploads by adding the appropriate interaction patterns.

File Download Testing with Playwright

Testing downloads requires a different approach. Playwright’s download API allows you to intercept and verify downloaded files.

test('should download a file successfully', async ({ page }) => {
  // Start waiting for the download before triggering it
  const downloadPromise = page.waitForEvent('download');

  // Trigger the download
  await page.click('button.download-primary');

  // Wait for download to complete
  const download = await downloadPromise;

  // Verify download details
  expect(download.suggestedFilename()).toBe('exported-data.csv');

  // Save the file temporarily for verification
  const path = await download.path();
  expect(path).toBeTruthy();

  // Read and verify file contents
  const fs = require('fs');
  const content = fs.readFileSync(path, 'utf-8');
  expect(content).toContain('expected data');
});

AI assistants excel at generating these download tests because they can incorporate error handling and cross-browser considerations that you might otherwise overlook.

Handling Dynamic File Names and Paths

Real-world applications often generate dynamic filenames or use temporary directories. An AI assistant can help you write flexible tests that handle these scenarios.

test('should handle dynamically named downloads', async ({ page }) => {
  const downloadPromise = page.waitForEvent('download');

  // Trigger download with timestamp in filename
  await page.click('button.export-report');

  const download = await downloadPromise;

  // Verify the filename follows the expected pattern
  const filename = download.suggestedFilename();
  expect(filename).toMatch(/^report-\d{8}\.pdf$/);

  // Handle path in temporary directory
  const downloadPath = download.path();
  expect(downloadPath).toContain('playwright-downloads');
});

Testing Upload Validation and Error Handling

test coverage includes negative test cases. AI can help generate tests for validation scenarios efficiently.

test('should reject oversized files', async ({ page }) => {
  await page.goto('/upload');

  // Create a file exceeding the size limit (e.g., 10MB)
  const largeBuffer = Buffer.alloc(11 * 1024 * 1024);
  await page.setInputFiles('input[type="file"]', {
    name: 'huge-file.pdf',
    mimeType: 'application/pdf',
    buffer: largeBuffer
  });

  await page.click('button[type="submit"]');

  // Verify error message appears
  await expect(page.locator('.error-message')).toBeVisible();
  await expect(page.locator('.error-message')).toContainText('File too large');
});

test('should reject invalid file types', async ({ page }) => {
  await page.goto('/upload');

  await page.setInputFiles('input[type="file"]', {
    name: 'malicious.exe',
    mimeType: 'application/x-executable',
    buffer: Buffer.from('malicious content')
  });

  await page.click('button[type="submit"]');

  await expect(page.locator('.error-message')).toContainText('Invalid file type');
});

Using AI to Accelerate Test Development

When working with an AI assistant, provide clear context to get better results. Include your Playwright version, test runner setup, and any specific libraries you use.

Instead of a vague request like “write a download test,” try something more specific: “Write a Playwright test using Jest that downloads a CSV file, verifies the filename matches the pattern report-*.csv, and asserts the file contains at least 10 rows of data.”

The AI can then generate a test tailored to your exact requirements, saving you from adapting generic code.

Best Practices for AI-Generated Tests

AI-generated tests require review and refinement. Always verify the generated code handles edge cases relevant to your application.

Maintain your test files in version control and run them consistently in your CI pipeline. AI assistants can help you add new test cases quickly, but human oversight ensures coverage remains.

Consider creating a library of reusable test utilities for common upload and download scenarios. You can ask AI to help design these utilities based on patterns that emerge across your tests.

Advanced File Upload Scenarios

Real-world applications require testing complex upload scenarios beyond basic file selection:

// Testing chunked/resumable uploads
test('should handle resumable upload with retry', async ({ page }) => {
  await page.goto('/upload');

  // Create a large file (simulate 100MB upload in chunks)
  const largeBuffer = Buffer.alloc(100 * 1024 * 1024);
  await page.setInputFiles('input[type="file"]', {
    name: 'large-dataset.bin',
    mimeType: 'application/octet-stream',
    buffer: largeBuffer
  });

  // Monitor upload progress
  const progressUpdates = [];
  await page.on('console', msg => {
    if (msg.text().includes('upload progress')) {
      progressUpdates.push(msg.text());
    }
  });

  // Simulate network interruption and resume
  await page.click('button[type="submit"]');

  // Wait for initial upload progress
  await page.waitForFunction(
    () => window.uploadProgress > 25,
    { timeout: 30000 }
  );

  // Kill network connection
  await page.context().setOffline(true);
  await page.waitForTimeout(2000);

  // Resume upload
  await page.context().setOffline(false);

  // Verify upload completes
  await expect(page.locator('.upload-success')).toBeVisible();
  expect(progressUpdates.length).toBeGreaterThan(5);
});

// Testing drag-and-drop uploads
test('should accept files via drag-and-drop', async ({ page }) => {
  await page.goto('/upload');

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

  // Simulate drag-and-drop event
  await dropZone.evaluate((element) => {
    const event = new DragEvent('drop', {
      bubbles: true,
      cancelable: true,
      dataTransfer: new DataTransfer()
    });
    element.dispatchEvent(event);
  });

  // Attach file during drop event
  await page.setInputFiles('input[type="file"]', {
    name: 'drag-and-drop-file.pdf',
    mimeType: 'application/pdf',
    buffer: Buffer.from('PDF content')
  });

  await expect(page.locator('.file-name')).toContainText('drag-and-drop-file');
});

// Testing multiple file uploads with ordering
test('should maintain file order in multi-file upload', async ({ page }) => {
  await page.goto('/upload');

  const files = [
    { name: 'first.txt', buffer: Buffer.from('First file') },
    { name: 'second.txt', buffer: Buffer.from('Second file') },
    { name: 'third.txt', buffer: Buffer.from('Third file') }
  ];

  // Upload all files
  await page.setInputFiles('input[type="file"]', files);

  // Verify order in UI
  const fileList = await page.locator('.file-list li');
  const count = await fileList.count();

  for (let i = 0; i < count; i++) {
    const text = await fileList.nth(i).textContent();
    expect(text).toContain(files[i].name);
  }
});

Testing Upload Security and Validation

Security testing is critical for file upload functionality:

// Testing security boundaries
test('should sanitize uploaded filenames', async ({ page }) => {
  await page.goto('/upload');

  // Attempt path traversal in filename
  await page.setInputFiles('input[type="file"]', {
    name: '../../../etc/passwd',
    mimeType: 'text/plain',
    buffer: Buffer.from('malicious')
  });

  await page.click('button[type="submit"]');

  // Verify filename is sanitized in the response
  const response = await page.waitForResponse(
    response => response.url().includes('/api/upload') && response.status() === 200
  );

  const data = await response.json();
  expect(data.filename).not.toContain('..');
  expect(data.filename).not.toContain('/');
});

test('should enforce MIME type restrictions', async ({ page }) => {
  await page.goto('/upload');

  // Attempt to upload executable with fake extension
  const maliciousFile = {
    name: 'document.pdf',
    mimeType: 'application/x-executable',
    buffer: Buffer.from('MZ\x90\x00\x03') // EXE header
  };

  await page.setInputFiles('input[type="file"]', maliciousFile);
  await page.click('button[type="submit"]');

  // Verify rejection
  await expect(page.locator('.error-message')).toContainText(
    'File type not allowed'
  );
});

test('should rate limit uploads per user', async ({ page }) => {
  await page.goto('/upload');

  // Attempt rapid sequential uploads
  const uploadPromises = [];

  for (let i = 0; i < 50; i++) {
    await page.setInputFiles('input[type="file"]', {
      name: `file${i}.txt`,
      mimeType: 'text/plain',
      buffer: Buffer.from('test content')
    });

    uploadPromises.push(
      page.click('button[type="submit"]')
    );
  }

  const results = await Promise.allSettled(uploadPromises);

  // Expect rate limiting to trigger
  const rejected = results.filter(r => r.status === 'rejected');
  expect(rejected.length).toBeGreaterThan(0);
});

Performance and Stress Testing for Uploads

Test upload performance under various conditions:

// Measure upload speed and resource usage
test('upload performance benchmarking', async ({ page }) => {
  await page.goto('/upload');

  const fileSizes = [
    { size: 1024 * 1024, name: '1MB' },
    { size: 10 * 1024 * 1024, name: '10MB' },
    { size: 50 * 1024 * 1024, name: '50MB' }
  ];

  const benchmarks = [];

  for (const { size, name } of fileSizes) {
    const buffer = Buffer.alloc(size);

    const startTime = Date.now();
    const startMetrics = await page.evaluate(() => ({
      memory: performance.memory?.usedJSHeapSize || 0,
      time: performance.now()
    }));

    await page.setInputFiles('input[type="file"]', {
      name: `benchmark-${name}.bin`,
      mimeType: 'application/octet-stream',
      buffer
    });

    await page.click('button[type="submit"]');
    await expect(page.locator('.upload-success')).toBeVisible();

    const duration = Date.now() - startTime;
    const endMetrics = await page.evaluate(() => ({
      memory: performance.memory?.usedJSHeapSize || 0,
      time: performance.now()
    }));

    benchmarks.push({
      fileSize: name,
      duration: duration,
      memoryIncrease: (endMetrics.memory - startMetrics.memory) / 1024 / 1024,
      throughput: (size / 1024 / 1024) / (duration / 1000)
    });
  }

  console.table(benchmarks);
});

Integrating with CI/CD Pipelines

Ensure upload tests run reliably in CI environments:

// Configuration for CI-compatible file uploads
const config = {
  // Use memory-based file system in CI
  uploadDir: process.env.CI
    ? '/tmp/uploads'
    : './test-uploads',

  // Disable browser cache in CI
  launchOptions: process.env.CI ? {
    args: [
      '--disable-blink-features=AutomationControlled',
      '--no-first-run',
      '--no-default-browser-check'
    ]
  } : {},

  // Timeout adjustments for slower CI runners
  timeout: process.env.CI ? 60000 : 30000
};

test.describe('File upload tests (CI-compatible)', () => {
  test.beforeEach(async ({ page }) => {
    // Create temporary upload directory if needed
    if (!fs.existsSync(config.uploadDir)) {
      fs.mkdirSync(config.uploadDir, { recursive: true });
    }
  });

  test('upload works in CI environment', async ({ page }) => {
    await page.goto('/upload');
    await page.setInputFiles('input[type="file"]', {
      name: 'ci-test.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('CI test content')
    });
    await page.click('button[type="submit"]');
    await expect(page.locator('.upload-success')).toBeVisible({ timeout: config.timeout });
  });
});

Built by theluckystrike — More at zovo.one