Spaces:
Paused
Paused
icebear0828 commited on
Commit ·
325c8ea
1
Parent(s): a5b6a79
test: add electron package tests (33 cases)
Browse files- prepare-pack: copy/clean behavior, idempotency, missing dirs
- auto-updater: state machine transitions, timer scheduling, throttle
- build: esbuild smoke test, output validation, export verification
- builder-config: YAML structure, path validation, version sync
packages/electron/__tests__/auto-updater.test.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Tests for electron/auto-updater.ts
|
| 3 |
+
*
|
| 4 |
+
* Mocks electron-updater and electron dialog to test state transitions
|
| 5 |
+
* without a real Electron runtime.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
| 9 |
+
import { EventEmitter } from "events";
|
| 10 |
+
|
| 11 |
+
// ── Mocks ────────────────────────────────────────────────────────────
|
| 12 |
+
|
| 13 |
+
// Create a mock autoUpdater as an EventEmitter with methods
|
| 14 |
+
const mockAutoUpdater = Object.assign(new EventEmitter(), {
|
| 15 |
+
autoDownload: true,
|
| 16 |
+
autoInstallOnAppQuit: false,
|
| 17 |
+
allowPrerelease: false,
|
| 18 |
+
checkForUpdates: vi.fn().mockResolvedValue(undefined),
|
| 19 |
+
downloadUpdate: vi.fn().mockResolvedValue(undefined),
|
| 20 |
+
quitAndInstall: vi.fn(),
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
vi.mock("electron-updater", () => ({
|
| 24 |
+
autoUpdater: mockAutoUpdater,
|
| 25 |
+
}));
|
| 26 |
+
|
| 27 |
+
vi.mock("electron", () => ({
|
| 28 |
+
BrowserWindow: class {},
|
| 29 |
+
dialog: {
|
| 30 |
+
showMessageBox: vi.fn().mockResolvedValue({ response: 1 }), // "Later" by default
|
| 31 |
+
},
|
| 32 |
+
}));
|
| 33 |
+
|
| 34 |
+
// Import after mocks are set up
|
| 35 |
+
const { getAutoUpdateState, initAutoUpdater, stopAutoUpdater } = await import(
|
| 36 |
+
"../electron/auto-updater.js"
|
| 37 |
+
);
|
| 38 |
+
|
| 39 |
+
describe("auto-updater state machine", () => {
|
| 40 |
+
const mockOptions = {
|
| 41 |
+
getMainWindow: vi.fn().mockReturnValue(null),
|
| 42 |
+
rebuildTrayMenu: vi.fn(),
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
beforeEach(() => {
|
| 46 |
+
vi.useFakeTimers();
|
| 47 |
+
vi.clearAllMocks();
|
| 48 |
+
mockAutoUpdater.removeAllListeners();
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
afterEach(() => {
|
| 52 |
+
stopAutoUpdater();
|
| 53 |
+
vi.useRealTimers();
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
it("initial state is idle", () => {
|
| 57 |
+
const state = getAutoUpdateState();
|
| 58 |
+
expect(state.checking).toBe(false);
|
| 59 |
+
expect(state.updateAvailable).toBe(false);
|
| 60 |
+
expect(state.downloading).toBe(false);
|
| 61 |
+
expect(state.downloaded).toBe(false);
|
| 62 |
+
expect(state.version).toBeNull();
|
| 63 |
+
expect(state.error).toBeNull();
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
it("configures autoUpdater on init", () => {
|
| 67 |
+
initAutoUpdater(mockOptions);
|
| 68 |
+
|
| 69 |
+
expect(mockAutoUpdater.autoDownload).toBe(false);
|
| 70 |
+
expect(mockAutoUpdater.autoInstallOnAppQuit).toBe(true);
|
| 71 |
+
expect(mockAutoUpdater.allowPrerelease).toBe(false);
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
it("schedules initial check after 30s delay", () => {
|
| 75 |
+
initAutoUpdater(mockOptions);
|
| 76 |
+
|
| 77 |
+
expect(mockAutoUpdater.checkForUpdates).not.toHaveBeenCalled();
|
| 78 |
+
|
| 79 |
+
vi.advanceTimersByTime(30_000);
|
| 80 |
+
|
| 81 |
+
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
it("schedules periodic check every 4 hours", () => {
|
| 85 |
+
initAutoUpdater(mockOptions);
|
| 86 |
+
|
| 87 |
+
// Initial delay
|
| 88 |
+
vi.advanceTimersByTime(30_000);
|
| 89 |
+
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
|
| 90 |
+
|
| 91 |
+
// 4 hours later
|
| 92 |
+
vi.advanceTimersByTime(4 * 60 * 60 * 1000);
|
| 93 |
+
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(2);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
it("transitions to checking state", () => {
|
| 97 |
+
initAutoUpdater(mockOptions);
|
| 98 |
+
|
| 99 |
+
mockAutoUpdater.emit("checking-for-update");
|
| 100 |
+
|
| 101 |
+
const state = getAutoUpdateState();
|
| 102 |
+
expect(state.checking).toBe(true);
|
| 103 |
+
expect(state.error).toBeNull();
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
it("transitions to update-available state", () => {
|
| 107 |
+
initAutoUpdater(mockOptions);
|
| 108 |
+
|
| 109 |
+
mockAutoUpdater.emit("update-available", { version: "2.0.0" });
|
| 110 |
+
|
| 111 |
+
const state = getAutoUpdateState();
|
| 112 |
+
expect(state.checking).toBe(false);
|
| 113 |
+
expect(state.updateAvailable).toBe(true);
|
| 114 |
+
expect(state.version).toBe("2.0.0");
|
| 115 |
+
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
it("transitions to update-not-available state", () => {
|
| 119 |
+
initAutoUpdater(mockOptions);
|
| 120 |
+
|
| 121 |
+
mockAutoUpdater.emit("checking-for-update");
|
| 122 |
+
mockAutoUpdater.emit("update-not-available");
|
| 123 |
+
|
| 124 |
+
const state = getAutoUpdateState();
|
| 125 |
+
expect(state.checking).toBe(false);
|
| 126 |
+
expect(state.updateAvailable).toBe(false);
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
it("tracks download progress", () => {
|
| 130 |
+
initAutoUpdater(mockOptions);
|
| 131 |
+
|
| 132 |
+
mockAutoUpdater.emit("download-progress", { percent: 50 });
|
| 133 |
+
|
| 134 |
+
const state = getAutoUpdateState();
|
| 135 |
+
expect(state.downloading).toBe(true);
|
| 136 |
+
expect(state.progress).toBe(50);
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
it("throttles tray rebuild to every 10% increment", () => {
|
| 140 |
+
initAutoUpdater(mockOptions);
|
| 141 |
+
mockOptions.rebuildTrayMenu.mockClear();
|
| 142 |
+
|
| 143 |
+
mockAutoUpdater.emit("download-progress", { percent: 5 });
|
| 144 |
+
expect(mockOptions.rebuildTrayMenu).not.toHaveBeenCalled();
|
| 145 |
+
|
| 146 |
+
mockAutoUpdater.emit("download-progress", { percent: 15 });
|
| 147 |
+
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalledTimes(1);
|
| 148 |
+
|
| 149 |
+
mockOptions.rebuildTrayMenu.mockClear();
|
| 150 |
+
mockAutoUpdater.emit("download-progress", { percent: 20 });
|
| 151 |
+
expect(mockOptions.rebuildTrayMenu).not.toHaveBeenCalled();
|
| 152 |
+
|
| 153 |
+
mockAutoUpdater.emit("download-progress", { percent: 100 });
|
| 154 |
+
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalledTimes(1);
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
it("transitions to downloaded state", () => {
|
| 158 |
+
initAutoUpdater(mockOptions);
|
| 159 |
+
|
| 160 |
+
mockAutoUpdater.emit("update-downloaded", { version: "2.0.0" });
|
| 161 |
+
|
| 162 |
+
const state = getAutoUpdateState();
|
| 163 |
+
expect(state.downloading).toBe(false);
|
| 164 |
+
expect(state.downloaded).toBe(true);
|
| 165 |
+
expect(state.progress).toBe(100);
|
| 166 |
+
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
it("handles errors gracefully", () => {
|
| 170 |
+
initAutoUpdater(mockOptions);
|
| 171 |
+
|
| 172 |
+
mockAutoUpdater.emit("error", new Error("Network timeout"));
|
| 173 |
+
|
| 174 |
+
const state = getAutoUpdateState();
|
| 175 |
+
expect(state.checking).toBe(false);
|
| 176 |
+
expect(state.downloading).toBe(false);
|
| 177 |
+
expect(state.error).toBe("Network timeout");
|
| 178 |
+
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
it("stopAutoUpdater clears all timers", () => {
|
| 182 |
+
initAutoUpdater(mockOptions);
|
| 183 |
+
|
| 184 |
+
stopAutoUpdater();
|
| 185 |
+
|
| 186 |
+
// Advance past both initial delay and periodic interval
|
| 187 |
+
vi.advanceTimersByTime(5 * 60 * 60 * 1000);
|
| 188 |
+
|
| 189 |
+
// checkForUpdates should not have been called (timers cleared)
|
| 190 |
+
expect(mockAutoUpdater.checkForUpdates).not.toHaveBeenCalled();
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
it("returns a copy of state (not reference)", () => {
|
| 194 |
+
const state1 = getAutoUpdateState();
|
| 195 |
+
const state2 = getAutoUpdateState();
|
| 196 |
+
|
| 197 |
+
expect(state1).not.toBe(state2);
|
| 198 |
+
expect(state1).toEqual(state2);
|
| 199 |
+
});
|
| 200 |
+
});
|
packages/electron/__tests__/build.test.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Smoke test for esbuild bundling.
|
| 3 |
+
*
|
| 4 |
+
* Verifies that electron/build.mjs produces valid output files
|
| 5 |
+
* with the expected exports.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { describe, it, expect, afterAll } from "vitest";
|
| 9 |
+
import { existsSync, rmSync, statSync } from "fs";
|
| 10 |
+
import { resolve } from "path";
|
| 11 |
+
import { execFileSync } from "child_process";
|
| 12 |
+
|
| 13 |
+
const PKG_DIR = resolve(import.meta.dirname, "..");
|
| 14 |
+
const DIST = resolve(PKG_DIR, "dist-electron");
|
| 15 |
+
|
| 16 |
+
describe("electron build (esbuild)", () => {
|
| 17 |
+
// Build once for all tests in this suite
|
| 18 |
+
const buildOnce = (() => {
|
| 19 |
+
let built = false;
|
| 20 |
+
return () => {
|
| 21 |
+
if (built) return;
|
| 22 |
+
execFileSync("node", ["electron/build.mjs"], {
|
| 23 |
+
cwd: PKG_DIR,
|
| 24 |
+
timeout: 30_000,
|
| 25 |
+
});
|
| 26 |
+
built = true;
|
| 27 |
+
};
|
| 28 |
+
})();
|
| 29 |
+
|
| 30 |
+
afterAll(() => {
|
| 31 |
+
// Clean up build output
|
| 32 |
+
if (existsSync(DIST)) {
|
| 33 |
+
rmSync(DIST, { recursive: true });
|
| 34 |
+
}
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
it("produces main.cjs (Electron main process)", () => {
|
| 38 |
+
buildOnce();
|
| 39 |
+
const mainCjs = resolve(DIST, "main.cjs");
|
| 40 |
+
expect(existsSync(mainCjs)).toBe(true);
|
| 41 |
+
expect(statSync(mainCjs).size).toBeGreaterThan(1000);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
it("produces server.mjs (backend server bundle)", () => {
|
| 45 |
+
buildOnce();
|
| 46 |
+
const serverMjs = resolve(DIST, "server.mjs");
|
| 47 |
+
expect(existsSync(serverMjs)).toBe(true);
|
| 48 |
+
expect(statSync(serverMjs).size).toBeGreaterThan(1000);
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
it("produces sourcemaps for both bundles", () => {
|
| 52 |
+
buildOnce();
|
| 53 |
+
expect(existsSync(resolve(DIST, "main.cjs.map"))).toBe(true);
|
| 54 |
+
expect(existsSync(resolve(DIST, "server.mjs.map"))).toBe(true);
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
it("server.mjs exports setPaths and startServer", async () => {
|
| 58 |
+
buildOnce();
|
| 59 |
+
const serverMjs = resolve(DIST, "server.mjs");
|
| 60 |
+
const mod = await import(serverMjs);
|
| 61 |
+
expect(typeof mod.setPaths).toBe("function");
|
| 62 |
+
expect(typeof mod.startServer).toBe("function");
|
| 63 |
+
});
|
| 64 |
+
});
|
packages/electron/__tests__/builder-config.test.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Validates electron-builder.yml configuration.
|
| 3 |
+
*
|
| 4 |
+
* Ensures all referenced files/directories exist and the config
|
| 5 |
+
* is structurally valid — catches the kind of path issues that
|
| 6 |
+
* have historically broken electron releases.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { describe, it, expect } from "vitest";
|
| 10 |
+
import { readFileSync, existsSync } from "fs";
|
| 11 |
+
import { resolve } from "path";
|
| 12 |
+
import yaml from "js-yaml";
|
| 13 |
+
|
| 14 |
+
const PKG_DIR = resolve(import.meta.dirname, "..");
|
| 15 |
+
const ROOT_DIR = resolve(PKG_DIR, "..", "..");
|
| 16 |
+
|
| 17 |
+
interface BuilderConfig {
|
| 18 |
+
appId: string;
|
| 19 |
+
productName: string;
|
| 20 |
+
electronVersion: string;
|
| 21 |
+
publish: { provider: string; owner: string; repo: string };
|
| 22 |
+
directories: { output: string };
|
| 23 |
+
files: Array<string | { from: string; to: string; filter?: string[] }>;
|
| 24 |
+
asarUnpack: string[];
|
| 25 |
+
extraResources: Array<{ from: string; to: string; filter?: string[] }>;
|
| 26 |
+
win: { target: Array<{ target: string; arch: string[] }>; icon: string };
|
| 27 |
+
mac: { target: Array<{ target: string; arch: string[] }>; icon: string };
|
| 28 |
+
linux: { target: Array<{ target: string; arch: string[] }>; icon: string };
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const config = yaml.load(
|
| 32 |
+
readFileSync(resolve(PKG_DIR, "electron-builder.yml"), "utf-8"),
|
| 33 |
+
) as BuilderConfig;
|
| 34 |
+
|
| 35 |
+
describe("electron-builder.yml", () => {
|
| 36 |
+
it("has valid YAML structure", () => {
|
| 37 |
+
expect(config.appId).toBe("com.codex-proxy.app");
|
| 38 |
+
expect(config.productName).toBe("Codex Proxy");
|
| 39 |
+
expect(config.electronVersion).toBeDefined();
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
it("has valid publish config", () => {
|
| 43 |
+
expect(config.publish.provider).toBe("github");
|
| 44 |
+
expect(config.publish.owner).toBeDefined();
|
| 45 |
+
expect(config.publish.repo).toBeDefined();
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
it("references existing icon file", () => {
|
| 49 |
+
const iconPath = resolve(PKG_DIR, config.mac.icon);
|
| 50 |
+
expect(existsSync(iconPath)).toBe(true);
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
it("icon file is referenced consistently across platforms", () => {
|
| 54 |
+
expect(config.win.icon).toBe(config.mac.icon);
|
| 55 |
+
expect(config.linux.icon).toBe(config.mac.icon);
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
it("files list includes dist-electron bundle", () => {
|
| 59 |
+
const hasDistElectron = config.files.some(
|
| 60 |
+
(f) => typeof f === "string" && f.includes("dist-electron"),
|
| 61 |
+
);
|
| 62 |
+
expect(hasDistElectron).toBe(true);
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
it("files list includes config, public, public-desktop globs", () => {
|
| 66 |
+
// After prepare-pack copies root dirs into packages/electron/,
|
| 67 |
+
// electron-builder picks them up via simple glob patterns
|
| 68 |
+
const globs = config.files.filter((f): f is string => typeof f === "string");
|
| 69 |
+
expect(globs).toContain("config/**/*");
|
| 70 |
+
expect(globs).toContain("public/**/*");
|
| 71 |
+
expect(globs).toContain("public-desktop/**/*");
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
it("root source directories for prepare-pack actually exist", () => {
|
| 75 |
+
// prepare-pack.mjs copies these from root before packing
|
| 76 |
+
const requiredDirs = ["config", "public", "bin"];
|
| 77 |
+
for (const dir of requiredDirs) {
|
| 78 |
+
const rootPath = resolve(ROOT_DIR, dir);
|
| 79 |
+
expect(
|
| 80 |
+
existsSync(rootPath),
|
| 81 |
+
`Root directory ${dir}/ should exist at ${rootPath}`,
|
| 82 |
+
).toBe(true);
|
| 83 |
+
}
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
it("extraResources bin/ maps to correct root directory", () => {
|
| 87 |
+
const binResource = config.extraResources.find(
|
| 88 |
+
(r) => r.to === "bin/" || r.to === "bin",
|
| 89 |
+
);
|
| 90 |
+
expect(binResource).toBeDefined();
|
| 91 |
+
// bin/ is copied from root by prepare-pack before packing
|
| 92 |
+
const rootBin = resolve(ROOT_DIR, "bin");
|
| 93 |
+
expect(
|
| 94 |
+
existsSync(rootBin),
|
| 95 |
+
`Root bin/ directory should exist at ${rootBin}`,
|
| 96 |
+
).toBe(true);
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
it("asarUnpack includes all runtime directories", () => {
|
| 100 |
+
const unpacked = config.asarUnpack;
|
| 101 |
+
expect(unpacked).toContain("config/**/*");
|
| 102 |
+
expect(unpacked).toContain("public/**/*");
|
| 103 |
+
expect(unpacked).toContain("public-desktop/**/*");
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
it("electronVersion matches installed version", () => {
|
| 107 |
+
const installedPkg = resolve(
|
| 108 |
+
ROOT_DIR,
|
| 109 |
+
"node_modules",
|
| 110 |
+
"electron",
|
| 111 |
+
"package.json",
|
| 112 |
+
);
|
| 113 |
+
if (existsSync(installedPkg)) {
|
| 114 |
+
const installed = JSON.parse(readFileSync(installedPkg, "utf-8")) as {
|
| 115 |
+
version: string;
|
| 116 |
+
};
|
| 117 |
+
expect(config.electronVersion).toBe(installed.version);
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
it("package.json has required fields for electron-builder", () => {
|
| 122 |
+
const pkg = JSON.parse(
|
| 123 |
+
readFileSync(resolve(PKG_DIR, "package.json"), "utf-8"),
|
| 124 |
+
) as Record<string, unknown>;
|
| 125 |
+
expect(pkg.name).toBeDefined();
|
| 126 |
+
expect(pkg.version).toBeDefined();
|
| 127 |
+
expect(pkg.description).toBeDefined();
|
| 128 |
+
expect(pkg.main).toBe("dist-electron/main.cjs");
|
| 129 |
+
});
|
| 130 |
+
});
|
packages/electron/__tests__/prepare-pack.test.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Tests for electron/prepare-pack.mjs
|
| 3 |
+
*
|
| 4 |
+
* Verifies that root-level resources (config/, public/, etc.) are correctly
|
| 5 |
+
* copied into packages/electron/ before electron-builder runs, and cleaned
|
| 6 |
+
* up afterward.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
| 10 |
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
|
| 11 |
+
import { resolve } from "path";
|
| 12 |
+
import { execFileSync } from "child_process";
|
| 13 |
+
|
| 14 |
+
const PKG_DIR = resolve(import.meta.dirname, "..");
|
| 15 |
+
const ROOT_DIR = resolve(PKG_DIR, "..", "..");
|
| 16 |
+
const SCRIPT = resolve(PKG_DIR, "electron", "prepare-pack.mjs");
|
| 17 |
+
|
| 18 |
+
// Directories that prepare-pack copies from root into packages/electron/
|
| 19 |
+
const DIRS = ["config", "public", "public-desktop", "bin"];
|
| 20 |
+
|
| 21 |
+
describe("prepare-pack.mjs", () => {
|
| 22 |
+
// Clean up any leftover copies before/after each test
|
| 23 |
+
function cleanCopies(): void {
|
| 24 |
+
for (const dir of DIRS) {
|
| 25 |
+
const dest = resolve(PKG_DIR, dir);
|
| 26 |
+
// Only remove if it's a copy (not the root original)
|
| 27 |
+
if (existsSync(dest) && resolve(dest) !== resolve(ROOT_DIR, dir)) {
|
| 28 |
+
rmSync(dest, { recursive: true });
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
beforeEach(cleanCopies);
|
| 34 |
+
afterEach(cleanCopies);
|
| 35 |
+
|
| 36 |
+
it("copies root directories into packages/electron/", () => {
|
| 37 |
+
execFileSync("node", [SCRIPT], { cwd: PKG_DIR });
|
| 38 |
+
|
| 39 |
+
for (const dir of DIRS) {
|
| 40 |
+
const rootDir = resolve(ROOT_DIR, dir);
|
| 41 |
+
const copyDir = resolve(PKG_DIR, dir);
|
| 42 |
+
if (existsSync(rootDir)) {
|
| 43 |
+
expect(existsSync(copyDir)).toBe(true);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
it("copies config/ with correct content", () => {
|
| 49 |
+
execFileSync("node", [SCRIPT], { cwd: PKG_DIR });
|
| 50 |
+
|
| 51 |
+
const rootConfig = resolve(ROOT_DIR, "config", "default.yaml");
|
| 52 |
+
const copyConfig = resolve(PKG_DIR, "config", "default.yaml");
|
| 53 |
+
|
| 54 |
+
if (existsSync(rootConfig)) {
|
| 55 |
+
expect(existsSync(copyConfig)).toBe(true);
|
| 56 |
+
expect(readFileSync(copyConfig, "utf-8")).toBe(
|
| 57 |
+
readFileSync(rootConfig, "utf-8"),
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
it("--clean removes copied directories", () => {
|
| 63 |
+
// First copy
|
| 64 |
+
execFileSync("node", [SCRIPT], { cwd: PKG_DIR });
|
| 65 |
+
|
| 66 |
+
// Verify at least config exists
|
| 67 |
+
const copyConfig = resolve(PKG_DIR, "config");
|
| 68 |
+
expect(existsSync(copyConfig)).toBe(true);
|
| 69 |
+
|
| 70 |
+
// Then clean
|
| 71 |
+
execFileSync("node", [SCRIPT, "--clean"], { cwd: PKG_DIR });
|
| 72 |
+
|
| 73 |
+
for (const dir of DIRS) {
|
| 74 |
+
expect(existsSync(resolve(PKG_DIR, dir))).toBe(false);
|
| 75 |
+
}
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
it("skips missing root directories without error", () => {
|
| 79 |
+
// Create a temp directory that doesn't have all root dirs
|
| 80 |
+
// The script should warn but not throw
|
| 81 |
+
const result = execFileSync("node", [SCRIPT], {
|
| 82 |
+
cwd: PKG_DIR,
|
| 83 |
+
encoding: "utf-8",
|
| 84 |
+
stdio: ["pipe", "pipe", "pipe"],
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
// Should succeed without throwing
|
| 88 |
+
expect(result).toBeDefined();
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
it("--clean is idempotent (no error when dirs already absent)", () => {
|
| 92 |
+
// Clean without prior copy — should not throw
|
| 93 |
+
expect(() => {
|
| 94 |
+
execFileSync("node", [SCRIPT, "--clean"], { cwd: PKG_DIR });
|
| 95 |
+
}).not.toThrow();
|
| 96 |
+
});
|
| 97 |
+
});
|
vitest.config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export default defineConfig({
|
|
| 16 |
"tests/unit/**/*.{test,spec}.ts",
|
| 17 |
"tests/integration/**/*.{test,spec}.ts",
|
| 18 |
"tests/e2e/**/*.{test,spec}.ts",
|
|
|
|
| 19 |
],
|
| 20 |
},
|
| 21 |
});
|
|
|
|
| 16 |
"tests/unit/**/*.{test,spec}.ts",
|
| 17 |
"tests/integration/**/*.{test,spec}.ts",
|
| 18 |
"tests/e2e/**/*.{test,spec}.ts",
|
| 19 |
+
"packages/electron/__tests__/**/*.{test,spec}.ts",
|
| 20 |
],
|
| 21 |
},
|
| 22 |
});
|