codex-proxy / packages /electron /__tests__ /auto-updater.test.ts
icebear0828
test: add electron package tests (33 cases)
325c8ea
raw
history blame
6.08 kB
/**
* Tests for electron/auto-updater.ts
*
* Mocks electron-updater and electron dialog to test state transitions
* without a real Electron runtime.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "events";
// ── Mocks ────────────────────────────────────────────────────────────
// Create a mock autoUpdater as an EventEmitter with methods
const mockAutoUpdater = Object.assign(new EventEmitter(), {
autoDownload: true,
autoInstallOnAppQuit: false,
allowPrerelease: false,
checkForUpdates: vi.fn().mockResolvedValue(undefined),
downloadUpdate: vi.fn().mockResolvedValue(undefined),
quitAndInstall: vi.fn(),
});
vi.mock("electron-updater", () => ({
autoUpdater: mockAutoUpdater,
}));
vi.mock("electron", () => ({
BrowserWindow: class {},
dialog: {
showMessageBox: vi.fn().mockResolvedValue({ response: 1 }), // "Later" by default
},
}));
// Import after mocks are set up
const { getAutoUpdateState, initAutoUpdater, stopAutoUpdater } = await import(
"../electron/auto-updater.js"
);
describe("auto-updater state machine", () => {
const mockOptions = {
getMainWindow: vi.fn().mockReturnValue(null),
rebuildTrayMenu: vi.fn(),
};
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
mockAutoUpdater.removeAllListeners();
});
afterEach(() => {
stopAutoUpdater();
vi.useRealTimers();
});
it("initial state is idle", () => {
const state = getAutoUpdateState();
expect(state.checking).toBe(false);
expect(state.updateAvailable).toBe(false);
expect(state.downloading).toBe(false);
expect(state.downloaded).toBe(false);
expect(state.version).toBeNull();
expect(state.error).toBeNull();
});
it("configures autoUpdater on init", () => {
initAutoUpdater(mockOptions);
expect(mockAutoUpdater.autoDownload).toBe(false);
expect(mockAutoUpdater.autoInstallOnAppQuit).toBe(true);
expect(mockAutoUpdater.allowPrerelease).toBe(false);
});
it("schedules initial check after 30s delay", () => {
initAutoUpdater(mockOptions);
expect(mockAutoUpdater.checkForUpdates).not.toHaveBeenCalled();
vi.advanceTimersByTime(30_000);
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
});
it("schedules periodic check every 4 hours", () => {
initAutoUpdater(mockOptions);
// Initial delay
vi.advanceTimersByTime(30_000);
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
// 4 hours later
vi.advanceTimersByTime(4 * 60 * 60 * 1000);
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalledTimes(2);
});
it("transitions to checking state", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("checking-for-update");
const state = getAutoUpdateState();
expect(state.checking).toBe(true);
expect(state.error).toBeNull();
});
it("transitions to update-available state", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("update-available", { version: "2.0.0" });
const state = getAutoUpdateState();
expect(state.checking).toBe(false);
expect(state.updateAvailable).toBe(true);
expect(state.version).toBe("2.0.0");
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
});
it("transitions to update-not-available state", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("checking-for-update");
mockAutoUpdater.emit("update-not-available");
const state = getAutoUpdateState();
expect(state.checking).toBe(false);
expect(state.updateAvailable).toBe(false);
});
it("tracks download progress", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("download-progress", { percent: 50 });
const state = getAutoUpdateState();
expect(state.downloading).toBe(true);
expect(state.progress).toBe(50);
});
it("throttles tray rebuild to every 10% increment", () => {
initAutoUpdater(mockOptions);
mockOptions.rebuildTrayMenu.mockClear();
mockAutoUpdater.emit("download-progress", { percent: 5 });
expect(mockOptions.rebuildTrayMenu).not.toHaveBeenCalled();
mockAutoUpdater.emit("download-progress", { percent: 15 });
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalledTimes(1);
mockOptions.rebuildTrayMenu.mockClear();
mockAutoUpdater.emit("download-progress", { percent: 20 });
expect(mockOptions.rebuildTrayMenu).not.toHaveBeenCalled();
mockAutoUpdater.emit("download-progress", { percent: 100 });
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalledTimes(1);
});
it("transitions to downloaded state", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("update-downloaded", { version: "2.0.0" });
const state = getAutoUpdateState();
expect(state.downloading).toBe(false);
expect(state.downloaded).toBe(true);
expect(state.progress).toBe(100);
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
});
it("handles errors gracefully", () => {
initAutoUpdater(mockOptions);
mockAutoUpdater.emit("error", new Error("Network timeout"));
const state = getAutoUpdateState();
expect(state.checking).toBe(false);
expect(state.downloading).toBe(false);
expect(state.error).toBe("Network timeout");
expect(mockOptions.rebuildTrayMenu).toHaveBeenCalled();
});
it("stopAutoUpdater clears all timers", () => {
initAutoUpdater(mockOptions);
stopAutoUpdater();
// Advance past both initial delay and periodic interval
vi.advanceTimersByTime(5 * 60 * 60 * 1000);
// checkForUpdates should not have been called (timers cleared)
expect(mockAutoUpdater.checkForUpdates).not.toHaveBeenCalled();
});
it("returns a copy of state (not reference)", () => {
const state1 = getAutoUpdateState();
const state2 = getAutoUpdateState();
expect(state1).not.toBe(state2);
expect(state1).toEqual(state2);
});
});