/** * E2E 测试公共设施(v0.8.2 P3-3) * * 封装"启动应用(隔离 userData)+ 本地 mock LLM + 清理"的完整生命周期。 * 每个测试文件独立调用 launchApp() —— 应用实例与 mock 互相隔离,杜绝跨用例 * 的运行态竞态(中断链路对"上一 run 的收尾兜底定时器"这类跨用例污染敏感)。 */ import type { ElectronApplication, Page } from '@playwright/test'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { _electron as electron } from 'playwright'; import { startMockLLM, type MockLLMHandle } from './mock-llm'; export interface AppHarness { electronApp: ElectronApplication; page: Page; mock: MockLLMHandle; userDataDir: string; workspaceDir: string; /** 关闭应用 + mock + 清理临时目录 */ cleanup: () => Promise; } export async function launchApp(): Promise { // 1. 本地 mock Provider(随机端口,仅监听 127.0.0.1) const mock = await startMockLLM(); // 2. 隔离的用户数据与工作空间目录 const userDataDir = mkdtempSync(join(tmpdir(), 'metona-e2e-user-')); const workspaceDir = join(userDataDir, 'workspace'); mkdirSync(workspaceDir, { recursive: true }); // workspace-config.json 指向隔离工作空间(main.ts 启动时优先读取) writeFileSync( join(userDataDir, 'workspace-config.json'), JSON.stringify({ workspacePath: workspaceDir }), 'utf-8', ); // 3. 启动 Electron(产物构建由 npm run test:e2e 的 build 步骤保证) const electronApp = await electron.launch({ args: ['.'], env: { ...process.env, METONA_USER_DATA_DIR: userDataDir, METONA_E2E_SEED_CONFIG: '1', METONA_E2E_LLM_URL: mock.url, // 隔离冒烟环境无更新源(生产环境 updater 由 feedUrl 空值禁用,双保险) METONA_E2E_DISABLE_UPDATE: '1', } as Record, }); const page = await electronApp.firstWindow(); await page.waitForLoadState('domcontentloaded'); const cleanup = async (): Promise => { await electronApp.close(); await mock.close(); // 调试期保留 userData(main.log 排查用);稳定后恢复清理 if (!process.env['METONA_E2E_KEEP_USER_DATA']) { try { rmSync(userDataDir, { recursive: true, force: true }); } catch { /* Windows 文件句柄释放延迟 — 清理失败不影响断言 */ } } }; return { electronApp, page, mock, userDataDir, workspaceDir, cleanup }; }