5月27日 22:58

Cypress 中怎么处理文件上传和下载?selectFile 和下载验证全流程

文件上传和下载是 Web 应用里的高频操作,但在 E2E 测试中常常被忽略或处理不当——要么上传后断言失败,要么下载文件根本找不到。Cypress 从 9.3 版本开始原生支持 selectFile(),下载验证也有成熟的套路。下面把上传和下载分开讲,每个环节都给出可运行的代码。

上传文件

selectFile 基本用法

cy.selectFile() 是 Cypress 9.3+ 引入的原生命令,替代了之前广泛使用的 cypress-file-upload 插件。它直接操作 <input type="file"> 元素,支持单文件、多文件和拖拽模式。

单文件上传

javascript
cy.get('input[type="file"]').selectFile('cypress/fixtures/report.pdf');

多文件上传

javascript
cy.get('input[type="file"]').selectFile([ 'cypress/fixtures/image1.png', 'cypress/fixtures/image2.png' ]);

拖拽上传(模拟用户拖文件到页面区域):

javascript
cy.get('.drop-zone').selectFile('cypress/fixtures/data.csv', { action: 'drag-drop' });

路径是相对于项目根目录的,测试文件放在 cypress/fixtures/ 下最规范。

selectFile 选项详解

selectFile 支持几个常用选项:

  • contents:直接传入文件内容,不需要物理文件。可以用 Cypress.Buffer.from() 构造:
javascript
cy.get('input[type="file"]').selectFile({ contents: Cypress.Buffer.from('name,value\nfoo,bar'), fileName: 'data.csv', mimeType: 'text/csv' });
  • fileName:指定文件名,服务端可能校验文件扩展名时有用。
  • mimeType:覆盖 MIME 类型,默认根据扩展名自动推断。
  • force:当 input 被隐藏或不可见时,设为 true 强制操作。
  • action'select'(默认)或 'drag-drop'
  • lastModified:模拟文件的最后修改时间。

大文件和异步上传处理

上传大文件时,前端通常会显示进度条,测试需要等待上传完成再做断言。正确做法是拦截上传请求并等待响应:

javascript
// 先拦截上传接口 cy.intercept('POST', '/api/upload').as('upload'); // 执行上传 cy.get('input[type="file"]').selectFile('cypress/fixtures/large-video.mp4'); // 等待上传接口返回成功 cy.wait('@upload').its('response.statusCode').should('eq', 200); // 验证 UI 状态 cy.get('.upload-status').should('contain', '上传成功');

不要用 cy.wait(5000) 这种硬编码等待。网络请求的耗时在不同环境下差异很大,硬等既慢又不稳定。

常见上传问题

问题 1:文件上传后页面没反应

通常是因为 input 元素没有正确触发 change 事件。确认 selectFile 操作的确实是 <input type="file">,而不是包裹它的 div 或 button。如果 input 被隐藏(很多 UI 库会隐藏原生 input),加 { force: true }

javascript
cy.get('input[type="file"]').selectFile('test.pdf', { force: true });

问题 2:从 cypress-file-upload 插件迁移

旧代码用 attachFile(),迁移只需改成 selectFile(),参数格式略有不同:

javascript
// 旧写法(cypress-file-upload 插件) cy.get('input').attachFile('test.pdf'); // 新写法(Cypress 原生) cy.get('input').selectFile('cypress/fixtures/test.pdf');

主要区别:路径要写完整相对路径,不再省略 cypress/fixtures/ 前缀。

问题 3:上传被 CSP 阻止

如果应用有严格的 Content-Security-Policy,Cypress 运行在 iframe 中可能受影响。在 cypress.config.js 中配置 chromeWebSecurity: false 可以绕过:

javascript
module.exports = { e2e: { chromeWebSecurity: false } };

生产环境不要关闭 CSP,这只是测试环境的权宜之计。

下载文件

Cypress 下载机制

Cypress 在测试运行时会把下载的文件保存到 cypress/downloads/ 目录(可在 cypress.config.js 中通过 downloadsFolder 修改)。验证下载的核心思路:触发下载 -> 读取文件 -> 断言内容。

基础下载验证

javascript
// 触发下载 cy.get('.download-btn').click(); // 读取下载目录中的文件 cy.readFile('cypress/downloads/report.csv').should('exist'); // 验证文件内容 cy.readFile('cypress/downloads/report.csv').should('contain', 'Header1,Header2');

通过拦截请求验证下载

更可靠的方式是拦截下载请求,确认服务端返回了正确的响应头和内容:

javascript
cy.intercept('GET', '/api/export', (req) => { req.reply((res) => { expect(res.headers['content-disposition']).to.include('attachment'); expect(res.headers['content-type']).to.eq('application/pdf'); }); }).as('download'); cy.get('.export-btn').click(); cy.wait('@download');

这种方式不依赖文件系统,执行更快,也不会因为磁盘写入延迟导致断言失败。

二进制文件验证

下载 PDF、图片等二进制文件时,用 base64 编码读取并验证文件头:

javascript
cy.get('.download-pdf-btn').click(); // PDF 文件以 %PDF 开头 cy.readFile('cypress/downloads/contract.pdf', 'base64') .should('startWith', 'JVBERi0'); // base64 编码的 %PDF

等待下载完成

文件写入磁盘是异步的,cy.readFile() 会在文件出现后自动重试,但如果文件很大,可能需要增加超时:

javascript
cy.readFile('cypress/downloads/large-export.zip', null, { timeout: 15000 }).should('exist');

也可以结合 UI 状态判断:

javascript
cy.get('.download-btn').click(); cy.get('.progress-bar').should('not.exist'); // 等进度条消失 cy.readFile('cypress/downloads/data.xlsx').should('exist');

配置下载目录

cypress.config.js 中可以自定义下载路径:

javascript
const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { downloadsFolder: 'cypress/downloads', setupNodeEvents(on, config) { // 每次测试前清空下载目录,避免旧文件干扰 on('before:spec', () => { fs.rmSync(config.downloadsFolder, { recursive: true, force: true }); fs.mkdirSync(config.downloadsFolder, { recursive: true }); }); } } });

每次测试前清空下载目录是好习惯,否则上一次测试下载的文件可能干扰本次断言。

拖拽上传的特殊处理

有些上传组件不是 <input type="file">,而是一个拖放区域(drop zone),用户把文件拖进去触发上传。selectFiledrag-drop action 可以模拟这个行为:

javascript
cy.get('.drop-zone').selectFile('cypress/fixtures/image.png', { action: 'drag-drop' });

如果拖放区域同时接受多个文件,传数组即可:

javascript
cy.get('.drop-zone').selectFile( ['cypress/fixtures/a.png', 'cypress/fixtures/b.png'], { action: 'drag-drop' } );

完整示例:上传后下载的端到端测试

下面是一个真实场景:上传 CSV 文件,服务端处理后返回处理结果,用户下载处理后的文件。

javascript
describe('CSV 导入导出', () => { beforeEach(() => { cy.intercept('POST', '/api/import').as('import'); cy.intercept('GET', '/api/export*').as('export'); }); it('上传 CSV 后下载处理结果', () => { cy.visit('/data-manager'); // 1. 上传文件 cy.get('input[type="file"]').selectFile('cypress/fixtures/input.csv'); cy.wait('@import').its('response.statusCode').should('eq', 200); cy.get('.import-result').should('contain', '导入 100 条数据'); // 2. 触发导出 cy.get('.export-btn').click(); cy.wait('@export').its('response.statusCode').should('eq', 200); // 3. 验证下载文件 cy.readFile('cypress/downloads/output.csv') .should('contain', '已处理'); }); });

CI 环境注意事项

在 CI 环境中跑文件下载测试时,有几个坑需要注意:

  • 无头模式下载目录cypress run(无头模式)和 cypress open(有头模式)使用相同的下载目录配置,但 CI 中没有图形界面,某些依赖浏览器原生下载对话框的组件可能行为不同。
  • 并行测试文件冲突:如果用 --parallel 并行跑测试,多个 runner 会共享同一个下载目录,文件名冲突会导致断言错误。解决方案是在 setupNodeEvents 中为每个 runner 设置独立的下载目录。
  • Docker 容器权限:确保 Docker 容器中 Cypress 进程对下载目录有写入权限。
javascript
// cypress.config.js - 并行测试隔离下载目录 setupNodeEvents(on, config) { const parallelIndex = config.parallelIndex ?? 0; config.downloadsFolder = `cypress/downloads/worker-${parallelIndex}`; return config; }

掌握 selectFile()cy.readFile() 这两个核心命令,再配合 cy.intercept() 拦截请求,Cypress 中的文件上传下载测试就能覆盖绝大部分场景。关键原则:用拦截请求代替硬编码等待,用 cy.readFile() 的重试机制代替手动轮询,测试前清空下载目录保持环境干净。

标签:Cypress