首页 / Playwright 入门教程 / 文件上传与下载

Playwright 入门教程

文件上传与下载

本教程共 59 篇 · 第 20 篇 · 更新于 2026-08-04 · 约 9 分钟阅读

PlaywrightsetInputFilesdownload文件上传文件下载filechooserUI 自动化

本节目标:能上传各种形式的文件,能捕获下载并把文件存到指定位置。

文件操作是自动化测试里最容易卡住的一环。原因是它涉及浏览器和操作系统的交界处——那个系统级的「选择文件」弹窗,脚本根本点不到。

Playwright 的解法很干脆:绕过弹窗,直接给 input 元素塞文件。下载则反过来,用事件把浏览器的下载行为接住。

上传:setInputFiles 基本用法

页面上有 <input type="file">,就用 setInputFiles()

import path from 'path';

// 上传单个文件
await page.getByLabel('上传文件').setInputFiles(path.join(__dirname, 'myfile.pdf'));

注意第一个参数必须指向 type="file" 的 input 元素。指向按钮或包裹它的 div 都会报错。

相对路径会以当前工作目录为基准解析,也就是你敲 npx playwright test 那个目录。跨机器跑很容易错,所以我一律用 path.join(__dirname, ...) 拼绝对路径。

多文件、目录与清空

// 一次上传多个
await page.getByLabel('上传文件').setInputFiles([
  path.join(__dirname, 'file1.txt'),
  path.join(__dirname, 'file2.txt'),
]);

// 上传整个目录(input 需带 webkitdirectory 属性)
await page.getByLabel('上传目录').setInputFiles(path.join(__dirname, 'mydir'));

// 清空已选文件
await page.getByLabel('上传文件').setInputFiles([]);

传空数组等于「取消选择」,用来测「必须选文件才能提交」这类校验。

不落盘:直接传内存里的内容

不想在仓库里放测试文件?可以直接构造:

await page.getByLabel('上传文件').setInputFiles({
  name: 'file.txt',
  mimeType: 'text/plain',
  buffer: Buffer.from('这是测试内容'),
});

三个字段都要给:文件名、MIME 类型、内容字节。

Tip

我很喜欢这种写法。测试大文件校验时,Buffer.alloc(10 * 1024 * 1024) 一行就造出 10MB 假文件,不用往 git 里塞二进制。

setInputFiles 不做可操作性检查

这一点必须知道:setInputFiles() 不检查元素可见、稳定、启用。

这是有意为之。现实中的上传组件几乎都把真正的 <input type="file"> 藏起来(display: none 或定位到屏幕外),外面套一个好看的按钮。要是检查可见性,这类组件全都测不了。

所以你可以直接对隐藏 input 操作:

// input 藏在样式里,照样能设置
await page
  .locator('input[type="file"]')
  .setInputFiles(path.join(__dirname, 'avatar.png'));

动态创建的 input:filechooser 事件

有些页面更极端,点击按钮时才用 JS 临时创建 input,用完就删。这时你根本抓不到那个元素。

filechooser 事件接:

// 1. 先挂上等待,注意这里没有 await
const fileChooserPromise = page.waitForEvent('filechooser');

// 2. 触发动作
await page.getByRole('button', { name: '选择文件' }).click();

// 3. 拿到文件选择器对象
const fileChooser = await fileChooserPromise;

// 4. 设置文件
await fileChooser.setFiles(path.join(__dirname, 'myfile.pdf'));
Warning

第 1 行不能加 await。加了就会卡在那里等事件,而触发事件的点击永远轮不到执行,直接死锁。这是 Playwright 里最经典的新手错误之一。

正确心法:先挂等待拿到 Promise,再触发动作,最后 await 那个 Promise。

fileChooser 对象还能告诉你更多信息:

console.log(fileChooser.isMultiple());   // 这个选择器是否允许多选
const inputHandle = fileChooser.element(); // 拿到对应的 input 元素句柄

下载:捕获 download 事件

页面每下载一个附件,就会触发一次 download 事件。捕获方式和上面文件选择器完全同构:

// 1. 先挂等待,同样不要 await
const downloadPromise = page.waitForEvent('download');

// 2. 点击下载链接
await page.getByText('下载文件').click();

// 3. 拿到 Download 对象
const download = await downloadPromise;

// 4. 等下载完成并另存
await download.saveAs('/path/to/save/' + download.suggestedFilename());
Warning

很多人会去翻 page.waitForDownload() 这个方法。Playwright 里没有这个 API,翻文档也找不到。它走的是统一的事件等待接口 page.waitForEvent('download')。同一套写法还覆盖 'popup''filechooser''dialog''requestfinished' 等事件。

Download 对象能做什么

拿到 download 之后,常用的几个方法:

// 服务端建议的文件名,比如 "report-2026.xlsx"
console.log(download.suggestedFilename());

// 下载的源 URL
console.log(download.url());

// 临时文件在磁盘上的路径(会等下载完成)
const tempPath = await download.path();

// 另存到指定位置(会自动创建目录)
await download.saveAs('./downloads/report.xlsx');

// 拿到可读流,适合大文件逐段校验
const stream = await download.createReadStream();

// 删除临时文件
await download.delete();

// 如果下载失败,返回错误信息;成功则为 null
console.log(await download.failure());

path()saveAs() 都会等待下载真正完成,所以拿到路径时文件肯定是完整的。

下载文件存在哪

默认情况下,所有下载都进临时目录。这里有个关键规则:

Warning

浏览器上下文(BrowserContext)关闭时,它产生的下载文件会被删除。 测试结束后想再看那个文件,必须在测试内用 saveAs() 挪走。

想改默认落盘位置,在启动浏览器时配 downloadsPath

const browser = await chromium.launch({
  downloadsPath: './my-downloads',
});

用测试运行器(Playwright Test)时一般不需要改这个,直接 saveAs() 更灵活。

不知道谁触发的下载

如果下载是页面自己发起的,你没法确定触发点,可以挂长期监听:

page.on('download', async download => {
  console.log('下载了:', download.suggestedFilename());
  await download.saveAs('./downloads/' + download.suggestedFilename());
});

这种写法有代价:控制流被分叉了。主流程可能在下载完成前就跑完退出,回调里的活儿做到一半就没了。

所以能用 waitForEvent 就用 waitForEvent。事件监听是兜底方案。

验证下载内容

光下载成功不够,通常还要看内容对不对:

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

test('导出报表', async ({ page }) => {
  await page.goto('https://example.com/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: '导出 CSV' }).click();
  const download = await downloadPromise;

  // 文件名符合预期
  expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}\.csv$/);

  // 存下来读内容
  const filePath = path.join(test.info().outputDir, download.suggestedFilename());
  await download.saveAs(filePath);

  const content = fs.readFileSync(filePath, 'utf-8');
  expect(content).toContain('订单号,金额,状态');
});

存到 test.info().outputDir 是个好习惯。这个目录由测试运行器管理,失败时会被收进报告产物,排查方便。

小结

  • 上传首选 setInputFiles(),它不检查可见性,隐藏 input 照样能用。
  • 支持单文件、多文件、目录、内存 Buffer 四种形式,传 [] 表示清空。
  • input 是动态创建的,改用 page.waitForEvent('filechooser')
  • 下载用 page.waitForEvent('download'),没有 waitForDownload 这个方法。
  • 等待要先挂、动作后触发,两行顺序反了必然死锁,第一行别加 await
  • 下载文件随上下文关闭而删除,要留就 saveAs()

下一章预告:弹窗与对话框 Dialog 处理——alert / confirm / prompt 这些 DOM 里找不到的原生弹窗怎么接管。