1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import * as Deferred from "effect/Deferred";
4import { DesktopSnapShotId } from "@t3tools/contracts";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as Layer from "effect/Layer";
8import * as Logger from "effect/Logger";
9import * as Option from "effect/Option";
10import * as Queue from "effect/Queue";
11import * as Ref from "effect/Ref";
12import * as References from "effect/References";
13import * as Schema from "effect/Schema";
14import * as TestClock from "effect/testing/TestClock";
15
16import * as Electron from "electron";
17import * as NodeEvents from "node:events";
18import { vi } from "vite-plus/test";
19
20vi.mock("electron", async (importOriginal) => ({
21 ...(await importOriginal<typeof import("electron")>()),
22 session: {
23 fromPartition: vi.fn(() => ({
24 getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/1.2.3"),
25 setPermissionRequestHandler: vi.fn(),
26 setUserAgent: vi.fn(),
27 })),
28 },
29 screen: {
30 getAllDisplays: vi.fn(() => [
31 {
32 bounds: { x: 0, y: 0, width: 1920, height: 1080 },
33 },
34 ]),
35 },
36}));
37
38import * as DesktopAssets from "../app/DesktopAssets.ts";
39import * as DesktopConfig from "../app/DesktopConfig.ts";
40import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
41import * as DesktopState from "../app/DesktopState.ts";
42import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
43import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";
44import * as ElectronApp from "../electron/ElectronApp.ts";
45import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
46import * as ElectronMenu from "../electron/ElectronMenu.ts";
47import * as ElectronShell from "../electron/ElectronShell.ts";
48import * as ElectronTheme from "../electron/ElectronTheme.ts";
49import * as ElectronWindow from "../electron/ElectronWindow.ts";
50import {
51 MENU_ACTION_CHANNEL,
52 SNAP_SHOT_EVENT_CHANNEL,
53 TRACKPAD_SCROLL_END_CHANNEL,
54 WINDOW_FULLSCREEN_STATE_CHANNEL,
55} from "../ipc/channels.ts";
56import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
57import * as DesktopWindow from "./DesktopWindow.ts";
58import * as PreviewManager from "../preview/Manager.ts";
59import * as PreviewPasskeys from "../preview/Passkeys.ts";
60
61const environmentInput = {
62 dirname: "/repo/apps/desktop/dist-electron",
63 homeDirectory: "/Users/alice",
64 platform: "darwin",
65 processArch: "arm64",
66 appVersion: "1.2.3",
67 appPath: "/repo",
68 isPackaged: false,
69 resourcesPath: "/repo/resources",
70 runningUnderArm64Translation: false,
71} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
72
73function makeFakeBrowserWindow() {
74 const windowListeners = new Map<string, (...args: readonly unknown[]) => void>();
75 const webContentsListeners = new Map<string, (...args: readonly unknown[]) => void>();
76 let zoomLevel = 0;
77 const webContents = {
78 copyImageAt: vi.fn(),
79 focus: vi.fn(),
80 isDestroyed: vi.fn(() => false),
81 getURL: vi.fn(() => "t3code-dev://app/"),
82 getZoomLevel: vi.fn(() => zoomLevel),
83 getZoomFactor: vi.fn(() => 1.2 ** zoomLevel),
84 setZoomLevel: vi.fn((level: number) => {
85 zoomLevel = level;
86 }),
87 isLoadingMainFrame: vi.fn(() => false),
88 on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {
89 webContentsListeners.set(eventName, listener);
90 }),
91 once: vi.fn<(eventName: string, listener: (...args: readonly unknown[]) => void) => void>(),
92 openDevTools: vi.fn(),
93 reload: vi.fn(),
94 reloadIgnoringCache: vi.fn(),
95 replaceMisspelling: vi.fn(),
96 send: vi.fn(),
97 setBackgroundThrottling: vi.fn(),
98 setWindowOpenHandler: vi.fn(),
99 toggleDevTools: vi.fn(),
100 };
101
102 const window = {
103 close: vi.fn(),
104 focus: vi.fn(),
105 getBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })),
106 getNormalBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })),
107 isDestroyed: vi.fn(() => false),
108 isFullScreen: vi.fn(() => false),
109 isMaximized: vi.fn(() => false),
110 isMinimized: vi.fn(() => false),
111 isVisible: vi.fn(() => true),
112 loadURL: vi.fn(() => Promise.resolve()),
113 maximize: vi.fn(),
114 on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {
115 windowListeners.set(eventName, listener);
116 }),
117 once: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {
118 windowListeners.set(eventName, listener);
119 }),
120 restore: vi.fn(),
121 setBackgroundColor: vi.fn(),
122 setAutoHideCursor: vi.fn(),
123 setFullScreen: vi.fn(),
124 setOpacity: vi.fn(),
125 setTitle: vi.fn(),
126 setTitleBarOverlay: vi.fn(),
127 setWindowButtonPosition: vi.fn(),
128 show: vi.fn(),
129 webContents,
130 };
131
132 return {
133 window: window as unknown as Electron.BrowserWindow,
134 getBounds: window.getBounds,
135 getNormalBounds: window.getNormalBounds,
136 isDestroyed: window.isDestroyed,
137 isFullScreen: window.isFullScreen,
138 isMaximized: window.isMaximized,
139 isMinimized: window.isMinimized,
140 loadURL: window.loadURL,
141 maximize: window.maximize,
142 openDevTools: webContents.openDevTools,
143 reload: webContents.reload,
144 reloadIgnoringCache: webContents.reloadIgnoringCache,
145 toggleDevTools: webContents.toggleDevTools,
146 send: webContents.send,
147 setZoomLevel: webContents.setZoomLevel,
148 setWindowButtonPosition: window.setWindowButtonPosition,
149 setBackgroundThrottling: webContents.setBackgroundThrottling,
150 setAutoHideCursor: window.setAutoHideCursor,
151 setFullScreen: window.setFullScreen,
152 setOpacity: window.setOpacity,
153 webContentsListeners,
154 webContentsOnce: webContents.once,
155 windowListeners,
156 };
157}
158
159const layerDesktopClientSettings = Layer.mock(DesktopClientSettings.DesktopClientSettings)({
160 get: Effect.succeedNone,
161});
162
163const layerElectronApp = Layer.mock(ElectronApp.ElectronApp)({
164 quit: Effect.void,
165});
166
167const layerDesktopAssets = Layer.succeed(DesktopAssets.DesktopAssets, {
168 iconPaths: Effect.succeed({
169 ico: Option.none<string>(),
170 icns: Option.none<string>(),
171 png: Option.none<string>(),
172 }),
173 resolveResourcePath: () => Effect.succeed(Option.none<string>()),
174} satisfies DesktopAssets.DesktopAssets["Service"]);
175
176const layerDesktopServerExposure = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
177 getState: Effect.die("unexpected getState"),
178 backendConfig: Effect.succeed({
179 port: 3773,
180 bindHost: "127.0.0.1",
181 httpBaseUrl: new URL("http://127.0.0.1:3773"),
182 tailscaleServeEnabled: false,
183 tailscaleServePort: 443,
184 }),
185 configureFromSettings: () => Effect.die("unexpected configureFromSettings"),
186 setMode: () => Effect.die("unexpected setMode"),
187 setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),
188 getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),
189} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
190
191const layerElectronMenu = Layer.succeed(ElectronMenu.ElectronMenu, {
192 setApplicationMenu: () => Effect.void,
193 popupTemplate: () => Effect.void,
194 showContextMenu: () => Effect.succeedNone,
195} satisfies ElectronMenu.ElectronMenu["Service"]);
196
197const layerElectronTheme = Layer.succeed(ElectronTheme.ElectronTheme, {
198 shouldUseDarkColors: Effect.succeed(false),
199 setSource: () => Effect.void,
200 onUpdated: () => Effect.void,
201} satisfies ElectronTheme.ElectronTheme["Service"]);
202
203const layerDesktopEnvironment = DesktopEnvironment.layer(environmentInput).pipe(
204 Layer.provide(
205 Layer.mergeAll(
206 NodeServices.layer,
207 DesktopConfig.layerTest({
208 T3CODE_PORT: "3773",
209 VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
210 }),
211 ),
212 ),
213);
214
215const desktopWindowBoundsEquivalence = Schema.toEquivalence(
216 DesktopAppSettings.DesktopWindowBoundsSchema,
217);
218
219function layerTest(input: {
220 readonly window: Electron.BrowserWindow;
221 readonly createCount: Ref.Ref<number>;
222 readonly mainWindow: Ref.Ref<Option.Option<Electron.BrowserWindow>>;
223 readonly createdWindowOptions?: Electron.BrowserWindowConstructorOptions[];
224 readonly desktopSettings?: DesktopAppSettings.DesktopSettings;
225 readonly mainWindowBoundsUpdates?: DesktopAppSettings.DesktopWindowBounds[];
226 readonly mainWindowMaximizedUpdates?: boolean[];
227 readonly beforeMainWindowBoundsUpdate?: (
228 bounds: DesktopAppSettings.DesktopWindowBounds,
229 ) => Effect.Effect<void>;
230 readonly openedExternalUrls?: unknown[];
231 readonly copiedTexts?: string[];
232 readonly onPopupTemplate?: (input: ElectronMenu.ElectronMenuTemplateInput) => Effect.Effect<void>;
233 readonly previewZoomReapplies?: number[];
234 readonly onReveal?: (window: Electron.BrowserWindow) => void;
235 readonly focusedWindow?: Electron.BrowserWindow;
236}) {
237 let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;
238 const layerDesktopAppSettings = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
239 get: Effect.sync(() => desktopSettings),
240 load: Effect.sync(() => desktopSettings),
241 setMainWindowBounds: (bounds, isMaximized) =>
242 Effect.gen(function* () {
243 if (input.beforeMainWindowBoundsUpdate) {
244 yield* input.beforeMainWindowBoundsUpdate(bounds);
245 }
246 const changed =
247 desktopSettings.mainWindowBounds === null ||
248 !desktopWindowBoundsEquivalence(desktopSettings.mainWindowBounds, bounds) ||
249 desktopSettings.mainWindowMaximized !== isMaximized;
250 if (changed) {
251 desktopSettings = {
252 ...desktopSettings,
253 mainWindowBounds: bounds,
254 mainWindowMaximized: isMaximized,
255 };
256 input.mainWindowBoundsUpdates?.push(bounds);
257 input.mainWindowMaximizedUpdates?.push(isMaximized);
258 }
259 return { settings: desktopSettings, changed };
260 }),
261 setServerExposureMode: () => Effect.die("unexpected server exposure update"),
262 setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),
263 setUpdateChannel: () => Effect.die("unexpected update channel change"),
264 setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),
265 setWslDistro: () => Effect.die("unexpected WSL distro change"),
266 setWslOnly: () => Effect.die("unexpected WSL-only toggle"),
267 setLocalEnvironmentEnabled: () => Effect.die("unexpected local environment toggle"),
268 applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"),
269 applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),
270 } satisfies DesktopAppSettings.DesktopAppSettings["Service"]);
271
272 const layerElectronWindow = Layer.succeed(ElectronWindow.ElectronWindow, {
273 create: (options) =>
274 Effect.sync(() => {
275 input.createdWindowOptions?.push(options);
276 }).pipe(
277 Effect.andThen(Ref.update(input.createCount, (count) => count + 1)),
278 Effect.as(input.window),
279 ),
280 main: Ref.get(input.mainWindow),
281 currentMainOrFirst: Ref.get(input.mainWindow),
282 focusedMainOrFirst:
283 input.focusedWindow === undefined
284 ? Ref.get(input.mainWindow)
285 : Effect.succeedSome(input.focusedWindow),
286 setMain: (window) => Ref.set(input.mainWindow, Option.some(window)),
287 clearMain: () => Ref.set(input.mainWindow, Option.none()),
288 prepareReveal: () => Effect.succeed(false),
289 reveal: (window) => Effect.sync(() => input.onReveal?.(window)),
290 sendAll: () => Effect.void,
291 destroyAll: Effect.void,
292 syncAllAppearance: (sync) => sync(input.window),
293 } satisfies ElectronWindow.ElectronWindow["Service"]);
294
295 return DesktopWindow.layer.pipe(
296 Layer.provide(
297 Layer.mergeAll(
298 layerDesktopAssets,
299 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
300 register: () => Effect.void,
301 recordMetrics: () => Effect.void,
302 shutdown: Effect.void,
303 }),
304 layerDesktopEnvironment,
305 layerDesktopAppSettings,
306 layerDesktopClientSettings,
307 layerDesktopServerExposure,
308 DesktopState.layer,
309 layerElectronApp,
310 Layer.succeed(ElectronMenu.ElectronMenu, {
311 setApplicationMenu: () => Effect.void,
312 showContextMenu: () => Effect.succeedNone,
313 popupTemplate: input.onPopupTemplate ?? (() => Effect.void),
314 }),
315 Layer.succeed(ElectronShell.ElectronShell, {
316 openExternal: (url) =>
317 Effect.sync(() => {
318 input.openedExternalUrls?.push(url);
319 return true;
320 }),
321 openSystemSettings: () => Effect.succeed(true),
322 copyText: (text) =>
323 Effect.sync(() => {
324 input.copiedTexts?.push(text);
325 }),
326 } satisfies ElectronShell.ElectronShell["Service"]),
327 layerElectronTheme,
328 layerElectronWindow,
329 Layer.mock(PreviewPasskeys.PreviewPasskeys)({
330 bridgeEnabled: false,
331 installSessionHandlers: () => {},
332 attachGuest: () => () => {},
333 }),
334 Layer.mock(PreviewManager.PreviewManager)({
335 getBrowserSession: () => Effect.succeed({} as Electron.Session),
336 setMainWindow: () => Effect.void,
337 prepareWebview: () => Effect.void,
338 isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"),
339 getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"),
340 reapplyZoom: () =>
341 Effect.sync(() => {
342 input.previewZoomReapplies?.push(input.window.webContents.getZoomLevel());
343 }),
344 }),
345 ),
346 ),
347 );
348}
349
350// Builds a DesktopWindow over a fake ElectronWindow whose `create` returns the
351// given outcomes in order (null => simulated open failure), and whose
352// currentMainOrFirst mirrors the real fallback to the first live window (the
353// splash, before any main is registered). Reveal targets are recorded so tests
354// can assert what activation actually surfaced.
355const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | null)[]) =>
356 Effect.gen(function* () {
357 const createdWindows = yield* Ref.make<Electron.BrowserWindow[]>([]);
358 const createCalls = yield* Ref.make(0);
359 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
360 const revealedWindows = yield* Ref.make<Electron.BrowserWindow[]>([]);
361 const fallbackWindow = createOutcomes.find(
362 (window): window is Electron.BrowserWindow => window !== null,
363 );
364
365 const currentMainOrFirst = Effect.gen(function* () {
366 const registered = yield* Ref.get(mainWindow);
367 if (Option.isSome(registered)) {
368 return registered;
369 }
370 const created = yield* Ref.get(createdWindows);
371 return Option.fromNullishOr(created[0] ?? null);
372 });
373
374 const electronWindowShape = {
375 create: () =>
376 Effect.gen(function* () {
377 const index = yield* Ref.getAndUpdate(createCalls, (count) => count + 1);
378 const outcome = createOutcomes[index] ?? null;
379 if (outcome === null) {
380 return yield* new ElectronWindow.ElectronWindowCreateError({
381 options: {
382 title: null,
383 width: null,
384 height: null,
385 minWidth: null,
386 minHeight: null,
387 show: null,
388 modal: null,
389 frame: null,
390 transparent: null,
391 backgroundColor: null,
392 webPreferences: {
393 preload: null,
394 partition: null,
395 backgroundThrottling: null,
396 sandbox: null,
397 contextIsolation: null,
398 nodeIntegration: null,
399 webviewTag: null,
400 },
401 },
402 cause: new Error("simulated window-open failure"),
403 });
404 }
405 yield* Ref.update(createdWindows, (windows) => [...windows, outcome]);
406 return outcome;
407 }),
408 main: Ref.get(mainWindow),
409 currentMainOrFirst,
410 focusedMainOrFirst: currentMainOrFirst,
411 setMain: (window) => Ref.set(mainWindow, Option.some(window)),
412 clearMain: () => Ref.set(mainWindow, Option.none()),
413 prepareReveal: () => Effect.succeed(false),
414 reveal: (window) => Ref.update(revealedWindows, (windows) => [...windows, window]),
415 sendAll: () => Effect.void,
416 destroyAll: Effect.void,
417 syncAllAppearance: (sync) => (fallbackWindow ? sync(fallbackWindow) : Effect.void),
418 } satisfies ElectronWindow.ElectronWindow["Service"];
419
420 const layer = DesktopWindow.layer.pipe(
421 Layer.provide(
422 Layer.mergeAll(
423 layerDesktopAssets,
424 Layer.succeed(DesktopRendererHistory.DesktopRendererHistory, {
425 register: () => Effect.void,
426 recordMetrics: () => Effect.void,
427 shutdown: Effect.void,
428 }),
429 layerDesktopEnvironment,
430 DesktopAppSettings.layerTest(),
431 layerDesktopClientSettings,
432 layerDesktopServerExposure,
433 layerElectronApp,
434 layerElectronMenu,
435 Layer.succeed(ElectronShell.ElectronShell, {
436 openExternal: () => Effect.succeed(true),
437 openSystemSettings: () => Effect.succeed(true),
438 copyText: () => Effect.void,
439 } satisfies ElectronShell.ElectronShell["Service"]),
440 layerElectronTheme,
441 Layer.succeed(ElectronWindow.ElectronWindow, electronWindowShape),
442 Layer.mock(PreviewPasskeys.PreviewPasskeys)({
443 bridgeEnabled: false,
444 installSessionHandlers: () => {},
445 attachGuest: () => () => {},
446 }),
447 Layer.mock(PreviewManager.PreviewManager)({
448 getBrowserSession: () => Effect.succeed({} as Electron.Session),
449 setMainWindow: () => Effect.void,
450 isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"),
451 getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"),
452 }),
453 ),
454 ),
455 );
456
457 return { layer, createCalls, mainWindow, revealedWindows } as const;
458 });
459
460const captureOne = DesktopSnapShotId.make("11111111-1111-4111-8111-111111111111");
461const captureTwo = DesktopSnapShotId.make("22222222-2222-4222-8222-222222222222");
462
463describe("DesktopWindow", () => {
464 it.effect("shows native context menus for browser guests and sign-in popups", () =>
465 Effect.gen(function* () {
466 const host = makeFakeBrowserWindow();
467 const popup = makeFakeBrowserWindow();
468 let focusedContents: unknown = host.window.webContents;
469 const makeContents = () => {
470 const contents = Object.assign(new NodeEvents.EventEmitter(), {
471 mainFrame: {
472 routingId: 7,
473 isDestroyed: vi.fn(() => false),
474 } as unknown as Electron.WebFrameMain,
475 isDestroyed: vi.fn(() => false),
476 focus: vi.fn(() => {
477 focusedContents = contents;
478 }),
479 copyImageAt: vi.fn(),
480 replaceMisspelling: vi.fn(),
481 });
482 return contents;
483 };
484 const guest = makeContents();
485 const popupContents = makeContents();
486 const popupWindow = { ...popup.window, webContents: popupContents };
487 const menus = yield* Queue.unbounded<{
488 input: ElectronMenu.ElectronMenuTemplateInput;
489 focusedContents: unknown;
490 }>();
491 const copiedTexts: string[] = [];
492 const layer = layerTest({
493 window: host.window,
494 createCount: yield* Ref.make(0),
495 mainWindow: yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()),
496 copiedTexts,
497 onPopupTemplate: (input) =>
498 Queue.offer(menus, { input, focusedContents }).pipe(Effect.asVoid),
499 });
500
501 yield* Effect.gen(function* () {
502 const desktopWindow = yield* DesktopWindow.DesktopWindow;
503 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
504 const attach = host.webContentsListeners.get("did-attach-webview");
505 assert.isDefined(attach);
506 attach({}, guest);
507 attach({}, guest);
508 guest.emit("did-create-window", popupWindow);
509 guest.emit("did-create-window", popupWindow);
510
511 for (const [contents, owner] of [
512 [guest, host.window],
513 [popupContents, popupWindow],
514 ] as const) {
515 const frame = { routingId: 7 } as Electron.WebFrameMain;
516 const preventDefault = vi.fn();
517 const params = {
518 frame,
519 x: 12,
520 y: 34,
521 misspelledWord: "helo",
522 dictionarySuggestions: ["hello"],
523 linkURL: "",
524 mediaType: "none",
525 editFlags: { canCut: false, canCopy: true, canPaste: true, canSelectAll: true },
526 };
527 focusedContents = host.window.webContents;
528 contents.emit("context-menu", { preventDefault }, params);
529 const menu = yield* Queue.take(menus);
530 assert.strictEqual(menu.input.window, owner);
531 assert.strictEqual(menu.input.frame, frame);
532 assert.strictEqual(menu.focusedContents, contents);
533 assert.equal(preventDefault.mock.calls.length, 1);
534 assert.deepEqual(
535 menu.input.template.filter((item) => item.role),
536 [
537 { role: "cut", enabled: false },
538 { role: "copy", enabled: true },
539 { role: "paste", enabled: true },
540 { role: "selectAll", enabled: true },
541 ],
542 );
543 const correction = menu.input.template.find((item) => item.label === "hello");
544 assert.isDefined(correction?.click);
545 correction.click({} as Electron.MenuItem, undefined, {} as Electron.KeyboardEvent);
546 assert.deepEqual(contents.replaceMisspelling.mock.calls, [["hello"]]);
547
548 contents.emit(
549 "context-menu",
550 { preventDefault },
551 {
552 ...params,
553 frame: contents.mainFrame,
554 misspelledWord: "",
555 dictionarySuggestions: [],
556 mediaType: "image",
557 linkURL: "https://example.com/image.png",
558 },
559 );
560 const imageMenu = (yield* Queue.take(menus)).input;
561 assert.strictEqual(imageMenu.frame, contents.mainFrame);
562 const copyImage = imageMenu.template.find((item) => item.label === "Copy Image");
563 const copyLink = imageMenu.template.find((item) => item.label === "Copy Link");
564 assert.isDefined(copyImage?.click);
565 assert.isDefined(copyLink?.click);
566 copyImage.click({} as Electron.MenuItem, undefined, {} as Electron.KeyboardEvent);
567 copyLink.click({} as Electron.MenuItem, undefined, {} as Electron.KeyboardEvent);
568 assert.deepEqual(contents.copyImageAt.mock.calls, [[12, 34]]);
569 assert.equal(copiedTexts.at(-1), "https://example.com/image.png");
570
571 contents.isDestroyed.mockReturnValue(true);
572 correction.click({} as Electron.MenuItem, undefined, {} as Electron.KeyboardEvent);
573 copyImage.click({} as Electron.MenuItem, undefined, {} as Electron.KeyboardEvent);
574 assert.equal(contents.replaceMisspelling.mock.calls.length, 1);
575 assert.equal(contents.copyImageAt.mock.calls.length, 1);
576 assert.equal(yield* Queue.size(menus), 0);
577 }
578 }).pipe(Effect.provide(layer));
579 }),
580 );
581
582 it("leaves fullscreen before concealing a pending quit", () => {
583 const fakeWindow = makeFakeBrowserWindow();
584
585 DesktopWindow.concealPendingQuitWindow(fakeWindow.window);
586 assert.deepEqual(fakeWindow.setOpacity.mock.calls, [[0]]);
587
588 fakeWindow.setOpacity.mockClear();
589 fakeWindow.isFullScreen.mockReturnValue(true);
590 DesktopWindow.concealPendingQuitWindow(fakeWindow.window);
591 assert.deepEqual(fakeWindow.setFullScreen.mock.calls, [[false]]);
592 assert.deepEqual(fakeWindow.setOpacity.mock.calls, [[0]]);
593
594 fakeWindow.setOpacity.mockClear();
595 fakeWindow.isFullScreen.mockReturnValue(false);
596 fakeWindow.isDestroyed.mockReturnValue(true);
597 DesktopWindow.concealPendingQuitWindow(fakeWindow.window);
598 assert.equal(fakeWindow.setOpacity.mock.calls.length, 0);
599 });
600
601 it("restores bounds only when the window fits within a connected display", () => {
602 const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 };
603 const displays = [
604 { x: 0, y: 0, width: 1920, height: 1080 },
605 { x: 1920, y: 0, width: 2560, height: 1440 },
606 ];
607
608 assert.deepEqual(
609 DesktopWindow.resolveInitialMainWindowBounds(persistedBounds, displays),
610 persistedBounds,
611 );
612 assert.deepEqual(
613 DesktopWindow.resolveInitialMainWindowBounds(persistedBounds, [displays[0]!]),
614 DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE,
615 );
616 });
617
618 it("recognizes only same-origin renderer navigations", () => {
619 assert.isTrue(
620 DesktopWindow.isSameOriginRendererNavigation({
621 applicationUrl: "t3code://app/",
622 navigationUrl: "t3code://app/settings/connections",
623 }),
624 );
625 assert.isFalse(
626 DesktopWindow.isSameOriginRendererNavigation({
627 applicationUrl: "t3code://app/",
628 navigationUrl: "https://accounts.microsoft.com/oauth",
629 }),
630 );
631 assert.isFalse(
632 DesktopWindow.isSameOriginRendererNavigation({
633 applicationUrl: "t3code://app/",
634 navigationUrl: "not a url",
635 }),
636 );
637 });
638
639 it.effect("does not open a development window until the backend is ready", () =>
640 Effect.gen(function* () {
641 const fakeWindow = makeFakeBrowserWindow();
642 const createCount = yield* Ref.make(0);
643 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
644 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
645 const layer = layerTest({
646 window: fakeWindow.window,
647 createCount,
648 mainWindow,
649 createdWindowOptions,
650 });
651
652 yield* Effect.gen(function* () {
653 const desktopWindow = yield* DesktopWindow.DesktopWindow;
654 yield* desktopWindow.activate;
655 assert.equal(yield* Ref.get(createCount), 0);
656
657 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
658 assert.equal(yield* Ref.get(createCount), 1);
659 assert.equal(createdWindowOptions[0]?.width, 1100);
660 assert.equal(createdWindowOptions[0]?.height, 780);
661 assert.isUndefined(createdWindowOptions[0]?.x);
662 assert.isUndefined(createdWindowOptions[0]?.y);
663 assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor);
664 assert.isFalse(createdWindowOptions[0]?.webPreferences?.backgroundThrottling);
665 assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]);
666 assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]);
667 assert.equal(fakeWindow.openDevTools.mock.calls.length, 1);
668 }).pipe(Effect.provide(layer));
669 }),
670 );
671
672 it.effect(
673 "opens and reopens the window without backend readiness when local execution is disabled",
674 () =>
675 Effect.gen(function* () {
676 const fakeWindow = makeFakeBrowserWindow();
677 const createCount = yield* Ref.make(0);
678 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
679 const layer = layerTest({
680 window: fakeWindow.window,
681 createCount,
682 mainWindow,
683 createdWindowOptions: [],
684 desktopSettings: {
685 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
686 localEnvironmentEnabled: false,
687 },
688 });
689 yield* Effect.gen(function* () {
690 const desktopWindow = yield* DesktopWindow.DesktopWindow;
691 yield* desktopWindow.createMainIfBackendReady;
692 assert.equal(yield* Ref.get(createCount), 1);
693 yield* Ref.set(mainWindow, Option.none());
694 yield* desktopWindow.activate;
695 assert.equal(yield* Ref.get(createCount), 2);
696 yield* Ref.set(mainWindow, Option.none());
697 yield* desktopWindow.dispatchMenuAction("new-thread");
698 assert.equal(yield* Ref.get(createCount), 3);
699 }).pipe(Effect.provide(layer));
700 }),
701 );
702
703 it.effect("blocks only repeated Cmd+W input before it reaches the native window menu", () =>
704 Effect.gen(function* () {
705 const fakeWindow = makeFakeBrowserWindow();
706 const createCount = yield* Ref.make(0);
707 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
708 const layer = layerTest({
709 window: fakeWindow.window,
710 createCount,
711 mainWindow,
712 });
713
714 yield* Effect.gen(function* () {
715 const desktopWindow = yield* DesktopWindow.DesktopWindow;
716 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
717
718 const beforeInput = fakeWindow.webContentsListeners.get("before-input-event");
719 if (!beforeInput) {
720 return yield* Effect.die("before-input-event listener was not registered");
721 }
722
723 let prevented = false;
724 const event = { preventDefault: () => (prevented = true) };
725 const input = {
726 type: "keyDown",
727 isAutoRepeat: true,
728 key: "W",
729 meta: true,
730 control: false,
731 alt: false,
732 shift: false,
733 };
734 beforeInput(event, input);
735 assert.isTrue(prevented);
736
737 prevented = false;
738 beforeInput(event, { ...input, isAutoRepeat: false });
739 assert.isFalse(prevented);
740
741 prevented = false;
742 beforeInput(event, { ...input, meta: false });
743 assert.isFalse(prevented);
744 }).pipe(Effect.provide(layer));
745 }),
746 );
747
748 it.effect("forwards native trackpad release to the renderer", () =>
749 Effect.gen(function* () {
750 const fakeWindow = makeFakeBrowserWindow();
751 const send = vi.spyOn(fakeWindow.window.webContents, "send");
752 const createCount = yield* Ref.make(0);
753 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
754 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
755
756 yield* Effect.gen(function* () {
757 const desktopWindow = yield* DesktopWindow.DesktopWindow;
758 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
759 const onInput = fakeWindow.webContentsListeners.get("input-event");
760 if (!onInput) return yield* Effect.die("input-event listener was not registered");
761 onInput({}, { type: "gestureScrollUpdate" });
762 assert.notInclude(
763 send.mock.calls.map(([channel]) => channel),
764 TRACKPAD_SCROLL_END_CHANNEL,
765 );
766 onInput({}, { type: "gestureScrollEnd" });
767 assert.isTrue(send.mock.calls.some(([channel]) => channel === TRACKPAD_SCROLL_END_CHANNEL));
768 }).pipe(Effect.provide(layer));
769 }),
770 );
771
772 // Chromium hands the main window's zoom level down to embedded preview
773 // guests, so every app zoom has to put the preview browser back at its own
774 // zoom or zooming the UI drags the previewed page with it.
775 it.effect("restores the preview browser's own zoom after zooming the app", () =>
776 Effect.gen(function* () {
777 const fakeWindow = makeFakeBrowserWindow();
778 const createCount = yield* Ref.make(0);
779 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
780 const previewZoomReapplies: number[] = [];
781 const layer = layerTest({
782 window: fakeWindow.window,
783 createCount,
784 mainWindow,
785 previewZoomReapplies,
786 });
787
788 yield* Effect.gen(function* () {
789 const desktopWindow = yield* DesktopWindow.DesktopWindow;
790 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
791
792 yield* desktopWindow.zoomMain("out");
793 yield* desktopWindow.zoomMain("out");
794 yield* desktopWindow.zoomMain("in");
795 yield* desktopWindow.zoomMain("reset");
796
797 assert.deepEqual(
798 fakeWindow.setZoomLevel.mock.calls.map(([level]) => level),
799 [-0.5, -1, -0.5, 0],
800 );
801 // Recorded after the window level moved, so the preview is put back at
802 // its own zoom on every step rather than left on the inherited one.
803 assert.deepEqual(previewZoomReapplies, [-0.5, -1, -0.5, 0]);
804 }).pipe(Effect.provide(layer));
805 }),
806 );
807
808 it.effect("reloads and inspects the main window even when a popup has focus", () =>
809 Effect.gen(function* () {
810 const fakeWindow = makeFakeBrowserWindow();
811 const popup = makeFakeBrowserWindow();
812 const createCount = yield* Ref.make(0);
813 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
814 const layer = layerTest({
815 window: fakeWindow.window,
816 createCount,
817 mainWindow,
818 focusedWindow: popup.window,
819 });
820
821 yield* Effect.gen(function* () {
822 const desktopWindow = yield* DesktopWindow.DesktopWindow;
823 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
824
825 yield* desktopWindow.runMainContentsCommand("reload");
826 yield* desktopWindow.runMainContentsCommand("forceReload");
827 yield* desktopWindow.runMainContentsCommand("toggleDevTools");
828
829 assert.equal(fakeWindow.reload.mock.calls.length, 1);
830 assert.equal(fakeWindow.reloadIgnoringCache.mock.calls.length, 1);
831 assert.equal(fakeWindow.toggleDevTools.mock.calls.length, 1);
832 assert.equal(popup.reload.mock.calls.length, 0);
833 assert.equal(popup.reloadIgnoringCache.mock.calls.length, 0);
834 assert.equal(popup.toggleDevTools.mock.calls.length, 0);
835 }).pipe(Effect.provide(layer));
836 }),
837 );
838
839 it.effect("keeps macOS window buttons centered when zooming and leaving fullscreen", () =>
840 Effect.gen(function* () {
841 const fakeWindow = makeFakeBrowserWindow();
842 const createCount = yield* Ref.make(0);
843 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
844 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
845
846 yield* Effect.gen(function* () {
847 const desktopWindow = yield* DesktopWindow.DesktopWindow;
848 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
849
850 for (const direction of ["in", "in", "out", "reset", "out"] as const) {
851 yield* desktopWindow.zoomMain(direction);
852 const position = fakeWindow.setWindowButtonPosition.mock.lastCall?.[0];
853 assert.isDefined(position);
854 // The 14-point native buttons should share the zoomed 52px header's center.
855 const headerCenter = 26 * fakeWindow.window.webContents.getZoomFactor();
856 assert.isAtMost(Math.abs(position.y + 7 - headerCenter), 0.5);
857 assert.equal(position.x, 16);
858 }
859
860 fakeWindow.isFullScreen.mockReturnValue(true);
861 fakeWindow.setWindowButtonPosition.mockClear();
862 yield* desktopWindow.zoomMain("reset");
863 assert.equal(fakeWindow.setWindowButtonPosition.mock.calls.length, 0);
864
865 fakeWindow.isFullScreen.mockReturnValue(false);
866 fakeWindow.windowListeners.get("leave-full-screen")?.();
867 assert.deepEqual(fakeWindow.setWindowButtonPosition.mock.lastCall, [{ x: 16, y: 19 }]);
868 }).pipe(Effect.provide(layer));
869 }),
870 );
871
872 it.effect("uses the persisted main window bounds when opening the window", () =>
873 Effect.gen(function* () {
874 const fakeWindow = makeFakeBrowserWindow();
875 const createCount = yield* Ref.make(0);
876 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
877 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
878 const layer = layerTest({
879 window: fakeWindow.window,
880 createCount,
881 mainWindow,
882 createdWindowOptions,
883 desktopSettings: {
884 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
885 mainWindowBounds: { x: 120, y: 80, width: 1320, height: 880 },
886 },
887 });
888
889 yield* Effect.gen(function* () {
890 const desktopWindow = yield* DesktopWindow.DesktopWindow;
891 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
892
893 assert.equal(createdWindowOptions[0]?.width, 1320);
894 assert.equal(createdWindowOptions[0]?.height, 880);
895 assert.equal(createdWindowOptions[0]?.x, 120);
896 assert.equal(createdWindowOptions[0]?.y, 80);
897 }).pipe(Effect.provide(layer));
898 }),
899 );
900
901 it.effect("restores the persisted maximized state", () =>
902 Effect.gen(function* () {
903 const fakeWindow = makeFakeBrowserWindow();
904 const createCount = yield* Ref.make(0);
905 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
906 const layer = layerTest({
907 window: fakeWindow.window,
908 createCount,
909 mainWindow,
910 desktopSettings: {
911 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
912 mainWindowBounds: { x: 120, y: 80, width: 1320, height: 880 },
913 mainWindowMaximized: true,
914 },
915 });
916
917 yield* Effect.gen(function* () {
918 const desktopWindow = yield* DesktopWindow.DesktopWindow;
919 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
920
921 assert.equal(fakeWindow.maximize.mock.calls.length, 0);
922 const readyToShow = fakeWindow.windowListeners.get("ready-to-show");
923 if (!readyToShow) {
924 return yield* Effect.die("window ready-to-show listener was not registered");
925 }
926 readyToShow();
927 assert.equal(fakeWindow.maximize.mock.calls.length, 1);
928 }).pipe(Effect.provide(layer));
929 }),
930 );
931
932 // The window boots hidden with throttling disabled so first paint runs at
933 // full speed; the first reveal must hand it back to normal hidden-window
934 // throttling or a minimized window stays expensive forever.
935 it.effect("re-enables background throttling on first reveal", () =>
936 Effect.gen(function* () {
937 const fakeWindow = makeFakeBrowserWindow();
938 const createCount = yield* Ref.make(0);
939 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
940 const layer = layerTest({
941 window: fakeWindow.window,
942 createCount,
943 mainWindow,
944 });
945
946 yield* Effect.gen(function* () {
947 const desktopWindow = yield* DesktopWindow.DesktopWindow;
948 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
949
950 assert.equal(fakeWindow.setBackgroundThrottling.mock.calls.length, 0);
951 const readyToShow = fakeWindow.windowListeners.get("ready-to-show");
952 if (!readyToShow) {
953 return yield* Effect.die("window ready-to-show listener was not registered");
954 }
955 readyToShow();
956 assert.deepEqual(fakeWindow.setBackgroundThrottling.mock.calls, [[true]]);
957 }).pipe(Effect.provide(layer));
958 }),
959 );
960
961 it.effect("debounces move and resize bounds updates", () =>
962 Effect.gen(function* () {
963 const fakeWindow = makeFakeBrowserWindow();
964 const createCount = yield* Ref.make(0);
965 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
966 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
967 const layer = layerTest({
968 window: fakeWindow.window,
969 createCount,
970 mainWindow,
971 mainWindowBoundsUpdates,
972 });
973
974 yield* Effect.gen(function* () {
975 const desktopWindow = yield* DesktopWindow.DesktopWindow;
976 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
977
978 const move = fakeWindow.windowListeners.get("move");
979 const resize = fakeWindow.windowListeners.get("resize");
980 if (!move || !resize) {
981 return yield* Effect.die("window bounds listeners were not registered");
982 }
983
984 fakeWindow.getBounds.mockReturnValue({ x: 120, y: 80, width: 1280, height: 840 });
985 move();
986 yield* TestClock.adjust(250);
987
988 fakeWindow.getBounds.mockReturnValue({ x: 160, y: 100, width: 1360, height: 900 });
989 resize();
990 yield* TestClock.adjust(499);
991 assert.deepEqual(mainWindowBoundsUpdates, []);
992
993 yield* TestClock.adjust(1);
994 yield* Effect.promise(() => Promise.resolve());
995 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 160, y: 100, width: 1360, height: 900 }]);
996 }).pipe(Effect.provide(layer));
997 }),
998 );
999
1000 it.effect("persists normal bounds and state for a maximized window", () =>
1001 Effect.gen(function* () {
1002 const fakeWindow = makeFakeBrowserWindow();
1003 fakeWindow.isMaximized.mockReturnValue(true);
1004 fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 });
1005 fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 });
1006 const createCount = yield* Ref.make(0);
1007 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1008 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1009 const mainWindowMaximizedUpdates: boolean[] = [];
1010 const layer = layerTest({
1011 window: fakeWindow.window,
1012 createCount,
1013 mainWindow,
1014 mainWindowBoundsUpdates,
1015 mainWindowMaximizedUpdates,
1016 });
1017
1018 yield* Effect.gen(function* () {
1019 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1020 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1021
1022 const close = fakeWindow.windowListeners.get("close");
1023 if (!close) {
1024 return yield* Effect.die("window close listener was not registered");
1025 }
1026 close();
1027 yield* Effect.promise(() => Promise.resolve());
1028
1029 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 220, y: 140, width: 1380, height: 920 }]);
1030 assert.deepEqual(mainWindowMaximizedUpdates, [true]);
1031 assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1);
1032 assert.equal(fakeWindow.getBounds.mock.calls.length, 0);
1033 }).pipe(Effect.provide(layer));
1034 }),
1035 );
1036
1037 it.effect("persists normal bounds and state from the native maximize event", () =>
1038 Effect.gen(function* () {
1039 const fakeWindow = makeFakeBrowserWindow();
1040 const createCount = yield* Ref.make(0);
1041 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1042 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1043 const mainWindowMaximizedUpdates: boolean[] = [];
1044 const layer = layerTest({
1045 window: fakeWindow.window,
1046 createCount,
1047 mainWindow,
1048 mainWindowBoundsUpdates,
1049 mainWindowMaximizedUpdates,
1050 });
1051
1052 yield* Effect.gen(function* () {
1053 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1054 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1055
1056 const maximize = fakeWindow.windowListeners.get("maximize");
1057 if (!maximize) {
1058 return yield* Effect.die("window maximize listener was not registered");
1059 }
1060
1061 fakeWindow.isMaximized.mockReturnValue(true);
1062 fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 });
1063 fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 });
1064 maximize();
1065 yield* TestClock.adjust(500);
1066 yield* Effect.promise(() => Promise.resolve());
1067
1068 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 220, y: 140, width: 1380, height: 920 }]);
1069 assert.deepEqual(mainWindowMaximizedUpdates, [true]);
1070 }).pipe(Effect.provide(layer));
1071 }),
1072 );
1073
1074 it.effect("does not persist bounds that fail the domain schema", () =>
1075 Effect.gen(function* () {
1076 const fakeWindow = makeFakeBrowserWindow();
1077 fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 839.4, height: 619.4 });
1078 const createCount = yield* Ref.make(0);
1079 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1080 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1081 const layer = layerTest({
1082 window: fakeWindow.window,
1083 createCount,
1084 mainWindow,
1085 mainWindowBoundsUpdates,
1086 });
1087
1088 yield* Effect.gen(function* () {
1089 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1090 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1091
1092 const resize = fakeWindow.windowListeners.get("resize");
1093 if (!resize) {
1094 return yield* Effect.die("window resize listener was not registered");
1095 }
1096 resize();
1097 yield* TestClock.adjust(500);
1098 yield* Effect.promise(() => Promise.resolve());
1099
1100 assert.deepEqual(mainWindowBoundsUpdates, []);
1101 }).pipe(Effect.provide(layer));
1102 }),
1103 );
1104
1105 it.effect("preserves unrestorable bounds until the user changes the window", () =>
1106 Effect.gen(function* () {
1107 const fakeWindow = makeFakeBrowserWindow();
1108 const createCount = yield* Ref.make(0);
1109 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1110 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1111 const layer = layerTest({
1112 window: fakeWindow.window,
1113 createCount,
1114 mainWindow,
1115 mainWindowBoundsUpdates,
1116 desktopSettings: {
1117 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
1118 mainWindowBounds: { x: 2040, y: 80, width: 1320, height: 880 },
1119 },
1120 });
1121
1122 yield* Effect.gen(function* () {
1123 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1124 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1125
1126 const close = fakeWindow.windowListeners.get("close");
1127 const move = fakeWindow.windowListeners.get("move");
1128 if (!close || !move) {
1129 return yield* Effect.die("window lifecycle listeners were not registered");
1130 }
1131
1132 close();
1133 yield* Effect.promise(() => Promise.resolve());
1134 assert.deepEqual(mainWindowBoundsUpdates, []);
1135
1136 fakeWindow.getBounds.mockReturnValue({ x: 80, y: 60, width: 1280, height: 840 });
1137 move();
1138 yield* TestClock.adjust(500);
1139 yield* Effect.promise(() => Promise.resolve());
1140 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 80, y: 60, width: 1280, height: 840 }]);
1141 }).pipe(Effect.provide(layer));
1142 }),
1143 );
1144
1145 it.effect("flushes normal bounds when fullscreen before the debounce completes", () =>
1146 Effect.gen(function* () {
1147 const fakeWindow = makeFakeBrowserWindow();
1148 fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 });
1149 fakeWindow.getNormalBounds.mockReturnValue({ x: 200, y: 130, width: 1400, height: 940 });
1150 const createCount = yield* Ref.make(0);
1151 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1152 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1153 const layer = layerTest({
1154 window: fakeWindow.window,
1155 createCount,
1156 mainWindow,
1157 mainWindowBoundsUpdates,
1158 });
1159
1160 yield* Effect.gen(function* () {
1161 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1162 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1163
1164 const resize = fakeWindow.windowListeners.get("resize");
1165 if (!resize) {
1166 return yield* Effect.die("window resize listener was not registered");
1167 }
1168 resize();
1169 yield* TestClock.adjust(250);
1170 fakeWindow.isFullScreen.mockReturnValue(true);
1171
1172 yield* desktopWindow.flushMainWindowBounds;
1173
1174 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 200, y: 130, width: 1400, height: 940 }]);
1175 assert.equal(fakeWindow.getBounds.mock.calls.length, 0);
1176 assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1);
1177 }).pipe(Effect.provide(layer));
1178 }),
1179 );
1180
1181 it.effect("flushes normal bounds when minimized before the debounce completes", () =>
1182 Effect.gen(function* () {
1183 const fakeWindow = makeFakeBrowserWindow();
1184 fakeWindow.getBounds.mockReturnValue({ x: -32_000, y: -32_000, width: 160, height: 28 });
1185 fakeWindow.getNormalBounds.mockReturnValue({ x: 180, y: 120, width: 1440, height: 960 });
1186 const createCount = yield* Ref.make(0);
1187 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1188 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1189 const layer = layerTest({
1190 window: fakeWindow.window,
1191 createCount,
1192 mainWindow,
1193 mainWindowBoundsUpdates,
1194 });
1195
1196 yield* Effect.gen(function* () {
1197 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1198 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1199
1200 const resize = fakeWindow.windowListeners.get("resize");
1201 if (!resize) {
1202 return yield* Effect.die("window resize listener was not registered");
1203 }
1204 resize();
1205 yield* TestClock.adjust(250);
1206 fakeWindow.isMinimized.mockReturnValue(true);
1207
1208 yield* desktopWindow.flushMainWindowBounds;
1209
1210 assert.deepEqual(mainWindowBoundsUpdates, [{ x: 180, y: 120, width: 1440, height: 960 }]);
1211 assert.equal(fakeWindow.getBounds.mock.calls.length, 0);
1212 assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1);
1213 }).pipe(Effect.provide(layer));
1214 }),
1215 );
1216
1217 it.effect("logs display lookup failures before falling back to the default size", () =>
1218 Effect.gen(function* () {
1219 const displayLookupFailure = new Error("screen API unavailable");
1220 vi.mocked(Electron.screen.getAllDisplays).mockImplementationOnce(() => {
1221 throw displayLookupFailure;
1222 });
1223 const logRecords: Array<{
1224 readonly message: unknown;
1225 readonly annotations: Readonly<Record<string, unknown>>;
1226 }> = [];
1227 const logger = Logger.make(({ fiber, message }) => {
1228 logRecords.push({
1229 message,
1230 annotations: { ...fiber.getRef(References.CurrentLogAnnotations) },
1231 });
1232 });
1233 const fakeWindow = makeFakeBrowserWindow();
1234 const createCount = yield* Ref.make(0);
1235 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1236 const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
1237 const layer = layerTest({
1238 window: fakeWindow.window,
1239 createCount,
1240 mainWindow,
1241 createdWindowOptions,
1242 });
1243
1244 yield* Effect.gen(function* () {
1245 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1246 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1247 }).pipe(
1248 Effect.provide(Layer.mergeAll(layer, Logger.layer([logger], { mergeWithExisting: false }))),
1249 );
1250
1251 const warning = logRecords.find(
1252 (record) =>
1253 Array.isArray(record.message) &&
1254 record.message[0] === "failed to read connected displays; using defaults",
1255 );
1256 assert.isDefined(warning);
1257 assert.strictEqual(warning.annotations.cause, displayLookupFailure);
1258 assert.equal(createdWindowOptions[0]?.width, 1100);
1259 assert.equal(createdWindowOptions[0]?.height, 780);
1260 assert.isUndefined(createdWindowOptions[0]?.x);
1261 assert.isUndefined(createdWindowOptions[0]?.y);
1262 }),
1263 );
1264
1265 it.effect("persists the current main window bounds before the window closes", () =>
1266 Effect.gen(function* () {
1267 const fakeWindow = makeFakeBrowserWindow();
1268 fakeWindow.getBounds.mockReturnValue({ x: 240, y: 160, width: 1410, height: 930 });
1269 const createCount = yield* Ref.make(0);
1270 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1271 const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
1272 const writeStarted = yield* Deferred.make<void>();
1273 const allowWrite = yield* Deferred.make<void>();
1274 const flushCompleted = yield* Deferred.make<void>();
1275 const layer = layerTest({
1276 window: fakeWindow.window,
1277 createCount,
1278 mainWindow,
1279 mainWindowBoundsUpdates,
1280 beforeMainWindowBoundsUpdate: () =>
1281 Deferred.succeed(writeStarted, undefined).pipe(
1282 Effect.andThen(Deferred.await(allowWrite)),
1283 Effect.asVoid,
1284 ),
1285 });
1286
1287 yield* Effect.gen(function* () {
1288 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1289 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1290
1291 const close = fakeWindow.windowListeners.get("close");
1292 if (!close) {
1293 return yield* Effect.die("window close listener was not registered");
1294 }
1295 close();
1296 yield* Deferred.await(writeStarted);
1297 fakeWindow.isDestroyed.mockReturnValue(true);
1298
1299 const flushFiber = yield* desktopWindow.flushMainWindowBounds.pipe(
1300 Effect.andThen(Deferred.succeed(flushCompleted, undefined)),
1301 Effect.forkChild({ startImmediately: true }),
1302 );
1303 yield* Effect.yieldNow;
1304 assert.isFalse(yield* Deferred.isDone(flushCompleted));
1305
1306 yield* Deferred.succeed(allowWrite, undefined);
1307 yield* Fiber.join(flushFiber);
1308 assert.isTrue(yield* Deferred.isDone(flushCompleted));
1309
1310 assert.deepEqual(mainWindowBoundsUpdates, [
1311 {
1312 x: 240,
1313 y: 160,
1314 width: 1410,
1315 height: 930,
1316 },
1317 ]);
1318 }).pipe(Effect.provide(layer));
1319 }),
1320 );
1321
1322 it.effect("publishes native macOS fullscreen changes to the renderer", () =>
1323 Effect.gen(function* () {
1324 const fakeWindow = makeFakeBrowserWindow();
1325 const createCount = yield* Ref.make(0);
1326 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1327 const layer = layerTest({
1328 window: fakeWindow.window,
1329 createCount,
1330 mainWindow,
1331 });
1332
1333 yield* Effect.gen(function* () {
1334 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1335 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1336
1337 const enterFullscreen = fakeWindow.windowListeners.get("enter-full-screen");
1338 const leaveFullscreen = fakeWindow.windowListeners.get("leave-full-screen");
1339 if (!enterFullscreen || !leaveFullscreen) {
1340 return yield* Effect.die("fullscreen listeners were not registered");
1341 }
1342
1343 enterFullscreen();
1344 leaveFullscreen();
1345 assert.deepEqual(fakeWindow.send.mock.calls, [
1346 [WINDOW_FULLSCREEN_STATE_CHANNEL, true],
1347 [WINDOW_FULLSCREEN_STATE_CHANNEL, false],
1348 ]);
1349 }).pipe(Effect.provide(layer));
1350 }),
1351 );
1352
1353 it.effect("recovers when the development renderer is temporarily unreachable", () =>
1354 Effect.gen(function* () {
1355 const fakeWindow = makeFakeBrowserWindow();
1356 const createCount = yield* Ref.make(0);
1357 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1358 const layer = layerTest({
1359 window: fakeWindow.window,
1360 createCount,
1361 mainWindow,
1362 });
1363
1364 yield* Effect.gen(function* () {
1365 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1366 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1367
1368 const didFailLoad = fakeWindow.webContentsListeners.get("did-fail-load");
1369 const didFinishLoad = fakeWindow.webContentsListeners.get("did-finish-load");
1370 if (!didFailLoad || !didFinishLoad) {
1371 return yield* Effect.die("renderer load listeners were not registered");
1372 }
1373
1374 didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true);
1375 assert.equal(fakeWindow.loadURL.mock.calls.length, 1);
1376
1377 yield* TestClock.adjust(100);
1378 assert.deepEqual(fakeWindow.loadURL.mock.calls, [
1379 ["t3code-dev://app/"],
1380 ["t3code-dev://app/"],
1381 ]);
1382 assert.equal(fakeWindow.reload.mock.calls.length, 0);
1383
1384 didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true);
1385 didFinishLoad();
1386 yield* TestClock.adjust(250);
1387 assert.equal(fakeWindow.loadURL.mock.calls.length, 2);
1388 assert.equal(fakeWindow.reload.mock.calls.length, 0);
1389 }).pipe(Effect.provide(layer));
1390 }),
1391 );
1392
1393 it("retries only transient failures for the development renderer", () => {
1394 assert.isTrue(
1395 DesktopWindow.isRetryableDevelopmentRendererLoadFailure({
1396 applicationUrl: "t3code-dev://app/",
1397 errorCode: -102,
1398 isMainFrame: true,
1399 validatedUrl: "t3code-dev://app/",
1400 }),
1401 );
1402 assert.isFalse(
1403 DesktopWindow.isRetryableDevelopmentRendererLoadFailure({
1404 applicationUrl: "t3code-dev://app/",
1405 errorCode: -3,
1406 isMainFrame: true,
1407 validatedUrl: "t3code-dev://app/",
1408 }),
1409 );
1410 assert.isFalse(
1411 DesktopWindow.isRetryableDevelopmentRendererLoadFailure({
1412 applicationUrl: "t3code-dev://app/",
1413 errorCode: -102,
1414 isMainFrame: true,
1415 validatedUrl: "https://example.com/",
1416 }),
1417 );
1418 });
1419
1420 it.effect("opens safe off-origin renderer navigations in the system browser", () =>
1421 Effect.gen(function* () {
1422 const fakeWindow = makeFakeBrowserWindow();
1423 const createCount = yield* Ref.make(0);
1424 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1425 const openedExternalUrls: unknown[] = [];
1426 const layer = layerTest({
1427 window: fakeWindow.window,
1428 createCount,
1429 mainWindow,
1430 openedExternalUrls,
1431 });
1432
1433 yield* Effect.gen(function* () {
1434 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1435 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1436
1437 const willNavigate = fakeWindow.webContentsListeners.get("will-navigate");
1438 if (!willNavigate) {
1439 return yield* Effect.die("will-navigate listener was not registered");
1440 }
1441 let prevented = false;
1442 willNavigate(
1443 {
1444 preventDefault: () => {
1445 prevented = true;
1446 },
1447 },
1448 "https://accounts.microsoft.com/oauth",
1449 );
1450 yield* Effect.promise(() => Promise.resolve());
1451
1452 assert.isTrue(prevented);
1453 assert.deepEqual(openedExternalUrls, ["https://accounts.microsoft.com/oauth"]);
1454 }).pipe(Effect.provide(layer));
1455 }),
1456 );
1457
1458 it.effect(
1459 "retries opening the real main on activate when a failed post-readiness open left only the splash",
1460 () =>
1461 Effect.gen(function* () {
1462 const splash = makeFakeBrowserWindow();
1463 const main = makeFakeBrowserWindow();
1464 // create #1 -> splash, #2 -> fails (the pool swallows this post-readiness
1465 // window-open error), #3 -> the real main on activate's retry.
1466 const scenario = yield* makeSplashScenario([splash.window, null, main.window]);
1467
1468 yield* Effect.gen(function* () {
1469 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1470
1471 // 1. WSL-only boot shows the connecting splash.
1472 yield* desktopWindow.showConnectingSplash;
1473 assert.equal(yield* Ref.get(scenario.createCalls), 1);
1474
1475 // 2. Backend reports ready, but opening the real main fails. The pool
1476 // swallows that error in production, so handleBackendReady fails
1477 // here without a registered main window -- only the splash is open.
1478 const readyExit = yield* Effect.exit(
1479 desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")),
1480 );
1481 assert.equal(readyExit._tag, "Failure");
1482 assert.equal(yield* Ref.get(scenario.createCalls), 2);
1483 assert.isTrue(Option.isNone(yield* Ref.get(scenario.mainWindow)));
1484
1485 // 3. Activating must not mistake the splash for the main window: it
1486 // retries the open and brings up the real main instead of leaving
1487 // the user stranded on "Connecting to WSL".
1488 yield* desktopWindow.activate;
1489 assert.equal(yield* Ref.get(scenario.createCalls), 3);
1490 const registeredMain = yield* Ref.get(scenario.mainWindow);
1491 assert.isTrue(Option.isSome(registeredMain));
1492 assert.equal(Option.getOrThrow(registeredMain), main.window);
1493 }).pipe(Effect.provide(scenario.layer));
1494 }),
1495 );
1496
1497 it.effect(
1498 "re-reveals the connecting splash on activate while the backend is still cold-booting",
1499 () =>
1500 Effect.gen(function* () {
1501 const splash = makeFakeBrowserWindow();
1502 // Only the splash is ever created; the backend never reports ready.
1503 const scenario = yield* makeSplashScenario([splash.window]);
1504
1505 yield* Effect.gen(function* () {
1506 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1507
1508 yield* desktopWindow.showConnectingSplash;
1509 assert.equal(yield* Ref.get(scenario.createCalls), 1);
1510
1511 // Taskbar/dock activation during cold boot must bring the splash back
1512 // rather than no-op and leave it hidden until the backend finishes.
1513 yield* desktopWindow.activate;
1514 assert.equal(yield* Ref.get(scenario.createCalls), 1);
1515 assert.deepEqual(yield* Ref.get(scenario.revealedWindows), [splash.window]);
1516 }).pipe(Effect.provide(scenario.layer));
1517 }),
1518 );
1519
1520 it.effect("does not dispatch menu actions to the splash before the backend is ready", () =>
1521 Effect.gen(function* () {
1522 const splash = makeFakeBrowserWindow();
1523 const main = makeFakeBrowserWindow();
1524 const scenario = yield* makeSplashScenario([splash.window, main.window]);
1525
1526 yield* Effect.gen(function* () {
1527 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1528
1529 yield* desktopWindow.showConnectingSplash;
1530 yield* desktopWindow.dispatchMenuAction("open-settings");
1531
1532 assert.equal(yield* Ref.get(scenario.createCalls), 1);
1533 assert.equal(splash.send.mock.calls.length, 0);
1534 assert.equal(main.send.mock.calls.length, 0);
1535 }).pipe(Effect.provide(scenario.layer));
1536 }),
1537 );
1538
1539 it.effect("dispatches menu actions after backend readiness when no main window exists", () =>
1540 Effect.gen(function* () {
1541 const splash = makeFakeBrowserWindow();
1542 const main = makeFakeBrowserWindow();
1543 const scenario = yield* makeSplashScenario([splash.window, null, main.window]);
1544
1545 yield* Effect.gen(function* () {
1546 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1547
1548 yield* desktopWindow.showConnectingSplash;
1549 const readyExit = yield* Effect.exit(
1550 desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")),
1551 );
1552 assert.equal(readyExit._tag, "Failure");
1553
1554 yield* desktopWindow.dispatchMenuAction("open-settings");
1555
1556 assert.equal(yield* Ref.get(scenario.createCalls), 3);
1557 assert.deepEqual(main.send.mock.calls, [[MENU_ACTION_CHANNEL, "open-settings"]]);
1558 }).pipe(Effect.provide(scenario.layer));
1559 }),
1560 );
1561
1562 it.effect("delivers capture completion and late errors without taking focus back", () =>
1563 Effect.gen(function* () {
1564 const operations: Array<string> = [];
1565 let foreground = "Discord";
1566 const fakeWindow = makeFakeBrowserWindow();
1567 fakeWindow.send.mockImplementation(() => {
1568 operations.push("send");
1569 });
1570 const createCount = yield* Ref.make(0);
1571 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1572 const layer = layerTest({
1573 window: fakeWindow.window,
1574 createCount,
1575 mainWindow,
1576 onReveal: () => {
1577 foreground = "T3 Code";
1578 operations.push("reveal");
1579 },
1580 });
1581
1582 yield* Effect.gen(function* () {
1583 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1584 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1585 yield* desktopWindow.dispatchSnapShotEvent({ type: "started", id: captureOne });
1586 assert.equal(foreground, "T3 Code");
1587 foreground = "Explorer";
1588 yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
1589 yield* desktopWindow.dispatchSnapShotEvent({ type: "failed", id: captureTwo });
1590
1591 assert.equal(foreground, "Explorer");
1592 assert.deepEqual(operations, ["send", "reveal", "send", "send"]);
1593 assert.deepEqual(fakeWindow.send.mock.calls, [
1594 [SNAP_SHOT_EVENT_CHANNEL, { type: "started", id: captureOne }],
1595 [SNAP_SHOT_EVENT_CHANNEL, { type: "ready", id: captureOne }],
1596 [SNAP_SHOT_EVENT_CHANNEL, { type: "failed", id: captureTwo }],
1597 ]);
1598 }).pipe(Effect.provide(layer));
1599 }),
1600 );
1601
1602 it.effect("delivers the renderer event even when the reveal fails", () =>
1603 Effect.gen(function* () {
1604 const fakeWindow = makeFakeBrowserWindow();
1605 const createCount = yield* Ref.make(0);
1606 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1607 const layer = layerTest({
1608 window: fakeWindow.window,
1609 createCount,
1610 mainWindow,
1611 onReveal: () => {
1612 throw new Error("another process kept the foreground");
1613 },
1614 });
1615
1616 yield* Effect.gen(function* () {
1617 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1618 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1619 yield* Effect.exit(
1620 desktopWindow.dispatchSnapShotEvent({ type: "started", id: captureOne }),
1621 );
1622
1623 assert.deepEqual(fakeWindow.send.mock.calls, [
1624 [SNAP_SHOT_EVENT_CHANNEL, { type: "started", id: captureOne }],
1625 ]);
1626 }).pipe(Effect.provide(layer));
1627 }),
1628 );
1629
1630 it.effect("leaves a completed capture pending while only the connecting splash exists", () =>
1631 Effect.gen(function* () {
1632 const splash = makeFakeBrowserWindow();
1633 const scenario = yield* makeSplashScenario([splash.window]);
1634
1635 yield* Effect.gen(function* () {
1636 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1637 yield* desktopWindow.showConnectingSplash;
1638 yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
1639
1640 assert.equal(yield* Ref.get(scenario.createCalls), 1);
1641 assert.equal(splash.send.mock.calls.length, 0);
1642 assert.deepEqual(yield* Ref.get(scenario.revealedWindows), []);
1643 }).pipe(Effect.provide(scenario.layer));
1644 }),
1645 );
1646
1647 it.effect("does not reopen a closed main window for a completed capture", () =>
1648 Effect.gen(function* () {
1649 const fakeWindow = makeFakeBrowserWindow();
1650 const createCount = yield* Ref.make(0);
1651 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
1652 const onReveal = vi.fn();
1653 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow, onReveal });
1654
1655 yield* Effect.gen(function* () {
1656 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1657 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1658 fakeWindow.isDestroyed.mockReturnValue(true);
1659 yield* Ref.set(mainWindow, Option.none());
1660 yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
1661
1662 assert.equal(yield* Ref.get(createCount), 1);
1663 assert.equal(fakeWindow.send.mock.calls.length, 0);
1664 assert.equal(onReveal.mock.calls.length, 0);
1665 }).pipe(Effect.provide(layer));
1666 }),
1667 );
1668
1669 it.effect("waits for a loading renderer without foregrounding it when capture is ready", () =>
1670 Effect.gen(function* () {
1671 const fakeWindow = makeFakeBrowserWindow();
1672 const createCount = yield* Ref.make(0);
1673 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(
1674 Option.some(fakeWindow.window),
1675 );
1676 const onReveal = vi.fn();
1677 const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow, onReveal });
1678 vi.mocked(fakeWindow.window.webContents.isLoadingMainFrame).mockReturnValue(true);
1679
1680 yield* Effect.gen(function* () {
1681 const desktopWindow = yield* DesktopWindow.DesktopWindow;
1682 yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
1683 assert.equal(fakeWindow.send.mock.calls.length, 0);
1684 const onLoad = fakeWindow.webContentsOnce.mock.calls.find(
1685 ([event]) => event === "did-finish-load",
1686 )?.[1];
1687 assert.isDefined(onLoad);
1688 onLoad?.();
1689
1690 assert.deepEqual(fakeWindow.send.mock.calls, [
1691 [SNAP_SHOT_EVENT_CHANNEL, { type: "ready", id: captureOne }],
1692 ]);
1693 assert.equal(onReveal.mock.calls.length, 0);
1694 assert.equal(yield* Ref.get(createCount), 0);
1695 }).pipe(Effect.provide(layer));
1696 }),
1697 );
1698});