Skip to content

Commit c7b375e

Browse files
[codex] Structure Electron window failures (#3276)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent abc253d commit c7b375e

2 files changed

Lines changed: 385 additions & 54 deletions

File tree

Lines changed: 194 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,118 @@
11
import { assert, describe, it } from "@effect/vitest";
2+
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
3+
import * as Cause from "effect/Cause";
24
import * as Effect from "effect/Effect";
5+
import * as Layer from "effect/Layer";
36
import type * as Electron from "electron";
47
import { beforeEach, vi } from "vite-plus/test";
58

6-
const { appFocusMock, getAllWindowsMock } = vi.hoisted(() => ({
7-
appFocusMock: vi.fn(),
8-
getAllWindowsMock: vi.fn(),
9-
}));
9+
const { appFocusMock, browserWindowMock, getAllWindowsMock, getFocusedWindowMock } = vi.hoisted(
10+
() => ({
11+
appFocusMock: vi.fn(),
12+
browserWindowMock: vi.fn(function BrowserWindowMock() {}),
13+
getAllWindowsMock: vi.fn(),
14+
getFocusedWindowMock: vi.fn(),
15+
}),
16+
);
1017

1118
vi.mock("electron", () => ({
1219
app: {
1320
focus: appFocusMock,
1421
},
15-
BrowserWindow: {
22+
BrowserWindow: Object.assign(browserWindowMock, {
1623
getAllWindows: getAllWindowsMock,
17-
},
24+
getFocusedWindow: getFocusedWindowMock,
25+
}),
1826
}));
1927

2028
import * as ElectronWindow from "./ElectronWindow.ts";
2129

22-
function makeBrowserWindow(input: { readonly destroyed: boolean }) {
30+
const TestLayer = ElectronWindow.layer.pipe(
31+
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
32+
);
33+
34+
function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) {
2335
return {
36+
id: input.id,
2437
isDestroyed: vi.fn(() => input.destroyed),
2538
} as unknown as Electron.BrowserWindow;
2639
}
2740

2841
describe("ElectronWindow", () => {
2942
beforeEach(() => {
3043
appFocusMock.mockReset();
44+
browserWindowMock.mockReset();
3145
getAllWindowsMock.mockReset();
46+
getFocusedWindowMock.mockReset();
3247
});
3348

49+
it.effect("preserves schema-safe creation context and the Electron cause", () =>
50+
Effect.gen(function* () {
51+
const cause = new Error("native BrowserWindow construction failed");
52+
browserWindowMock.mockImplementationOnce(function BrowserWindowFailure() {
53+
throw cause;
54+
});
55+
const options = {
56+
title: "T3 Code",
57+
width: 1100,
58+
height: 780,
59+
minWidth: 840,
60+
minHeight: 620,
61+
show: false,
62+
modal: false,
63+
frame: true,
64+
transparent: false,
65+
backgroundColor: "#101010",
66+
icon: {} as Electron.NativeImage,
67+
webPreferences: {
68+
preload: "/tmp/preload.js",
69+
partition: "persist:t3code-preview-test",
70+
sandbox: true,
71+
contextIsolation: true,
72+
nodeIntegration: false,
73+
webviewTag: true,
74+
spellcheck: true,
75+
},
76+
} satisfies Electron.BrowserWindowConstructorOptions;
77+
const electronWindow = yield* ElectronWindow.ElectronWindow;
78+
79+
const error = yield* electronWindow.create(options).pipe(Effect.flip);
80+
81+
assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError);
82+
assert.isTrue(ElectronWindow.isElectronWindowCreateError(error));
83+
assert.deepEqual(error.options, {
84+
title: "T3 Code",
85+
width: 1100,
86+
height: 780,
87+
minWidth: 840,
88+
minHeight: 620,
89+
show: false,
90+
modal: false,
91+
frame: true,
92+
transparent: false,
93+
backgroundColor: "#101010",
94+
webPreferences: {
95+
preload: "/tmp/preload.js",
96+
partition: "persist:t3code-preview-test",
97+
sandbox: true,
98+
contextIsolation: true,
99+
nodeIntegration: false,
100+
webviewTag: true,
101+
},
102+
});
103+
assert.isFalse("icon" in error.options);
104+
assert.isFalse("spellcheck" in error.options.webPreferences);
105+
assert.strictEqual(error.cause, cause);
106+
assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).');
107+
assert.notInclude(error.message, cause.message);
108+
assert.deepEqual(browserWindowMock.mock.calls, [[options]]);
109+
}).pipe(Effect.provide(TestLayer)),
110+
);
111+
34112
it.effect("skips windows destroyed before appearance sync runs", () =>
35113
Effect.gen(function* () {
36-
const liveWindow = makeBrowserWindow({ destroyed: false });
37-
const destroyedWindow = makeBrowserWindow({ destroyed: true });
114+
const liveWindow = makeBrowserWindow({ id: 1, destroyed: false });
115+
const destroyedWindow = makeBrowserWindow({ id: 2, destroyed: true });
38116
getAllWindowsMock.mockReturnValue([destroyedWindow, liveWindow]);
39117

40118
const syncedWindows: Electron.BrowserWindow[] = [];
@@ -46,6 +124,112 @@ describe("ElectronWindow", () => {
46124
);
47125

48126
assert.deepEqual(syncedWindows, [liveWindow]);
49-
}).pipe(Effect.provide(ElectronWindow.layer)),
127+
}).pipe(Effect.provide(TestLayer)),
128+
);
129+
130+
it.effect("preserves window enumeration failures as structured defects", () =>
131+
Effect.gen(function* () {
132+
const cause = new Error("window enumeration failed");
133+
getAllWindowsMock.mockImplementationOnce(() => {
134+
throw cause;
135+
});
136+
137+
const electronWindow = yield* ElectronWindow.ElectronWindow;
138+
const exit = yield* Effect.exit(electronWindow.currentMainOrFirst);
139+
140+
assert.equal(exit._tag, "Failure");
141+
if (exit._tag === "Failure") {
142+
const error = Cause.squash(exit.cause);
143+
assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError);
144+
assert.equal(error.operation, "list-windows");
145+
assert.equal(error.platform, "linux");
146+
assert.isNull(error.windowId);
147+
assert.isNull(error.channel);
148+
assert.strictEqual(error.cause, cause);
149+
assert.notInclude(error.message, cause.message);
150+
}
151+
}).pipe(Effect.provide(TestLayer)),
152+
);
153+
154+
it.effect("preserves reveal failures with the target window", () =>
155+
Effect.gen(function* () {
156+
const cause = new Error("window restore failed");
157+
const window = {
158+
id: 41,
159+
isDestroyed: vi.fn(() => false),
160+
isMinimized: vi.fn(() => true),
161+
restore: vi.fn(() => {
162+
throw cause;
163+
}),
164+
} as unknown as Electron.BrowserWindow;
165+
166+
const electronWindow = yield* ElectronWindow.ElectronWindow;
167+
const exit = yield* Effect.exit(electronWindow.reveal(window));
168+
169+
assert.equal(exit._tag, "Failure");
170+
if (exit._tag === "Failure") {
171+
const error = Cause.squash(exit.cause);
172+
assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError);
173+
assert.equal(error.operation, "reveal-window");
174+
assert.equal(error.windowId, 41);
175+
assert.isNull(error.channel);
176+
assert.strictEqual(error.cause, cause);
177+
}
178+
}).pipe(Effect.provide(TestLayer)),
179+
);
180+
181+
it.effect("preserves message delivery failures with window and channel context", () =>
182+
Effect.gen(function* () {
183+
const cause = new Error("renderer send failed");
184+
const window = {
185+
id: 42,
186+
isDestroyed: vi.fn(() => false),
187+
webContents: {
188+
send: vi.fn(() => {
189+
throw cause;
190+
}),
191+
},
192+
} as unknown as Electron.BrowserWindow;
193+
getAllWindowsMock.mockReturnValueOnce([window]);
194+
195+
const electronWindow = yield* ElectronWindow.ElectronWindow;
196+
const exit = yield* Effect.exit(electronWindow.sendAll("desktop:update", { ready: true }));
197+
198+
assert.equal(exit._tag, "Failure");
199+
if (exit._tag === "Failure") {
200+
const error = Cause.squash(exit.cause);
201+
assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError);
202+
assert.equal(error.operation, "send-window-message");
203+
assert.equal(error.windowId, 42);
204+
assert.equal(error.channel, "desktop:update");
205+
assert.strictEqual(error.cause, cause);
206+
}
207+
}).pipe(Effect.provide(TestLayer)),
208+
);
209+
210+
it.effect("preserves destroy failures with the target window", () =>
211+
Effect.gen(function* () {
212+
const cause = new Error("window destroy failed");
213+
const window = {
214+
id: 43,
215+
destroy: vi.fn(() => {
216+
throw cause;
217+
}),
218+
} as unknown as Electron.BrowserWindow;
219+
getAllWindowsMock.mockReturnValueOnce([window]);
220+
221+
const electronWindow = yield* ElectronWindow.ElectronWindow;
222+
const exit = yield* Effect.exit(electronWindow.destroyAll);
223+
224+
assert.equal(exit._tag, "Failure");
225+
if (exit._tag === "Failure") {
226+
const error = Cause.squash(exit.cause);
227+
assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError);
228+
assert.equal(error.operation, "destroy-window");
229+
assert.equal(error.windowId, 43);
230+
assert.isNull(error.channel);
231+
assert.strictEqual(error.cause, cause);
232+
}
233+
}).pipe(Effect.provide(TestLayer)),
50234
);
51235
});

0 commit comments

Comments
 (0)