1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import { beforeEach, vi } from "vite-plus/test";
4
5const { autoUpdaterMock } = vi.hoisted(() => ({
6 autoUpdaterMock: {
7 allowDowngrade: false,
8 allowPrerelease: false,
9 autoDownload: true,
10 autoInstallOnAppQuit: true,
11 channel: "latest",
12 disableDifferentialDownload: false,
13 fullChangelog: false,
14 checkForUpdates: vi.fn(() => Promise.resolve(null)),
15 downloadUpdate: vi.fn(() => Promise.resolve([])),
16 on: vi.fn(),
17 quitAndInstall: vi.fn(),
18 removeListener: vi.fn(),
19 setFeedURL: vi.fn(),
20 },
21}));
22
23vi.mock("electron-updater", () => ({
24 autoUpdater: autoUpdaterMock,
25}));
26
27import * as ElectronUpdater from "./ElectronUpdater.ts";
28
29describe("ElectronUpdater", () => {
30 beforeEach(() => {
31 autoUpdaterMock.allowDowngrade = false;
32 autoUpdaterMock.allowPrerelease = false;
33 autoUpdaterMock.autoDownload = true;
34 autoUpdaterMock.autoInstallOnAppQuit = true;
35 autoUpdaterMock.channel = "latest";
36 autoUpdaterMock.disableDifferentialDownload = false;
37 autoUpdaterMock.fullChangelog = false;
38 autoUpdaterMock.checkForUpdates.mockClear();
39 autoUpdaterMock.checkForUpdates.mockImplementation(() => Promise.resolve(null));
40 autoUpdaterMock.downloadUpdate.mockClear();
41 autoUpdaterMock.downloadUpdate.mockImplementation(() => Promise.resolve([]));
42 autoUpdaterMock.on.mockClear();
43 autoUpdaterMock.quitAndInstall.mockClear();
44 autoUpdaterMock.removeListener.mockClear();
45 autoUpdaterMock.setFeedURL.mockClear();
46 });
47
48 it.effect("scopes updater event listeners", () =>
49 Effect.gen(function* () {
50 const listener = vi.fn();
51
52 yield* Effect.scoped(
53 Effect.gen(function* () {
54 const updater = yield* ElectronUpdater.ElectronUpdater;
55 yield* updater.on("update-available", listener);
56 }),
57 );
58
59 assert.deepEqual(autoUpdaterMock.on.mock.calls, [["update-available", listener]]);
60 assert.deepEqual(autoUpdaterMock.removeListener.mock.calls, [["update-available", listener]]);
61 }).pipe(Effect.provide(ElectronUpdater.layer)),
62 );
63
64 it.effect("wraps rejected update checks in the method-specific typed error", () =>
65 Effect.gen(function* () {
66 const cause = new Error("network unavailable");
67 autoUpdaterMock.checkForUpdates.mockImplementationOnce(() => Promise.reject(cause));
68 const updater = yield* ElectronUpdater.ElectronUpdater;
69 autoUpdaterMock.channel = "beta";
70
71 const error = yield* updater.checkForUpdates.pipe(Effect.flip);
72
73 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);
74 assert.equal(error.channel, "beta");
75 assert.strictEqual(error.cause, cause);
76 assert.equal(error.message, "Electron updater failed to check for updates on channel beta.");
77 assert.notInclude(error.message, cause.message);
78 }).pipe(Effect.provide(ElectronUpdater.layer)),
79 );
80
81 it.effect("preserves the execution-time channel on download failures", () =>
82 Effect.gen(function* () {
83 const cause = new Error("download unavailable");
84 autoUpdaterMock.downloadUpdate.mockImplementationOnce(() => Promise.reject(cause));
85 const updater = yield* ElectronUpdater.ElectronUpdater;
86 autoUpdaterMock.channel = "nightly";
87
88 const error = yield* updater.downloadUpdate.pipe(Effect.flip);
89
90 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterDownloadUpdateError);
91 assert.equal(error.channel, "nightly");
92 assert.strictEqual(error.cause, cause);
93 assert.equal(
94 error.message,
95 "Electron updater failed to download the update on channel nightly.",
96 );
97 assert.notInclude(error.message, cause.message);
98 }).pipe(Effect.provide(ElectronUpdater.layer)),
99 );
100
101 it.effect("sets full changelog mode", () =>
102 Effect.gen(function* () {
103 const updater = yield* ElectronUpdater.ElectronUpdater;
104
105 yield* updater.setFullChangelog(true);
106 assert.equal(autoUpdaterMock.fullChangelog, true);
107
108 yield* updater.setFullChangelog(false);
109 assert.equal(autoUpdaterMock.fullChangelog, false);
110 }).pipe(Effect.provide(ElectronUpdater.layer)),
111 );
112
113 it.effect("preserves quit-and-install flags and the execution-time channel", () =>
114 Effect.gen(function* () {
115 const cause = new Error("quit and install failed");
116 autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => {
117 throw cause;
118 });
119 const updater = yield* ElectronUpdater.ElectronUpdater;
120 autoUpdaterMock.channel = "alpha";
121
122 const error = yield* updater
123 .quitAndInstall({ isSilent: true, isForceRunAfter: false })
124 .pipe(Effect.flip);
125
126 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterQuitAndInstallError);
127 assert.equal(error.channel, "alpha");
128 assert.equal(error.isSilent, true);
129 assert.equal(error.isForceRunAfter, false);
130 assert.strictEqual(error.cause, cause);
131 assert.equal(
132 error.message,
133 "Electron updater failed to quit and install the update on channel alpha (silent: true, force run after: false).",
134 );
135 assert.notInclude(error.message, cause.message);
136 assert.deepEqual(autoUpdaterMock.quitAndInstall.mock.calls, [[true, false]]);
137 }).pipe(Effect.provide(ElectronUpdater.layer)),
138 );
139});