feat(FN-5022): merge fusion/fn-5022

This commit is contained in:
gsxdsm
2026-05-18 05:46:37 -07:00
parent 54bb1651b7
commit dd0d4b74b7
15 changed files with 400 additions and 53 deletions

View File

@@ -319,6 +319,7 @@ describe("native integrations", () => {
await vi.waitFor(() => {
expect(mocks.autoUpdater.on).toHaveBeenCalledWith("update-available", expect.any(Function));
expect(mocks.autoUpdater.on).toHaveBeenCalledWith("update-downloaded", expect.any(Function));
expect(mocks.autoUpdater.on).toHaveBeenCalledWith("update-not-available", expect.any(Function));
expect(mocks.autoUpdater.on).toHaveBeenCalledWith("error", expect.any(Function));
expect(mocks.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
});
@@ -358,13 +359,33 @@ describe("native integrations", () => {
);
});
it("error handler does not crash", async () => {
it("update-not-available triggers notification and renderer IPC", async () => {
const { setupAutoUpdater } = await importNativeModule();
setupAutoUpdater(mocks.browserWindow as never);
await vi.dynamicImportSettled();
mocks.updaterHandlers.get("update-not-available")?.({ version: "1.2.0" });
const latestNotification = mocks.notificationInstances.at(-1);
expect(latestNotification?.options).toMatchObject({
title: "Fusion is up to date",
body: "You're on the latest version",
silent: true,
});
expect(mocks.browserWindow.webContents.send).toHaveBeenCalledWith(
"update-not-available",
expect.objectContaining({ version: "1.2.0" }),
);
});
it("error handler sends renderer IPC and does not crash", async () => {
const { setupAutoUpdater } = await importNativeModule();
setupAutoUpdater(mocks.browserWindow as never);
await vi.dynamicImportSettled();
expect(() => mocks.updaterHandlers.get("error")?.(new Error("network"))).not.toThrow();
expect(mocks.browserWindow.webContents.send).toHaveBeenCalledWith("update-error", { message: "network" });
});
it("catches checkForUpdates rejection", async () => {
@@ -375,6 +396,22 @@ describe("native integrations", () => {
await vi.dynamicImportSettled();
});
it("calling setupAutoUpdater twice does not rebind listeners or rerun initial check", async () => {
const { setupAutoUpdater } = await importNativeModule();
setupAutoUpdater(mocks.browserWindow as never);
await vi.dynamicImportSettled();
await vi.waitFor(() => {
expect(mocks.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
});
setupAutoUpdater(mocks.browserWindow as never);
await vi.dynamicImportSettled();
expect(mocks.autoUpdater.on).toHaveBeenCalledTimes(4);
expect(mocks.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
});
it("wraps setup in try/catch when updater throws during registration", async () => {
const { setupAutoUpdater } = await importNativeModule();
mocks.autoUpdater.on.mockImplementationOnce(() => {
@@ -386,6 +423,62 @@ describe("native integrations", () => {
});
});
describe("triggerUpdateCheck", () => {
it("returns checking when checkForUpdates succeeds", async () => {
const { triggerUpdateCheck } = await importNativeModule();
await expect(triggerUpdateCheck(mocks.browserWindow as never)).resolves.toEqual({ status: "checking" });
});
it("returns error when checkForUpdates rejects", async () => {
const { triggerUpdateCheck } = await importNativeModule();
mocks.autoUpdater.checkForUpdates.mockRejectedValue(new Error("network down"));
await expect(triggerUpdateCheck(mocks.browserWindow as never)).resolves.toEqual({
status: "error",
error: "network down",
});
});
it("returns unavailable when autoUpdater export is missing", async () => {
vi.resetModules();
vi.doMock("electron-updater", () => ({ default: {} }));
const { triggerUpdateCheck } = await importNativeModule();
await expect(triggerUpdateCheck(mocks.browserWindow as never)).resolves.toEqual({
status: "unavailable",
reason: "updater_unavailable",
});
vi.resetModules();
vi.doMock("electron-updater", () => ({
default: { autoUpdater: mocks.autoUpdater },
autoUpdater: mocks.autoUpdater,
}));
});
});
describe("startUpdateCheckInterval", () => {
it("schedules interval, triggers checks, and clears timer with stable disposer per window", async () => {
const { startUpdateCheckInterval } = await importNativeModule();
const disposerA = startUpdateCheckInterval(mocks.browserWindow as never, 1_000);
const disposerB = startUpdateCheckInterval(mocks.browserWindow as never, 1_000);
expect(disposerB).toBe(disposerA);
await vi.advanceTimersByTimeAsync(1_000);
await vi.dynamicImportSettled();
expect(mocks.autoUpdater.checkForUpdates).toHaveBeenCalled();
disposerA();
const callsAfterDispose = mocks.autoUpdater.checkForUpdates.mock.calls.length;
await vi.advanceTimersByTimeAsync(1_000);
expect(mocks.autoUpdater.checkForUpdates.mock.calls.length).toBe(callsAfterDispose);
});
});
describe("desktop launch mode", () => {
it("loadDesktopLaunchMode returns choose when file is missing", async () => {
const { loadDesktopLaunchMode } = await importNativeModule();