Playwright 第11章:文件上传与下载
文件操作是 Web 自动化测试中的常见场景。Playwright 提供了完善的文件上传和下载处理机制,支持文件选择器监听、文件类型验证和下载文件管理等。
文件上传
fileChooser 事件
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/upload');
// 监听 fileChooser 事件
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.click('#upload-btn'), // 触发文件选择
]);
// 设置单个文件
await fileChooser.setFiles('document.pdf');
// 设置多个文件
await fileChooser.setFiles([
'photo1.jpg',
'photo2.png',
'document.pdf',
]);
// 检查文件选择器属性
console.log('是否支持多选:', fileChooser.isMultiple());
// 取消文件选择
await fileChooser.cancel();
await browser.close();
直接设置文件输入
对于 <input type="file"> 元素,可以直接使用 setInputFiles():
const page = await browser.newPage();
// 直接设置文件(最可靠的方式)
await page.setInputFiles('#file-input', 'resume.pdf');
// 设置多个文件
await page.setInputFiles('#file-input', [
'file1.pdf',
'file2.jpg',
'file3.png',
]);
// 使用定位器
await page.locator('#avatar-upload').setInputFiles('avatar.jpg');
// 清空文件选择
await page.setInputFiles('#file-input', []);
// 验证文件已选择
const fileInput = page.locator('#file-input');
await expect(fileInput).toHaveValue(/resume\.pdf/);
文件上传类型验证
const page = await browser.newPage();
await page.goto('https://example.com/upload');
await page.setInputFiles('#file-input', 'test.jpg');
// 验证文件类型限制
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.click('#image-upload'),
]);
// 只允许图片格式
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
const files = fileChooser.element()?.files;
// 上传文件后验证提示
await expect(page.locator('.file-name')).toContainText('test.jpg');
await expect(page.locator('.file-size')).toBeVisible();
await expect(page.locator('.file-type')).toContainText('image/jpeg');
// 验证文件预览
await expect(page.locator('.preview img')).toBeVisible();
文件下载
基础下载操作
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/download');
// 监听下载事件
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-btn'),
]);
// 获取下载信息
console.log('文件名:', download.suggestedFilename());
console.log('下载URL:', download.url());
console.log('页面URL:', download.page()?.url());
// 保存到磁盘
await download.saveAs('./downloads/' + download.suggestedFilename());
// 或保存为自定义文件名
await download.saveAs('./downloads/report.pdf');
// 获取文件内容(不保存到磁盘)
const fileStream = await download.createReadStream();
const buffers: Buffer[] = [];
for await (const chunk of fileStream) {
buffers.push(chunk);
}
const content = Buffer.concat(buffers);
console.log('文件大小:', content.length, 'bytes');
// 等待下载完成
console.log('下载进度:', await download.failure()); // null 表示成功
await browser.close();
下载配置
const browser = await chromium.launch();
// 配置下载路径
const context = await browser.newContext({
acceptDownloads: true, // 默认开启
});
const page = await context.newPage();
await page.goto('https://example.com/download');
// 触发下载
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('.download-link'),
]);
// 保存到指定目录
await download.saveAs(`./downloads/${Date.now()}_${download.suggestedFilename()}`);
// 批量下载
const downloadLinks = page.locator('.download-item');
const count = await downloadLinks.count();
for (let i = 0; i < count; i++) {
const [download] = await Promise.all([
page.waitForEvent('download'),
downloadLinks.nth(i).click(),
]);
await download.saveAs(`./downloads/file_${i}_${download.suggestedFilename()}`);
console.log(`已下载: ${download.suggestedFilename()}`);
}
文件类型验证
验证下载文件
import fs from 'fs';
import path from 'path';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/downloads');
// 下载 PDF 文件
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('.download-pdf'),
]);
const filePath = './downloads/' + download.suggestedFilename();
await download.saveAs(filePath);
// 验证文件存在
expect(fs.existsSync(filePath)).toBeTruthy();
// 验证文件大小
const stats = fs.statSync(filePath);
expect(stats.size).toBeGreaterThan(0);
console.log('文件大小:', stats.size, 'bytes');
// 验证文件类型
const ext = path.extname(filePath).toLowerCase();
expect(['.pdf', '.doc', '.docx', '.xlsx']).toContain(ext);
// 验证文件内容(PDF 文件头)
const buffer = fs.readFileSync(filePath);
const header = buffer.toString('ascii', 0, 10);
expect(header).toContain('%PDF'); // PDF 文件标识
// 清理下载文件
fs.unlinkSync(filePath);
文件完整性校验
import crypto from 'crypto';
import fs from 'fs';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/download');
// 下载文件
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-csv'),
]);
const filePath = './downloads/data.csv';
await download.saveAs(filePath);
// 计算文件哈希
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
// 验证文件内容(CSV 格式验证)
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.trim().split('
');
// 验证 CSV 头部
expect(lines[0]).toBe('id,name,email,created_at');
// 验证数据行数
expect(lines.length).toBeGreaterThan(1);
// 验证每行数据格式
for (let i = 1; i < lines.length; i++) {
const columns = lines[i].split(',');
expect(columns.length).toBe(4); // 4 列
expect(columns[0]).toMatch(/^\d+$/); // id 为数字
}
console.log(`文件行数: ${lines.length - 1}`);
console.log('SHA256:', hash);
完整用例:文件上传下载测试
import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test.describe('文件操作测试', () => {
const testFilePath = path.resolve('test-data/sample.jpg');
const downloadDir = './test-downloads';
test.beforeAll(() => {
// 确保下载目录存在
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true });
}
});
test('上传文件并验证', async ({ page }) => {
await page.goto('/upload');
// 上传文件
await page.setInputFiles('#file-upload', testFilePath);
// 验证上传预览
await expect(page.locator('.file-name')).toContainText('sample.jpg');
await expect(page.locator('.upload-success')).toBeVisible();
// 提交表单
await page.click('.submit-btn');
await expect(page.locator('.success-message')).toContainText('上传成功');
});
test('下载文件并验证', async ({ page }) => {
await page.goto('/download');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#export-btn'),
]);
const filePath = path.join(downloadDir, download.suggestedFilename());
await download.saveAs(filePath);
// 验证文件
expect(fs.existsSync(filePath)).toBeTruthy();
expect(fs.statSync(filePath).size).toBeGreaterThan(0);
});
test('批量下载并验证', async ({ page }) => {
await page.goto('/batch-download');
const files = page.locator('.download-btn');
const fileCount = await files.count();
for (let i = 0; i < fileCount; i++) {
const [download] = await Promise.all([
page.waitForEvent('download'),
files.nth(i).click(),
]);
const filePath = path.join(downloadDir, download.suggestedFilename());
await download.saveAs(filePath);
console.log(`已下载: ${download.suggestedFilename()}`);
}
// 验证下载数量
const downloadedFiles = fs.readdirSync(downloadDir);
expect(downloadedFiles.length).toBe(fileCount);
});
});
总结
Playwright 的 fileChooser 事件和 setInputFiles() 方法提供了完善的文件上传解决方案,download 事件支持灵活的文件下载管理。文件上传支持单文件和批量文件,下载支持保存到磁盘或获取文件流直接处理。在实际测试中,建议结合文件类型验证和完整性校验确保文件操作的正确性,同时注意下载目录的清理和管理。