/** * Electron Builder Agent Test — SF-06 * * 验证 Electron Wrapper 生成器的所有关键功能。 */ import { describe, it, before } from "node:test"; import assert from "node:assert/strict"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { existsSync, rmSync, readFileSync } from "node:fs"; const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url))); const WORKSPACE = resolve(__dirname, ".."); let buildElectron, writeElectron; before(async () => { const mod = await import("../scripts/electron-builder-agent.mjs"); buildElectron = mod.buildElectron; writeElectron = mod.writeElectron; }); // ═══════════════════════════════════════════════════════ // 1 — Desktop Project 生成 // ═══════════════════════════════════════════════════════ describe("1 — Desktop Project 结构生成", () => { let result; before(() => { result = buildElectron({ projectName: "PetCare" }); }); it("生成成功(无错误)", () => { assert.ok(!result.error, `Unexpected error: ${result.error}`); assert.ok(result.stats.totalFiles >= 8, `Expected >= 8 files, got ${result.stats.totalFiles}`); }); it("包含 electron/ 目录下的所有核心文件", () => { assert.ok(result.files["electron/main.ts"], "Missing main.ts"); assert.ok(result.files["electron/preload.ts"], "Missing preload.ts"); assert.ok(result.files["electron/ipc.ts"], "Missing ipc.ts"); assert.ok(result.files["electron/tray.ts"], "Missing tray.ts"); assert.ok(result.files["electron/updater.ts"], "Missing updater.ts"); assert.ok(result.files["electron/utils.ts"], "Missing utils.ts"); }); it("包含 assets/icon.png", () => { assert.ok(result.files["assets/icon.png"], "Missing icon.png"); assert.ok(result.files["assets/icon.png"].length > 0, "Empty icon.png"); }); }); // ═══════════════════════════════════════════════════════ // 2 — main.ts 内容验证 // ═══════════════════════════════════════════════════════ describe("2 — main.ts 内容验证", () => { let result; before(() => { result = buildElectron({ projectName: "PetCare" }); }); it("包含 BrowserWindow", () => { assert.ok(result.files["electron/main.ts"].includes("BrowserWindow")); }); it("包含 Menu", () => { assert.ok(result.files["electron/main.ts"].includes("Menu")); }); it("包含 Tray setup", () => { assert.ok(result.files["electron/main.ts"].includes("setupTray")); }); it("包含 IPC setup", () => { assert.ok(result.files["electron/main.ts"].includes("setupIPC")); }); it("包含 Single Instance Lock", () => { assert.ok(result.files["electron/main.ts"].includes("checkSingleInstance")); assert.ok(result.files["electron/utils.ts"].includes("requestSingleInstanceLock")); }); it("包含 Auto Start 配置", () => { assert.ok(result.files["electron/main.ts"].includes("activate")); }); it("包含 contextIsolation: true", () => { assert.ok(result.files["electron/main.ts"].includes("contextIsolation: true")); }); it("包含 nodeIntegration: false", () => { assert.ok(result.files["electron/main.ts"].includes("nodeIntegration: false")); }); it("开发环境加载 localhost URL", () => { assert.ok(result.files["electron/main.ts"].includes("localhost")); }); it("生产环境加载 file 或 resources 路径", () => { const main = result.files["electron/main.ts"]; assert.ok(main.includes("resourcesPath") || main.includes("loadFile")); }); it("包含外部链接处理", () => { assert.ok(result.files["electron/main.ts"].includes("setWindowOpenHandler")); }); it("包含 GPU crash 处理", () => { assert.ok(result.files["electron/main.ts"].includes("gpu-process-crashed")); }); it("包含 API server 生产环境启动", () => { assert.ok(result.files["electron/main.ts"].includes("startApiServer")); }); }); // ═══════════════════════════════════════════════════════ // 3 — preload.ts 内容验证 // ═══════════════════════════════════════════════════════ describe("3 — preload.ts 内容验证", () => { let result; before(() => { result = buildElectron({ projectName: "CRM" }); }); it("使用 contextBridge", () => { assert.ok(result.files["electron/preload.ts"].includes("contextBridge")); }); it("使用 ipcRenderer", () => { assert.ok(result.files["electron/preload.ts"].includes("ipcRenderer")); }); it("暴露 electronAPI", () => { assert.ok(result.files["electron/preload.ts"].includes("electronAPI")); }); it("包含窗口控制 API", () => { const preload = result.files["electron/preload.ts"]; assert.ok(preload.includes("minimizeWindow")); assert.ok(preload.includes("maximizeWindow")); assert.ok(preload.includes("closeWindow")); }); it("包含文件对话框 API", () => { const preload = result.files["electron/preload.ts"]; assert.ok(preload.includes("openFile")); assert.ok(preload.includes("saveFile")); }); it("包含通知 API", () => { assert.ok(result.files["electron/preload.ts"].includes("showNotification")); }); it("包含持久化存储 API", () => { const preload = result.files["electron/preload.ts"]; assert.ok(preload.includes("storeGet")); assert.ok(preload.includes("storeSet")); assert.ok(preload.includes("storeDelete")); }); it("包含更新 API", () => { const preload = result.files["electron/preload.ts"]; assert.ok(preload.includes("checkForUpdates")); assert.ok(preload.includes("downloadUpdate")); assert.ok(preload.includes("quitAndInstall")); }); it("包含 TypeScript 类型声明", () => { assert.ok(result.files["electron/preload.ts"].includes("interface ElectronAPI")); assert.ok(result.files["electron/preload.ts"].includes("declare global")); }); }); // ═══════════════════════════════════════════════════════ // 4 — package.json 验证 // ═══════════════════════════════════════════════════════ describe("4 — package.json 验证", () => { let result, pkg; before(() => { result = buildElectron({ projectName: "Inventory" }); pkg = JSON.parse(result.files["package.json"]); }); it("包含正确名称", () => { assert.ok(pkg.name.includes("inventory-desktop")); }); it("包含 main 入口", () => { assert.equal(pkg.main, "dist/electron/main.js"); }); it("包含 dev 脚本", () => { assert.ok(pkg.scripts.dev, "Missing dev script"); assert.ok(pkg.scripts.dev.includes("concurrently")); }); it("包含 build 脚本", () => { assert.ok(pkg.scripts.build, "Missing build script"); }); it("包含平台构建脚本", () => { assert.ok(pkg.scripts["build:electron:win"], "Missing win build"); assert.ok(pkg.scripts["build:electron:mac"], "Missing mac build"); assert.ok(pkg.scripts["build:electron:linux"], "Missing linux build"); }); it("依赖 electron", () => { assert.ok(pkg.devDependencies.electron, "Missing electron"); }); it("依赖 electron-builder", () => { assert.ok(pkg.devDependencies["electron-builder"], "Missing electron-builder"); }); it("依赖 electron-updater", () => { assert.ok(pkg.dependencies["electron-updater"], "Missing electron-updater"); }); it("依赖 concurrently", () => { assert.ok(pkg.devDependencies.concurrently, "Missing concurrently"); }); it("依赖 typescript", () => { assert.ok(pkg.devDependencies.typescript, "Missing typescript"); }); it("build 配置包含 appId", () => { assert.ok(pkg.build.appId.includes("inventory.desktop")); }); it("build 配置包含 productName", () => { assert.equal(pkg.build.productName, "Inventory"); }); }); // ═══════════════════════════════════════════════════════ // 5 — electron-builder.yml 验证 // ═══════════════════════════════════════════════════════ describe("5 — electron-builder.yml 验证", () => { let result, yml; before(() => { result = buildElectron({ projectName: "PetCare" }); yml = result.files["electron-builder.yml"]; }); it("包含 appId", () => { assert.ok(yml.includes("appId: com.petcare.desktop")); }); it("包含 productName", () => { assert.ok(yml.includes("productName: PetCare")); }); it("包含 macOS 配置", () => { assert.ok(yml.includes("mac:")); assert.ok(yml.includes("dmg")); assert.ok(yml.includes("arm64")); }); it("包含 Windows 配置", () => { assert.ok(yml.includes("win:")); assert.ok(yml.includes("nsis")); }); it("包含 Linux 配置", () => { assert.ok(yml.includes("linux:")); assert.ok(yml.includes("AppImage")); assert.ok(yml.includes("deb")); }); it("包含 extraResources(web + api)", () => { assert.ok(yml.includes("extraResources")); assert.ok(yml.includes("../web/out")); assert.ok(yml.includes("../api/dist")); }); it("包含 publish 配置", () => { assert.ok(yml.includes("publish:")); assert.ok(yml.includes("provider: generic")); }); }); // ═══════════════════════════════════════════════════════ // 6 — tsconfig.json 验证 // ═══════════════════════════════════════════════════════ describe("6 — tsconfig.json 验证", () => { let result, tsconfig; before(() => { result = buildElectron(); tsconfig = JSON.parse(result.files["tsconfig.json"]); }); it("target 是 ES2022", () => { assert.equal(tsconfig.compilerOptions.target, "ES2022"); }); it("module 是 commonjs(Electron 主进程需要)", () => { assert.equal(tsconfig.compilerOptions.module, "commonjs"); }); it("strict 模式开启", () => { assert.equal(tsconfig.compilerOptions.strict, true); }); it("outDir 是 dist", () => { assert.equal(tsconfig.compilerOptions.outDir, "dist"); }); it("include 包含 electron/", () => { assert.ok(tsconfig.include.some(p => p.includes("electron"))); }); }); // ═══════════════════════════════════════════════════════ // 7 — 其他文件验证 // ═══════════════════════════════════════════════════════ describe("7 — 辅助文件验证", () => { let result; before(() => { result = buildElectron({ projectName: "CRM" }); }); it("README.md 存在且包含关键内容", () => { const readme = result.files["README.md"]; assert.ok(readme, "Missing README.md"); assert.ok(readme.includes("CRM"), "Missing project name"); assert.ok(readme.includes("Development"), "Missing Development section"); assert.ok(readme.includes("Build"), "Missing Build section"); assert.ok(readme.includes("IPC API"), "Missing IPC API section"); }); it("entitlements.mac.plist 存在", () => { const plist = result.files["entitlements.mac.plist"]; assert.ok(plist, "Missing entitlements.mac.plist"); assert.ok(plist.includes("com.apple.security.network.client")); }); it("icon.png 存在且非空", () => { assert.ok(result.files["assets/icon.png"], "Missing icon.png"); assert.ok(result.files["assets/icon.png"].length > 0); }); }); // ═══════════════════════════════════════════════════════ // 8 — 自定义端口配置 // ═══════════════════════════════════════════════════════ describe("8 — 自定义端口配置", () => { it("使用自定义 web 端口", () => { const result = buildElectron({ webPort: 4000 }); assert.ok(result.files["electron/main.ts"].includes("4000")); }); it("使用自定义 api 端口", () => { const result = buildElectron({ apiPort: 4001 }); assert.ok(result.files["electron/main.ts"].includes("4001")); assert.ok(result.files["electron/utils.ts"].includes("4001")); }); it("默认端口是 3000/3001", () => { const result = buildElectron(); assert.ok(result.files["electron/main.ts"].includes("3000")); assert.ok(result.files["electron/main.ts"].includes("3001")); }); }); // ═══════════════════════════════════════════════════════ // 9 — 写入 I/O // ═══════════════════════════════════════════════════════ describe("9 — 文件写入 I/O", () => { it("writeElectron 写入完整目录树", () => { const result = buildElectron({ projectName: "TestApp" }); const tmpDir = resolve(WORKSPACE, ".tmp-electron-test"); writeElectron(result, tmpDir); assert.ok(existsSync(resolve(tmpDir, "package.json"))); assert.ok(existsSync(resolve(tmpDir, "electron-builder.yml"))); assert.ok(existsSync(resolve(tmpDir, "tsconfig.json"))); assert.ok(existsSync(resolve(tmpDir, "electron/main.ts"))); assert.ok(existsSync(resolve(tmpDir, "electron/preload.ts"))); assert.ok(existsSync(resolve(tmpDir, "electron/ipc.ts"))); assert.ok(existsSync(resolve(tmpDir, "electron/tray.ts"))); assert.ok(existsSync(resolve(tmpDir, "electron/updater.ts"))); assert.ok(existsSync(resolve(tmpDir, "electron/utils.ts"))); assert.ok(existsSync(resolve(tmpDir, "assets/icon.png"))); assert.ok(existsSync(resolve(tmpDir, "entitlements.mac.plist"))); assert.ok(existsSync(resolve(tmpDir, "README.md"))); rmSync(tmpDir, { recursive: true, force: true }); }); it("所有生成的文件非空", () => { const result = buildElectron(); for (const [path, content] of Object.entries(result.files)) { const len = typeof content === "string" ? content.length : content.byteLength; assert.ok(len > 0, `${path}: empty content`); } }); it("所有 package.json 可解析", () => { const result = buildElectron(); const pkg = JSON.parse(result.files["package.json"]); assert.ok(pkg.name, "Missing package name"); assert.ok(pkg.scripts, "Missing scripts"); }); }); // ═══════════════════════════════════════════════════════ // 10 — 多领域覆盖 // ═══════════════════════════════════════════════════════ describe("10 — 多领域覆盖(PetCare / CRM / Inventory)", () => { const domains = [ { name: "PetCare", expected: "petcare" }, { name: "CRM", expected: "crm" }, { name: "Inventory", expected: "inventory" }, ]; for (const { name, expected } of domains) { it(`${name} — 生成成功`, () => { const result = buildElectron({ projectName: name }); assert.ok(!result.error, `${name}: ${result.message}`); assert.ok(result.files["electron/main.ts"], `${name}: missing main.ts`); assert.ok(result.files["electron/preload.ts"], `${name}: missing preload.ts`); assert.ok(result.files["package.json"], `${name}: missing package.json`); assert.ok(result.files["electron-builder.yml"], `${name}: missing electron-builder.yml`); }); it(`${name} — package.json 包含正确名称`, () => { const result = buildElectron({ projectName: name }); const pkg = JSON.parse(result.files["package.json"]); assert.ok(pkg.name.includes(expected), `${name}: expected '${expected}' in name, got '${pkg.name}'`); }); it(`${name} — electron-builder.yml 包含正确 appId`, () => { const result = buildElectron({ projectName: name }); assert.ok( result.files["electron-builder.yml"].includes(`com.${expected}.desktop`), `${name}: expected 'com.${expected}.desktop' in appId` ); }); } });