.agents/skills/playwright-best-practices/testing-patterns/file-operations.md
For advanced patterns (progress tracking, cancellation, retry logic), see file-upload-download.md
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')
})
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})
})
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)
})
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$/)
}
})
// 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
})
},
})
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()
})
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()
})
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()
})
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()
})
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()
})
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()
})
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()
})
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')
})
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')
})
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-Pattern | Problem | Solution |
|---|---|---|
| Not waiting for download | Race condition, test fails | Always use waitForEvent("download") |
| Hardcoded download paths | Conflicts in parallel runs | Use testInfo.outputPath() |
| Skipping content verification | Download might be empty/corrupt | Verify file content when possible |
Using force: true for hidden inputs | May not trigger proper events | Use setInputFiles on hidden inputs directly |