1. 项目概述
Playwright作为新一代浏览器自动化测试工具,在处理文件交互场景时展现出独特的优势。最近在为一个金融数据平台做自动化测试时,我发现文件上传/下载的稳定性直接影响了整个测试套件的可靠性。经过三个版本迭代和数十次真实环境验证,终于总结出一套完整的解决方案。
文件传输看似简单,实则暗藏玄机。上传时可能遇到表单加密、动态元素、异步回调等问题;下载则面临进度判断、超时控制、文件校验等挑战。本文将分享从基础操作到企业级解决方案的全套实践,包含7种常见场景的应对策略和3个真实项目的优化案例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心需求解析
2.1 文件上传的四大技术难点
-
动态元素定位:现代前端框架生成的元素ID常带哈希值,例如:
html复制<input id="file-upload-5f3d8a" type="file">解决方案是使用CSS属性选择器:
typescript复制await page.locator('input[type="file"]').setInputFiles('test.pdf') -
非标准上传控件:如拖拽上传区域实际是div元素,需触发特定事件:
typescript复制const uploadArea = page.locator('.drop-zone') await uploadArea.dispatchEvent('dragenter') const fileChooser = await page.waitForEvent('filechooser') await fileChooser.setFiles('test.pdf') -
多文件批量处理:金融系统常需同时上传多个对账单:
typescript复制const files = ['Q1.pdf', 'Q2.pdf', 'Q3.pdf'] await page.locator('#multi-upload').setInputFiles(files) -
上传进度监控:大文件需可视化进度,通过监听请求实现:
typescript复制page.on('request', request => { if (request.url().includes('/upload')) { console.log(`Upload progress: ${request.postDataBuffer()?.length} bytes`) } })
2.2 下载完成的五种判断方式
-
等待下载事件(推荐方案):
typescript复制const downloadPromise = page.waitForEvent('download') await page.getByText('Export CSV').click() const download = await downloadPromise const path = await download.path() // 获取临时文件路径 -
网络请求监听:
typescript复制const responsePromise = page.waitForResponse(res => res.url().includes('/export') && res.status() === 200 ) await page.click('#export-btn') const response = await responsePromise -
文件系统轮询:
typescript复制async function waitForFile(path: string, timeout = 30000) { const start = Date.now() while (Date.now() - start < timeout) { if (fs.existsSync(path)) { const stats = fs.statSync(path) if (stats.size > 0) return true } await new Promise(r => setTimeout(r, 500)) } throw new Error('File not found within timeout') } -
DOM状态检测:
typescript复制await page.waitForSelector('.download-complete', { state: 'visible' }) -
结合文件哈希校验:
typescript复制const expectedHash = 'a1b2c3d4...' const fileBuffer = fs.readFileSync(downloadPath) const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex') expect(actualHash).toBe(expectedHash)
3. 企业级解决方案设计
3.1 上传组件封装
typescript复制class FileUploader {
constructor(private page: Page) {}
async upload(
selector: string,
filePaths: string[],
options?: {
timeout?: number
progressCallback?: (percent: number) => void
}
) {
const uploadStart = Date.now()
const fileChooserPromise = this.page.waitForEvent('filechooser')
await this.page.locator(selector).click()
const fileChooser = await fileChooserPromise
if (options?.progressCallback) {
this.page.on('request', request => {
if (request.url().includes('/upload')) {
const loaded = request.postDataBuffer()?.length || 0
const total = filePaths.reduce((sum, f) => sum + fs.statSync(f).size, 0)
options.progressCallback(Math.round((loaded / total) * 100))
}
})
}
await fileChooser.setFiles(filePaths)
await this.page.waitForTimeout(500) // 确保上传完成
if (Date.now() - uploadStart > (options?.timeout || 30000)) {
throw new Error('Upload timeout exceeded')
}
}
}
3.2 下载管理器实现
typescript复制class DownloadManager {
private downloads = new Map<string, Download>()
constructor(private page: Page) {
page.on('download', download => {
const key = download.suggestedFilename()
this.downloads.set(key, download)
})
}
async waitForDownload(
filename: string,
options?: {
timeout?: number
verifyCallback?: (path: string) => Promise<boolean>
}
): Promise<string> {
const start = Date.now()
const timeout = options?.timeout || 60000
while (Date.now() - start < timeout) {
const download = this.downloads.get(filename)
if (download) {
const path = await download.path()
if (options?.verifyCallback) {
if (await options.verifyCallback(path)) {
return path
}
} else if (fs.existsSync(path)) {
return path
}
}
await this.page.waitForTimeout(500)
}
throw new Error(`Download timeout for ${filename}`)
}
}
4. 实战案例解析
4.1 银行对账单上传系统
场景特点:
- 需要同时上传PDF和XML文件
- 后端有严格的格式校验
- 文件大小常超过50MB
解决方案:
typescript复制test('上传年度对账单', async ({ page }) => {
const uploader = new FileUploader(page)
const progressLog: number[] = []
await uploader.upload(
'#statement-upload',
['2023-Q1.pdf', '2023-Q1.xml', '2023-Q2.pdf', '2023-Q2.xml'],
{
timeout: 120000,
progressCallback: percent => {
progressLog.push(percent)
console.log(`上传进度: ${percent}%`)
}
}
)
// 验证进度连续性
for (let i = 1; i < progressLog.length; i++) {
expect(progressLog[i]).toBeGreaterThanOrEqual(progressLog[i-1])
}
// 验证成功提示
await expect(page.locator('.upload-success')).toBeVisible()
})
4.2 电商平台图片批量下载
特殊需求:
- 需要并发下载数百张商品图片
- 服务器有速率限制
- 需要自动重试失败项
实现方案:
typescript复制async function batchDownload(
page: Page,
urls: string[],
concurrency = 3
) {
const downloadManager = new DownloadManager(page)
const results: Array<{url: string; success: boolean}> = []
const queue = [...urls]
const workers = Array(concurrency).fill(null).map(async () => {
while (
