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 事件支持灵活的文件下载管理。文件上传支持单文件和批量文件,下载支持保存到磁盘或获取文件流直接处理。在实际测试中,建议结合文件类型验证和完整性校验确保文件操作的正确性,同时注意下载目录的清理和管理。