1. 项目概述
Playwright作为新一代浏览器自动化测试工具,在处理文件交互场景时展现出独特的优势。最近在为一个金融数据平台做自动化测试时,我发现文件上传/下载的稳定性直接影响了整个测试套件的可靠性。经过三个版本迭代和数十次真实环境验证,终于总结出一套完整的解决方案。
文件传输看似简单,实则暗藏玄机。上传时可能遇到表单加密、动态元素、异步回调等问题;下载则面临进度判断、超时控制、文件校验等挑战。本文将分享从基础操作到企业级解决方案的全套实践,包含7种常见场景的应对策略和3个真实项目的优化案例。
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 (queue.length) {
const url = queue.pop()!
try {
await page.goto(url)
const filename = url.split('/').pop()!
await downloadManager.waitForDownload(filename, {
timeout: 30000,
verifyCallback: async path => {
const stats = fs.statSync(path)
return stats.size > 1024 // 确保文件大于1KB
}
})
results.push({url, success: true})
} catch {
results.push({url, success: false})
}
}
})
await Promise.all(workers)
return results
}
5. 高级技巧与异常处理
5.1 上传失败自动重试机制
typescript复制async function reliableUpload(
page: Page,
selector: string,
filePath: string,
maxAttempts = 3
) {
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await page.locator(selector).setInputFiles(filePath)
await page.waitForSelector('.upload-success', { timeout: 30000 })
return // 成功则退出
} catch (error) {
lastError = error as Error
console.warn(`Attempt ${attempt} failed: ${error}`)
await page.reload()
}
}
throw new Error(`Upload failed after ${maxAttempts} attempts: ${lastError}`)
}
5.2 下载文件完整性校验
typescript复制async function verifyDownload(
filePath: string,
expected: {
size?: number
hash?: string
header?: Buffer
}
) {
if (!fs.existsSync(filePath)) {
throw new Error('File not found')
}
const stats = fs.statSync(filePath)
if (expected.size && stats.size !== expected.size) {
throw new Error(`Size mismatch: expected ${expected.size}, got ${stats.size}`)
}
if (expected.hash) {
const fileBuffer = fs.readFileSync(filePath)
const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex')
if (actualHash !== expected.hash) {
throw new Error('Hash verification failed')
}
}
if (expected.header) {
const fd = fs.openSync(filePath, 'r')
const header = Buffer.alloc(expected.header.length)
fs.readSync(fd, header, 0, header.length, 0)
fs.closeSync(fd)
if (!header.equals(expected.header)) {
throw new Error('File header mismatch')
}
}
}
6. 性能优化实践
6.1 并行上传加速策略
typescript复制async function parallelUpload(
page: Page,
files: Array<{
selector: string
path: string
}>,
maxParallel = 3
) {
const semaphore = new Semaphore(maxParallel)
const results = await Promise.all(files.map(async file => {
const release = await semaphore.acquire()
try {
const start = Date.now()
await page.locator(file.selector).setInputFiles(file.path)
const elapsed = Date.now() - start
return { ...file, success: true, duration: elapsed }
} catch (error) {
return { ...file, success: false, error: error as Error }
} finally {
release()
}
}))
const failed = results.filter(r => !r.success)
if (failed.length) {
throw new Error(`${failed.length} uploads failed`)
}
return results
}
6.2 下载限速模拟
typescript复制async function simulateSlowDownload(
page: Page,
downloadUrl: string,
options: {
kbPerSecond: number
variance?: number
}
) {
await page.route(downloadUrl, async route => {
const originalResponse = await route.fetch()
const originalBuffer = await originalResponse.body()
const chunkSize = options.kbPerSecond * 1024
const chunks = []
for (let i = 0; i < originalBuffer.length; i += chunkSize) {
chunks.push(originalBuffer.slice(i, i + chunkSize))
}
const customResponse = new originalResponse.constructor()
Object.assign(customResponse, originalResponse)
customResponse.body = () => {
const readable = new Readable({
async read() {
for (const chunk of chunks) {
this.push(chunk)
await new Promise(resolve =>
setTimeout(resolve, 1000 + (options.variance ?
Math.random() * options.variance * 1000 : 0))
)
}
this.push(null)
}
})
return readable
}
route.fulfill({ response: customResponse })
})
}
7. 跨浏览器兼容方案
7.1 浏览器特定处理逻辑
typescript复制async function handleFileUpload(
browserName: string,
page: Page,
selector: string,
filePath: string
) {
// Firefox对某些上传控件需要特殊处理
if (browserName === 'firefox') {
await page.locator(selector).evaluate(el => {
el.style.display = 'block'
el.style.visibility = 'visible'
})
}
// Safari需要额外的点击事件
if (browserName === 'webkit') {
await page.locator(selector).click({ force: true })
await page.waitForTimeout(500)
}
await page.locator(selector).setInputFiles(filePath)
}
7.2 下载路径兼容处理
typescript复制function getDownloadPath(
browserName: string,
filename: string
) {
const baseDir = process.env.DOWNLOAD_DIR || path.join(process.cwd(), 'downloads')
// 不同浏览器默认下载位置不同
const browserPaths: Record<string, string> = {
chromium: path.join(baseDir, 'chrome'),
firefox: path.join(baseDir, 'firefox'),
webkit: path.join(baseDir, 'safari')
}
const browserDir = browserPaths[browserName] || baseDir
if (!fs.existsSync(browserDir)) {
fs.mkdirSync(browserDir, { recursive: true })
}
return path.join(browserDir, filename)
}
8. 监控与日志体系
8.1 上传下载性能埋点
typescript复制interface TransferMetrics {
startTime: number
endTime?: number
bytesTransferred?: number
success?: boolean
error?: string
}
class TransferMonitor {
private metrics = new Map<string, TransferMetrics>()
startTransfer(id: string) {
this.metrics.set(id, { startTime: Date.now() })
}
endTransfer(id: string, success: boolean, bytes?: number, error?: string) {
const metric = this.metrics.get(id)
if (metric) {
metric.endTime = Date.now()
metric.success = success
metric.bytesTransferred = bytes
metric.error = error
}
}
getMetrics() {
return Array.from(this.metrics.entries()).map(([id, m]) => ({
id,
duration: m.endTime ? m.endTime - m.startTime : null,
...m
}))
}
}
8.2 自动化生成测试报告
typescript复制function generateFileTransferReport(
results: Array<{
type: 'upload' | 'download'
filename: string
status: 'success' | 'failed'
duration: number
size?: number
error?: string
}>
) {
const summary = {
total: results.length,
success: results.filter(r => r.status === 'success').length,
uploads: results.filter(r => r.type === 'upload').length,
downloads: results.filter(r => r.type === 'download').length,
avgDuration: Math.round(
results.reduce((sum, r) => sum + r.duration, 0) / results.length
)
}
const html = `
<!DOCTYPE html>
<html>
<head>
<title>File Transfer Test Report</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
tr:nth-child(even) { background-color: #f2f2f2; }
.success { color: green; }
.failed { color: red; }
</style>
</head>
<body>
<h1>File Transfer Test Report</h1>
<h2>Summary</h2>
<ul>
<li>Total operations: ${summary.total}</li>
<li>Success rate: ${Math.round((summary.success / summary.total) * 100)}%</li>
<li>Average duration: ${summary.avgDuration}ms</li>
</ul>
<h2>Details</h2>
<table>
<tr>
<th>Type</th>
<th>Filename</th>
<th>Status</th>
<th>Duration (ms)</th>
<th>Size (bytes)</th>
<th>Error</th>
</tr>
${results.map(r => `
<tr>
<td>${r.type}</td>
<td>${r.filename}</td>
<td class="${r.status}">${r.status}</td>
<td>${r.duration}</td>
<td>${r.size || 'N/A'}</td>
<td>${r.error || ''}</td>
</tr>
`).join('')}
</table>
</body>
</html>
`
const reportPath = path.join(process.cwd(), 'file-transfer-report.html')
fs.writeFileSync(reportPath, html)
return reportPath
}
9. CI/CD集成方案
9.1 GitHub Actions工作流配置
yaml复制name: File Transfer Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- name: Run Playwright tests
run: npx playwright test --project=${{ matrix.browser }}
env:
DOWNLOAD_DIR: ${{ github.workspace }}/downloads
- name: Upload test report
if: always()
uses: actions/upload-artifact@v3
with:
name: file-transfer-report-${{ matrix.browser }}
path: |
playwright-report/
file-transfer-report.html
downloads/
9.2 失败自动截图收集
typescript复制async function captureFailureArtifacts(
page: Page,
testInfo: TestInfo,
context: {
operation: string
fileInfo?: string
}
) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const screenshotPath = testInfo.outputPath(
`failure-${context.operation}-${timestamp}.png`
)
await page.screenshot({ path: screenshotPath, fullPage: true })
const pageContent = await page.content()
const htmlPath = testInfo.outputPath(
`failure-${context.operation}-${timestamp}.html`
)
fs.writeFileSync(htmlPath, pageContent)
testInfo.attachments.push(
{
name: 'failure-screenshot',
path: screenshotPath,
contentType: 'image/png'
},
{
name: 'failure-page',
path: htmlPath,
contentType: 'text/html'
}
)
if (context.fileInfo) {
const logPath = testInfo.outputPath(
`failure-${context.operation}-${timestamp}.log`
)
fs.writeFileSync(logPath, context.fileInfo)
testInfo.attachments.push({
name: 'failure-context',
path: logPath,
contentType: 'text/plain'
})
}
}
10. 移动端适配技巧
10.1 移动端文件上传模拟
typescript复制async function mobileFileUpload(
page: Page,
options: {
cameraPhoto?: string
galleryImage?: string
document?: string
}
) {
// 模拟相册选择
if (options.galleryImage) {
await page.emulateMedia({ colorScheme: 'light', reducedMotion: 'reduce' })
await page.locator('#gallery-upload').click()
await page.getByText('选择照片').click()
await page.locator('.image-grid img').first().click()
await page.getByText('确认').click()
}
// 模拟拍照上传
if (options.cameraPhoto) {
await page.context().grantPermissions(['camera'])
await page.locator('#camera-upload').click()
await page.waitForSelector('.camera-preview')
await page.evaluate(photoPath => {
const preview = document.querySelector('.camera-preview') as HTMLCanvasElement
const ctx = preview.getContext('2d')!
const img = new Image()
img.src = photoPath
ctx.drawImage(img, 0, 0, preview.width, preview.height)
}, options.cameraPhoto)
await page.getByText('使用照片').click()
}
// 模拟文档选择
if (options.document) {
await page.locator('#file-picker').click()
await page.getByRole('button', { name: '浏览' }).click()
await page.locator('input[type="file"]').setInputFiles(options.document)
}
}
10.2 触摸事件增强
typescript复制async function enhancedTouchUpload(
page: Page,
selector: string,
filePath: string
) {
const element = page.locator(selector)
const box = await element.boundingBox()
if (!box) throw new Error('Element not visible')
// 模拟触摸按下
await page.touchscreen.touchStart(box.x + box.width/2, box.y + box.height/2)
// 模拟轻微移动以触发拖拽事件
await page.touchscreen.touchMove(box.x + box.width/2 + 5, box.y + box.height/2 + 5)
await page.waitForTimeout(300)
// 释放触摸
await page.touchscreen.touchEnd()
// 处理文件选择
const fileChooser = await page.waitForEvent('filechooser')
await fileChooser.setFiles(filePath)
}
11. 安全防护措施
11.1 恶意文件检测
typescript复制async function scanUploadedFile(
filePath: string,
options: {
maxSize?: number
allowedTypes?: string[]
virusScan?: boolean
} = {}
) {
const stats = fs.statSync(filePath)
// 检查文件大小
if (options.maxSize && stats.size > options.maxSize) {
throw new Error(`File exceeds maximum size limit of ${options.maxSize} bytes`)
}
// 检查文件类型
if (options.allowedTypes) {
const fileType = await fromFile(filePath)
if (!fileType || !options.allowedTypes.includes(fileType.mime)) {
throw new Error(`Unsupported file type: ${fileType?.mime || 'unknown'}`)
}
}
// 模拟病毒扫描
if (options.virusScan) {
const fileBuffer = fs.readFileSync(filePath)
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex')
// 这里应该调用实际的病毒扫描服务API
const isMalicious = await checkVirusTotal(hash)
if (isMalicious) {
throw new Error('File detected as malicious')
}
}
}
11.2 下载文件安全隔离
typescript复制class SandboxedDownload {
private tempDir: string
constructor() {
this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'playwright-download-'))
}
async handleDownload(download: Download) {
const filename = download.suggestedFilename()
const safePath = path.join(this.tempDir, sanitizeFilename(filename))
await download.saveAs(safePath)
return {
path: safePath,
stats: fs.statSync(safePath),
read: () => fs.readFileSync(safePath),
dispose: () => fs.unlinkSync(safePath)
}
}
cleanup() {
fs.rmSync(this.tempDir, { recursive: true, force: true })
}
private sanitizeFilename(name: string) {
return name.replace(/[^a-zA-Z0-9._-]/g, '_')
}
}
12. 调试技巧与工具
12.1 网络请求拦截调试
typescript复制async function debugFileTransfer(page: Page) {
// 启用请求/响应日志
page.on('request', request => {
if (request.url().includes('/upload') || request.url().includes('/download')) {
console.log('>>', request.method(), request.url())
}
})
page.on('response', response => {
if (response.url().includes('/upload') || response.url().includes('/download')) {
console.log('<<', response.status(), response.url())
}
})
// 启用慢速网络模拟
await page.context().setOffline(false)
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 })
await page.context().setHTTPCredentials({
username: 'test',
password: 'test123'
})
// 捕获控制台日志
page.on('console', msg => {
if (msg.text().includes('upload') || msg.text().includes('download')) {
console.log('Console:', msg.text())
}
})
}
12.2 可视化追踪工具
typescript复制async function traceFileTransfer(
page: Page,
options: {
traceName: string
screenshots?: boolean
snapshots?: boolean
sources?: boolean
}
) {
const tracePath = `traces/${options.traceName}.zip`
// 开始追踪
await page.context().tracing.start({
name: options.traceName,
screenshots: options.screenshots,
snapshots: options.snapshots,
sources: options.sources
})
// 返回一个停止追踪并保存的工具函数
return {
stopAndSave: async () => {
await page.context().tracing.stop({ path: tracePath })
console.log(`Trace saved to ${tracePath}`)
// 在本地开发时自动打开追踪查看器
if (process.env.NODE_ENV === 'development') {
const { exec } = require('child_process')
exec(`npx playwright show-trace ${tracePath}`)
}
}
}
}
13. 企业级最佳实践
13.1 自动化测试策略
typescript复制interface FileTestConfig {
testCases: Array<{
name: string
type: 'upload' | 'download'
file: string
validations: Array<{
type: 'size' | 'hash' | 'content'
value: string | number
}>
}>
concurrency: number
retries: number
timeout: number
}
async function runFileTransferTestSuite(
page: Page,
config: FileTestConfig
) {
const results = []
const transferMonitor = new TransferMonitor()
for (const testCase of config.testCases) {
const testId = `${testCase.name}-${Date.now()}`
transferMonitor.startTransfer(testId)
try {
let filePath: string | undefined
if (testCase.type === 'upload') {
await reliableUpload(page, '#file-input', testCase.file, config.retries)
await expect(page.locator('.upload-success')).toBeVisible()
} else {
const downloadPromise = page.waitForEvent('download')
await page.click('#download-btn')
const download = await downloadPromise
filePath = await download.path()
for (const validation of testCase.validations) {
if (validation.type === 'size') {
const stats = fs.statSync(filePath)
expect(stats.size).toBe(validation.value)
}
// 其他验证逻辑...
}
}
transferMonitor.endTransfer(testId, true)
results.push({ name: testCase.name, status: 'passed' })
} catch (error) {
transferMonitor.endTransfer(testId, false, undefined, error.message)
results.push({ name: testCase.name, status: 'failed', error: error.message })
await captureFailureArtifacts(page, testInfo, {
operation: testCase.type,
fileInfo: `File: ${testCase.file}\nError: ${error.message}`
})
}
}
return {
results,
metrics: transferMonitor.getMetrics()
}
}
13.2 性能基准测试
typescript复制async function benchmarkFileTransfer(
page: Page,
operations: Array<{
type: 'upload' | 'download'
file: string
iterations: number
}>
) {
const benchmarks = []
for (const op of operations) {
const durations = []
let successCount = 0
for (let i = 0; i < op.iterations; i++) {
try {
const start = Date.now()
if (op.type === 'upload') {
await page.locator('#file-input').setInputFiles(op.file)
await page.waitForSelector('.upload-success', { timeout: 10000 })
} else {
const downloadPromise = page.waitForEvent('download')
await page.click('#download-btn')
const download = await downloadPromise
await download.path()
}
const duration = Date.now() - start
durations.push(duration)
successCount++
} catch (error) {
console.warn(`Operation failed: ${error}`)
}
}
if (durations.length > 0) {
benchmarks.push({
type: op.type,
file: op.file,
iterations: op.iterations,
successRate: (successCount / op.iterations) * 100,
avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
minDuration: Math.min(...durations),
maxDuration: Math.max(...durations),
percentiles: {
p50: calculatePercentile(durations, 50),
p90: calculatePercentile(durations, 90),
p95: calculatePercentile(durations, 95)
}
})
}
}
return benchmarks
}
function calculatePercentile(values: number[], percentile: number) {
const sorted = [...values].sort((a, b) => a - b)
const index = Math.ceil((percentile / 100) * sorted.length) - 1
return sorted[index]
}
14. 扩展应用场景
14.1 云存储集成测试
typescript复制async function testCloudStorageIntegration(
page: Page,
cloudConfig: {
provider: 'aws' | 'azure' | 'gcp'
bucket: string
credentials: {
accessKey: string
secretKey: string
}
}
) {
// 模拟云存储授权
await page.evaluate((config) => {
localStorage.setItem('cloudConfig', JSON.stringify(config))
}, cloudConfig)
// 测试大文件分块上传
const largeFile = generateTestFile(1024 * 1024 * 50) // 50MB
await page.locator('#cloud-upload').setInputFiles(largeFile.path)
await page.waitForSelector('.upload-progress', { state: 'visible' })
await expect(page.locator('.upload-progress')).toHaveText('100%', { timeout: 300000 })
// 验证云端文件
const cloudFile = await getCloudFileInfo(cloudConfig, largeFile.name)
expect(cloudFile.size).toBe(largeFile.size)
expect(cloudFile.hash).toBe(largeFile.hash)
// 测试下载
await page.locator('#cloud-download').click()
const download = await page.waitForEvent('download')
const localPath = await download.path()
const localFile = {
size: fs.statSync(localPath).size,
hash: crypto.createHash('sha256').update(fs.readFileSync(localPath)).digest('hex')
}
expect(localFile.size).toBe(cloudFile.size)
expect(localFile.hash).toBe(cloudFile.hash)
}
function generateTestFile(size: number) {
const name = `test-${Date.now()}.bin`
const path = `./temp/${name}`
const buffer = crypto.randomBytes(size)
fs.writeFileSync(path, buffer)
return {
name,
path,
size,
hash: crypto.createHash('sha256').update(buffer).digest('hex')
}
}
14.2 跨域文件共享测试
typescript复制async function testCrossOriginFileSharing(
page: Page,
originA: string,
originB: string
) {
// 在originA上传文件
await page.goto(originA)
const fileA = generateTestFile(1024 * 1024) // 1MB
await page.locator('#upload').setInputFiles(fileA.path)
const shareUrl = await page.locator('#share-url').inputValue()
// 在originB访问共享文件
const pageB = await page.context().newPage()
await pageB.goto(originB)
await pageB.locator('#shared-url').fill(shareUrl)
await pageB.locator('#download-shared').click()
// 验证下载的文件
const download = await pageB.waitForEvent('download')
const downloadedPath = await download.path()
const downloadedHash = crypto
.createHash('sha256')
.update(fs.readFileSync(downloadedPath))
.digest('hex')
expect(downloadedHash).toBe(fileA.hash)
await pageB.close()
}
15. 未来演进方向
15.1 基于AI的智能文件验证
typescript复制async function aiValidateFile(
filePath: string,
options: {
image?: {
detectObjects?: boolean
checkQuality?: boolean
}
document?: {
extractText?: boolean
checkSensitiveInfo?: boolean
}
}
) {
const fileType = await fromFile(filePath)
const fileBuffer = fs.readFileSync(filePath)
// 图像智能分析
if (fileType?.mime.startsWith('image/') && options.image) {
const imageAnalysis = await analyzeImage(fileBuffer, {
objectDetection: options.image.detectObjects,
qualityCheck: options.image.checkQuality
})
if (imageAnalysis.objects?.some(obj => obj.label === 'weapon')) {
throw new Error('Sensitive content detected in image')
}
if (imageAnalysis.qualityScore && imageAnalysis.qualityScore < 0.5) {
throw new Error('Low quality image detected')
}
}
// 文档智能分析
if (
(fileType?.mime === 'application/pdf' ||
fileType?.mime === 'application/msword') &&
options.document
) {
const textContent = await extractText(fileBuffer)
if (options.document.checkSensitiveInfo) {