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 { 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 } } /** * 主进程 stub:showOpenDialog 按调用序返回指定路径(每次调用取该组第一个文件)。 * 闭包计数状态保留在主进程,覆盖「打开左侧 → 打开右侧」的连续调用。 */ export async function stubOpenDialogSeq(app: ElectronApplication, seqPaths: string[][]): Promise { 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) } /** 主进程 stub:showSaveDialog 固定返回指定保存路径 */ export async function stubSaveDialog(app: ElectronApplication, savePath: string): Promise { await app.evaluate(({ dialog }, p: string) => { dialog.showSaveDialog = async () => ({ canceled: false, filePath: p }) }, savePath) } /** 创建临时目录(存放测试 fixture / 导出产物) */ export async function makeTmpDir(prefix = 'difflens-e2e-'): Promise { return mkdtemp(join(tmpdir(), prefix)) } /** 清理临时目录(递归、不存在时忽略) */ export async function removeTmpDir(dir: string): Promise { await rm(dir, { recursive: true, force: true }) } /** * 写入文本 fixture:encoding 为 'utf8'(可带 bom)或 'gbk'(iconv-lite 编码)。 * 返回文件绝对路径。 */ export async function writeTextFixture( dir: string, name: string, content: string, encoding: 'utf8' | 'utf8-bom' | 'gbk' = 'utf8' ): Promise { 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 { 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() }