1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import { beforeEach, vi } from "vite-plus/test";
4
5const {
6 appendSwitchMock,
7 autoUpdaterOnMock,
8 autoUpdaterRemoveListenerMock,
9 exitMock,
10 getAppPathMock,
11 getSystemLocaleMock,
12 getVersionMock,
13 onMock,
14 quitMock,
15 relaunchMock,
16 removeListenerMock,
17 removeSwitchMock,
18 setAboutPanelOptionsMock,
19 setAppUserModelIdMock,
20 setAsDefaultProtocolClientMock,
21 setDesktopNameMock,
22 setDockIconMock,
23 setNameMock,
24 setPathMock,
25 whenReadyMock,
26} = vi.hoisted(() => ({
27 appendSwitchMock: vi.fn(),
28 autoUpdaterOnMock: vi.fn(),
29 autoUpdaterRemoveListenerMock: vi.fn(),
30 exitMock: vi.fn(),
31 getAppPathMock: vi.fn(() => "/app"),
32 getSystemLocaleMock: vi.fn(() => "en-GB"),
33 getVersionMock: vi.fn(() => "1.2.3"),
34 onMock: vi.fn(),
35 quitMock: vi.fn(),
36 relaunchMock: vi.fn(),
37 removeListenerMock: vi.fn(),
38 removeSwitchMock: vi.fn(),
39 setAboutPanelOptionsMock: vi.fn(),
40 setAppUserModelIdMock: vi.fn(),
41 setAsDefaultProtocolClientMock: vi.fn(() => true),
42 setDesktopNameMock: vi.fn(),
43 setDockIconMock: vi.fn(),
44 setNameMock: vi.fn(),
45 setPathMock: vi.fn(),
46 whenReadyMock: vi.fn(() => Promise.resolve()),
47}));
48
49vi.mock("electron", () => ({
50 autoUpdater: {
51 on: autoUpdaterOnMock,
52 removeListener: autoUpdaterRemoveListenerMock,
53 },
54 app: {
55 commandLine: {
56 appendSwitch: appendSwitchMock,
57 removeSwitch: removeSwitchMock,
58 },
59 dock: {
60 setIcon: setDockIconMock,
61 },
62 getAppPath: getAppPathMock,
63 getSystemLocale: getSystemLocaleMock,
64 getVersion: getVersionMock,
65 isPackaged: true,
66 name: "T3 Code",
67 on: onMock,
68 quit: quitMock,
69 relaunch: relaunchMock,
70 removeListener: removeListenerMock,
71 runningUnderARM64Translation: false,
72 setAboutPanelOptions: setAboutPanelOptionsMock,
73 setAsDefaultProtocolClient: setAsDefaultProtocolClientMock,
74 setAppUserModelId: setAppUserModelIdMock,
75 setDesktopName: setDesktopNameMock,
76 setName: setNameMock,
77 setPath: setPathMock,
78 whenReady: whenReadyMock,
79 exit: exitMock,
80 },
81}));
82
83import * as ElectronApp from "./ElectronApp.ts";
84
85describe("ElectronApp", () => {
86 beforeEach(() => {
87 vi.unstubAllEnvs();
88 appendSwitchMock.mockClear();
89 autoUpdaterOnMock.mockClear();
90 autoUpdaterRemoveListenerMock.mockClear();
91 exitMock.mockClear();
92 onMock.mockClear();
93 quitMock.mockClear();
94 relaunchMock.mockClear();
95 removeListenerMock.mockClear();
96 removeSwitchMock.mockClear();
97 setPathMock.mockClear();
98 });
99
100 it.effect("reads app metadata through the service", () =>
101 Effect.gen(function* () {
102 const electronApp = yield* ElectronApp.ElectronApp;
103 const metadata = yield* electronApp.metadata;
104
105 assert.deepEqual(metadata, {
106 appVersion: "1.2.3",
107 appPath: "/app",
108 isPackaged: true,
109 resourcesPath: process.resourcesPath,
110 runningUnderArm64Translation: false,
111 });
112 }).pipe(Effect.provide(ElectronApp.layer)),
113 );
114
115 it.effect("reports the selected payload version and path while retaining signed resources", () =>
116 Effect.gen(function* () {
117 vi.stubEnv(
118 "T3CODE_DESKTOP_PAYLOAD",
119 JSON.stringify({
120 version: "0.0.2000000000",
121 directory: "/staged/release",
122 shell: "a".repeat(64),
123 runtime: "b".repeat(64),
124 }),
125 );
126 const metadata = yield* (yield* ElectronApp.ElectronApp).metadata;
127 assert.equal(metadata.appVersion, "0.0.2000000000");
128 assert.equal(metadata.appPath, "/staged/release");
129 assert.equal(metadata.resourcesPath, process.resourcesPath);
130 vi.unstubAllEnvs();
131 }).pipe(Effect.provide(ElectronApp.layer)),
132 );
133
134 it.effect("reads the OS locale through the service", () =>
135 Effect.gen(function* () {
136 const electronApp = yield* ElectronApp.ElectronApp;
137
138 assert.strictEqual(yield* electronApp.systemLocale, "en-GB");
139 }).pipe(Effect.provide(ElectronApp.layer)),
140 );
141
142 it.effect("normalizes POSIX-style locale identifiers that Intl rejects", () =>
143 Effect.gen(function* () {
144 getSystemLocaleMock.mockImplementationOnce(() => "en_GB");
145 const electronApp = yield* ElectronApp.ElectronApp;
146
147 assert.strictEqual(yield* electronApp.systemLocale, "en-GB");
148 }).pipe(Effect.provide(ElectronApp.layer)),
149 );
150
151 it.effect("reports which app metadata property failed", () =>
152 Effect.gen(function* () {
153 const cause = new Error("version unavailable");
154 getVersionMock.mockImplementationOnce(() => {
155 throw cause;
156 });
157
158 const electronApp = yield* ElectronApp.ElectronApp;
159 const error = yield* electronApp.metadata.pipe(Effect.flip);
160
161 assert.instanceOf(error, ElectronApp.ElectronAppMetadataReadError);
162 assert.strictEqual(error.property, "app-version");
163 assert.strictEqual(error.cause, cause);
164 assert.strictEqual(
165 error.message,
166 'Failed to read Electron app metadata property "app-version".',
167 );
168 }).pipe(Effect.provide(ElectronApp.layer)),
169 );
170
171 it.effect("preserves Electron readiness failures", () =>
172 Effect.gen(function* () {
173 const cause = new Error("ready failed");
174 whenReadyMock.mockRejectedValueOnce(cause);
175
176 const electronApp = yield* ElectronApp.ElectronApp;
177 const error = yield* electronApp.whenReady.pipe(Effect.flip);
178
179 assert.instanceOf(error, ElectronApp.ElectronAppWhenReadyError);
180 assert.strictEqual(error.isPackaged, true);
181 assert.strictEqual(error.cause, cause);
182 assert.strictEqual(
183 error.message,
184 "Failed to wait for the Electron app to become ready (packaged: true).",
185 );
186 }).pipe(Effect.provide(ElectronApp.layer)),
187 );
188
189 it.effect("scopes app event listeners", () =>
190 Effect.gen(function* () {
191 const listener = vi.fn();
192
193 yield* Effect.scoped(
194 Effect.gen(function* () {
195 const electronApp = yield* ElectronApp.ElectronApp;
196 yield* electronApp.on("activate", listener);
197 }),
198 );
199
200 assert.deepEqual(onMock.mock.calls, [["activate", listener]]);
201 assert.deepEqual(removeListenerMock.mock.calls, [["activate", listener]]);
202 }).pipe(Effect.provide(ElectronApp.layer)),
203 );
204
205 it.effect("scopes native updater quit listeners", () =>
206 Effect.gen(function* () {
207 const listener = vi.fn();
208
209 yield* Effect.scoped(
210 Effect.gen(function* () {
211 const electronApp = yield* ElectronApp.ElectronApp;
212 yield* electronApp.onBeforeQuitForUpdate(listener);
213 }),
214 );
215
216 assert.deepEqual(autoUpdaterOnMock.mock.calls, [["before-quit-for-update", listener]]);
217 assert.deepEqual(autoUpdaterRemoveListenerMock.mock.calls, [
218 ["before-quit-for-update", listener],
219 ]);
220 }).pipe(Effect.provide(ElectronApp.layer)),
221 );
222
223 it.effect("removes command-line switches through the service", () =>
224 Effect.gen(function* () {
225 const electronApp = yield* ElectronApp.ElectronApp;
226 yield* electronApp.removeCommandLineSwitch("password-store");
227
228 assert.deepEqual(removeSwitchMock.mock.calls, [["password-store"]]);
229 }).pipe(Effect.provide(ElectronApp.layer)),
230 );
231});