feat: 解码 worker 化、Playwright E2E 测试体系与文件夹对比(0.6.0)

This commit is contained in:
2026-08-18 14:40:44 +08:00
parent 2b43b5e676
commit be619c2377
24 changed files with 2030 additions and 151 deletions
+95
View File
@@ -0,0 +1,95 @@
import { _electron as electron, type ElectronApplication, type Page } from '@playwright/test'
import { mkdtemp, writeFile, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import iconv from 'iconv-lite'
/**
* E2E 辅助:启动构建产物(out/)对应的真实 Electron 应用,
* 以及文件对话框 stub 与临时 fixture 生成。
* Electron 模式复用项目自带 Electron 二进制,无需下载 Playwright 浏览器。
*/
export interface LaunchedApp {
app: ElectronApplication
window: Page
}
/**
* 启动应用并等待首个窗口就绪。
* fresh(默认 true):清除上一轮测试残留的偏好持久化(localStorage 跨实例共享 userData
* 并重载页面,保证以默认选项启动;偏好持久化用例的验证实例显式传 false。
*/
export async function launchApp(options: { fresh?: boolean } = {}): Promise<LaunchedApp> {
const { fresh = true } = options
const app = await electron.launch({ args: ['.'] })
const window = await app.firstWindow()
await window.waitForLoadState('domcontentloaded')
if (fresh) {
await window.evaluate(() => window.localStorage.clear())
await window.reload()
await window.waitForLoadState('domcontentloaded')
}
return { app, window }
}
/**
* 主进程 stubshowOpenDialog 按调用序返回指定路径(每次调用取该组第一个文件)。
* 闭包计数状态保留在主进程,覆盖「打开左侧 → 打开右侧」的连续调用。
*/
export async function stubOpenDialogSeq(app: ElectronApplication, seqPaths: string[][]): Promise<void> {
await app.evaluate(({ dialog }, seq: string[][]) => {
let i = 0
dialog.showOpenDialog = async () => {
const paths = seq[Math.min(i, seq.length - 1)] ?? []
i++
return { canceled: false, filePaths: paths }
}
}, seqPaths)
}
/** 主进程 stubshowSaveDialog 固定返回指定保存路径 */
export async function stubSaveDialog(app: ElectronApplication, savePath: string): Promise<void> {
await app.evaluate(({ dialog }, p: string) => {
dialog.showSaveDialog = async () => ({ canceled: false, filePath: p })
}, savePath)
}
/** 创建临时目录(存放测试 fixture / 导出产物) */
export async function makeTmpDir(prefix = 'difflens-e2e-'): Promise<string> {
return mkdtemp(join(tmpdir(), prefix))
}
/** 清理临时目录(递归、不存在时忽略) */
export async function removeTmpDir(dir: string): Promise<void> {
await rm(dir, { recursive: true, force: true })
}
/**
* 写入文本 fixtureencoding 为 'utf8'(可带 bom)或 'gbk'iconv-lite 编码)。
* 返回文件绝对路径。
*/
export async function writeTextFixture(
dir: string,
name: string,
content: string,
encoding: 'utf8' | 'utf8-bom' | 'gbk' = 'utf8'
): Promise<string> {
const path = join(dir, name)
if (encoding === 'gbk') {
await writeFile(path, iconv.encode(content, 'gbk'))
} else if (encoding === 'utf8-bom') {
await writeFile(path, '\ufeff' + content, 'utf8')
} else {
await writeFile(path, content, 'utf8')
}
return path
}
/** 通过「粘贴文本」入口向指定侧粘贴内容(覆盖粘贴弹窗全链路) */
export async function pasteText(window: Page, side: 'left' | 'right', text: string): Promise<void> {
await window.getByRole('button', { name: '粘贴文本' }).click()
await window.getByRole('button', { name: side === 'left' ? '粘贴到左侧' : '粘贴到右侧' }).click()
await window.getByPlaceholder(/粘贴或输入/).fill(text)
await window.getByRole('button', { name: '开始对比' }).click()
}