Back to Sanity

File Upload & Download Testing

.agents/skills/playwright-best-practices/testing-patterns/file-operations.md

5.24.010.0 KB
Original Source

File Upload & Download Testing

For advanced patterns (progress tracking, cancellation, retry logic), see file-upload-download.md

Table of Contents

  1. File Downloads
  2. File Uploads
  3. Drag and Drop
  4. File Content Verification

File Downloads

Basic Download

typescript
test('download PDF report', async ({page}) => {
  await page.goto('/reports')

  // Start waiting for download before clicking
  const downloadPromise = page.waitForEvent('download')
  await page.getByRole('button', {name: 'Download PDF'}).click()
  const download = await downloadPromise

  // Verify filename
  expect(download.suggestedFilename()).toBe('report.pdf')

  // Save to specific path
  await download.saveAs('./downloads/report.pdf')
})

Download with Custom Path

typescript
test('download to temp directory', async ({page}, testInfo) => {
  await page.goto('/exports')

  const downloadPromise = page.waitForEvent('download')
  await page.getByRole('link', {name: 'Export CSV'}).click()
  const download = await downloadPromise

  // Save to test output directory
  const path = testInfo.outputPath(download.suggestedFilename())
  await download.saveAs(path)

  // Attach to test report
  await testInfo.attach('downloaded-file', {path})
})

Verify Download Content

typescript
import fs from 'fs'
import path from 'path'

test('verify CSV content', async ({page}, testInfo) => {
  await page.goto('/data')

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

  const filePath = testInfo.outputPath('export.csv')
  await download.saveAs(filePath)

  // Read and verify content
  const content = fs.readFileSync(filePath, 'utf-8')
  expect(content).toContain('Name,Email,Status')
  expect(content).toContain('John Doe')

  // Verify row count
  const rows = content.trim().split('\n')
  expect(rows.length).toBeGreaterThan(1)
})

Multiple Downloads

typescript
test('download multiple files', async ({page}) => {
  await page.goto('/batch-export')

  await page.getByRole('checkbox', {name: 'Select All'}).check()

  // Collect all downloads
  const downloads: Download[] = []
  page.on('download', (download) => downloads.push(download))

  await page.getByRole('button', {name: 'Download Selected'}).click()

  // Wait for all downloads
  await expect.poll(() => downloads.length, {timeout: 30000}).toBe(5)

  // Verify each download
  for (const download of downloads) {
    expect(download.suggestedFilename()).toMatch(/\.pdf$/)
  }
})

Download Fixture

typescript
// fixtures/download.fixture.ts
import {test as base, Download} from '@playwright/test'
import fs from 'fs'
import path from 'path'

type DownloadFixtures = {
  downloadDir: string
  downloadAndVerify: (trigger: () => Promise<void>, expectedFilename: string) => Promise<string>
}

export const test = base.extend<DownloadFixtures>({
  downloadDir: async ({}, use, testInfo) => {
    const dir = testInfo.outputPath('downloads')
    fs.mkdirSync(dir, {recursive: true})
    await use(dir)
  },

  downloadAndVerify: async ({page, downloadDir}, use) => {
    await use(async (trigger, expectedFilename) => {
      const downloadPromise = page.waitForEvent('download')
      await trigger()
      const download = await downloadPromise

      expect(download.suggestedFilename()).toBe(expectedFilename)

      const filePath = path.join(downloadDir, expectedFilename)
      await download.saveAs(filePath)
      return filePath
    })
  },
})

File Uploads

Basic Upload

typescript
test('upload profile picture', async ({page}) => {
  await page.goto('/settings/profile')

  // Upload file
  await page.getByLabel('Profile Picture').setInputFiles('./fixtures/avatar.png')

  // Verify preview
  await expect(page.getByAltText('Profile preview')).toBeVisible()

  await page.getByRole('button', {name: 'Save'}).click()
  await expect(page.getByText('Profile updated')).toBeVisible()
})

Multiple File Upload

typescript
test('upload multiple documents', async ({page}) => {
  await page.goto('/documents/upload')

  await page
    .getByLabel('Documents')
    .setInputFiles(['./fixtures/doc1.pdf', './fixtures/doc2.pdf', './fixtures/doc3.pdf'])

  // Verify all files listed
  await expect(page.getByText('doc1.pdf')).toBeVisible()
  await expect(page.getByText('doc2.pdf')).toBeVisible()
  await expect(page.getByText('doc3.pdf')).toBeVisible()

  await page.getByRole('button', {name: 'Upload All'}).click()
  await expect(page.getByText('3 files uploaded')).toBeVisible()
})

Upload with File Chooser

typescript
test('upload via file chooser dialog', async ({page}) => {
  await page.goto('/upload')

  // Handle file chooser
  const fileChooserPromise = page.waitForEvent('filechooser')
  await page.getByRole('button', {name: 'Choose File'}).click()
  const fileChooser = await fileChooserPromise

  await fileChooser.setFiles('./fixtures/document.pdf')

  await expect(page.getByText('document.pdf')).toBeVisible()
})

Clear and Re-upload

typescript
test('replace uploaded file', async ({page}) => {
  await page.goto('/upload')

  const input = page.getByLabel('Document')

  // Upload first file
  await input.setInputFiles('./fixtures/old.pdf')
  await expect(page.getByText('old.pdf')).toBeVisible()

  // Clear selection
  await input.setInputFiles([])

  // Upload new file
  await input.setInputFiles('./fixtures/new.pdf')
  await expect(page.getByText('new.pdf')).toBeVisible()
  await expect(page.getByText('old.pdf')).toBeHidden()
})

Upload from Buffer

typescript
test('upload generated file', async ({page}) => {
  await page.goto('/upload')

  // Create file content dynamically
  const content = 'Name,Email\nJohn,[email protected]'

  await page.getByLabel('CSV File').setInputFiles({
    name: 'users.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from(content),
  })

  await expect(page.getByText('users.csv')).toBeVisible()
})

Drag and Drop

Drag and Drop Upload

typescript
test('drag and drop file upload', async ({page}) => {
  await page.goto('/upload')

  const dropzone = page.getByTestId('dropzone')

  // Create a DataTransfer with the file
  const dataTransfer = await page.evaluateHandle(() => new DataTransfer())

  // Read file and add to DataTransfer
  const buffer = fs.readFileSync('./fixtures/image.png')
  await page.evaluate(
    async ([dataTransfer, data]) => {
      const file = new File([new Uint8Array(data)], 'image.png', {
        type: 'image/png',
      })
      dataTransfer.items.add(file)
    },
    [dataTransfer, [...buffer]] as const,
  )

  // Dispatch drop event
  await dropzone.dispatchEvent('drop', {dataTransfer})

  await expect(page.getByText('image.png uploaded')).toBeVisible()
})

Simpler Drag and Drop

typescript
test('drag and drop with setInputFiles', async ({page}) => {
  await page.goto('/upload')

  // Most dropzones have a hidden file input
  const input = page.locator('input[type="file"]')

  // This works even if the input is hidden
  await input.setInputFiles('./fixtures/document.pdf')

  await expect(page.getByText('document.pdf')).toBeVisible()
})

File Content Verification

Verify PDF Content

typescript
import pdf from 'pdf-parse'

test('verify PDF content', async ({page}, testInfo) => {
  await page.goto('/invoice/123')

  const downloadPromise = page.waitForEvent('download')
  await page.getByRole('button', {name: 'Download Invoice'}).click()
  const download = await downloadPromise

  const path = testInfo.outputPath('invoice.pdf')
  await download.saveAs(path)

  // Parse PDF
  const dataBuffer = fs.readFileSync(path)
  const data = await pdf(dataBuffer)

  expect(data.text).toContain('Invoice #123')
  expect(data.text).toContain('Total: $99.99')
})

Verify Excel Content

typescript
import XLSX from 'xlsx'

test('verify Excel export', async ({page}, testInfo) => {
  await page.goto('/reports')

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

  const path = testInfo.outputPath('report.xlsx')
  await download.saveAs(path)

  // Parse Excel
  const workbook = XLSX.readFile(path)
  const sheet = workbook.Sheets[workbook.SheetNames[0]]
  const data = XLSX.utils.sheet_to_json(sheet)

  expect(data).toHaveLength(10)
  expect(data[0]).toHaveProperty('Name')
  expect(data[0]).toHaveProperty('Email')
})

Verify JSON Download

typescript
test('verify JSON export', async ({page}, testInfo) => {
  await page.goto('/api-data')

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

  const path = testInfo.outputPath('data.json')
  await download.saveAs(path)

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

  expect(content.users).toHaveLength(5)
  expect(content.exportDate).toBeDefined()
})

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Not waiting for downloadRace condition, test failsAlways use waitForEvent("download")
Hardcoded download pathsConflicts in parallel runsUse testInfo.outputPath()
Skipping content verificationDownload might be empty/corruptVerify file content when possible
Using force: true for hidden inputsMay not trigger proper eventsUse setInputFiles on hidden inputs directly