| @@ -66,6 +66,7 @@ function makeFakeBrowserWindow() { · 65 unmodified lines |
| 66 | let zoomLevel = 0; | 66 | let zoomLevel = 0; |
| 67 | const webContents = { | 67 | const webContents = { |
| 68 | copyImageAt: vi.fn(), | 68 | copyImageAt: vi.fn(), |
| | 69 | downloadURL: vi.fn(), |
| 69 | getURL: vi.fn(() => "t3code-dev://app/"), | 70 | getURL: vi.fn(() => "t3code-dev://app/"), |
| 70 | getZoomLevel: vi.fn(() => zoomLevel), | 71 | getZoomLevel: vi.fn(() => zoomLevel), |
| 71 | setZoomLevel: vi.fn((level: number) => { | 72 | setZoomLevel: vi.fn((level: number) => { |
| @@ -115,6 +116,8 @@ function makeFakeBrowserWindow() { · 43 unmodified lines |
| 115 | | 116 | |
| 116 | return { | 117 | return { |
| 117 | window: window as unknown as Electron.BrowserWindow, | 118 | window: window as unknown as Electron.BrowserWindow, |
| | 119 | copyImageAt: webContents.copyImageAt, |
| | 120 | downloadURL: webContents.downloadURL, |
| 118 | getBounds: window.getBounds, | 121 | getBounds: window.getBounds, |
| 119 | getNormalBounds: window.getNormalBounds, | 122 | getNormalBounds: window.getNormalBounds, |
| 120 | isDestroyed: window.isDestroyed, | 123 | isDestroyed: window.isDestroyed, |
| @@ -208,6 +211,8 @@ function makeTestLayer(input: { · 87 unmodified lines |
| 208 | bounds: DesktopAppSettings.DesktopWindowBounds, | 211 | bounds: DesktopAppSettings.DesktopWindowBounds, |
| 209 | ) => Effect.Effect<void>; | 212 | ) => Effect.Effect<void>; |
| 210 | readonly openedExternalUrls?: unknown[]; | 213 | readonly openedExternalUrls?: unknown[]; |
| | 214 | readonly copiedTexts?: string[]; |
| | 215 | readonly popupTemplates?: Electron.MenuItemConstructorOptions[][]; |
| 211 | readonly previewZoomReapplies?: number[]; | 216 | readonly previewZoomReapplies?: number[]; |
| 212 | }) { | 217 | }) { |
| 213 | let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS; | 218 | let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS; |
| @@ -273,14 +278,24 @@ function makeTestLayer(input: { · 59 unmodified lines |
| 273 | desktopServerExposureLayer, | 278 | desktopServerExposureLayer, |
| 274 | DesktopState.layer, | 279 | DesktopState.layer, |
| 275 | electronAppLayer, | 280 | electronAppLayer, |
| 276 | electronMenuLayer, | 281 | input.popupTemplates |
| | 282 | ? Layer.succeed(ElectronMenu.ElectronMenu, { |
| | 283 | setApplicationMenu: () => Effect.void, |
| | 284 | popupTemplate: ({ template }) => |
| | 285 | Effect.sync(() => input.popupTemplates?.push([...template])), |
| | 286 | showContextMenu: () => Effect.succeed(Option.none()), |
| | 287 | } satisfies ElectronMenu.ElectronMenu["Service"]) |
| | 288 | : electronMenuLayer, |
| 277 | Layer.succeed(ElectronShell.ElectronShell, { | 289 | Layer.succeed(ElectronShell.ElectronShell, { |
| 278 | openExternal: (url) => | 290 | openExternal: (url) => |
| 279 | Effect.sync(() => { | 291 | Effect.sync(() => { |
| 280 | input.openedExternalUrls?.push(url); | 292 | input.openedExternalUrls?.push(url); |
| 281 | return true; | 293 | return true; |
| 282 | }), | 294 | }), |
| 283 | copyText: () => Effect.void, | 295 | copyText: (text) => |
| | 296 | Effect.sync(() => { |
| | 297 | input.copiedTexts?.push(text); |
| | 298 | }), |
| 284 | } satisfies ElectronShell.ElectronShell["Service"]), | 299 | } satisfies ElectronShell.ElectronShell["Service"]), |
| 285 | electronThemeLayer, | 300 | electronThemeLayer, |
| 286 | electronWindowLayer, | 301 | electronWindowLayer, |
| @@ -398,6 +413,70 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n · 111 unmodified lines |
| 398 | }); | 413 | }); |
| 399 | | 414 | |
| 400 | describe("DesktopWindow", () => { | 415 | describe("DesktopWindow", () => { |
| | 416 | it.effect("keeps copy, save, and URL actions native for external images", () => |
| | 417 | Effect.gen(function* () { |
| | 418 | const fakeWindow = makeFakeBrowserWindow(); |
| | 419 | const createCount = yield* Ref.make(0); |
| | 420 | const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); |
| | 421 | const copiedTexts: string[] = []; |
| | 422 | const popupTemplates: Electron.MenuItemConstructorOptions[][] = []; |
| | 423 | const layer = makeTestLayer({ |
| | 424 | window: fakeWindow.window, |
| | 425 | createCount, |
| | 426 | mainWindow, |
| | 427 | copiedTexts, |
| | 428 | popupTemplates, |
| | 429 | }); |
| | 430 | const imageUrl = "https://images.example/screenshot.png"; |
| | 431 | |
| | 432 | yield* Effect.gen(function* () { |
| | 433 | const desktopWindow = yield* DesktopWindow.DesktopWindow; |
| | 434 | yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); |
| | 435 | const contextMenu = fakeWindow.webContentsListeners.get("context-menu"); |
| | 436 | if (!contextMenu) return yield* Effect.die("context-menu listener was not registered"); |
| | 437 | |
| | 438 | contextMenu({ preventDefault: vi.fn() }, { |
| | 439 | dictionarySuggestions: [], |
| | 440 | editFlags: { canCopy: false, canCut: false, canPaste: false, canSelectAll: false }, |
| | 441 | linkURL: "", |
| | 442 | mediaType: "image", |
| | 443 | misspelledWord: "", |
| | 444 | srcURL: imageUrl, |
| | 445 | x: 42, |
| | 446 | y: 84, |
| | 447 | } as unknown as Electron.ContextMenuParams); |
| | 448 | yield* Effect.promise(() => Promise.resolve()); |
| | 449 | |
| | 450 | const template = popupTemplates.at(-1) ?? []; |
| | 451 | const byLabel = (label: string) => template.find((item) => item.label === label); |
| | 452 | assert.ok(byLabel("Copy URL")); |
| | 453 | assert.ok(byLabel("Save Image")); |
| | 454 | assert.ok(byLabel("Copy Image")); |
| | 455 | |
| | 456 | byLabel("Copy URL")?.click?.( |
| | 457 | {} as Electron.MenuItem, |
| | 458 | fakeWindow.window, |
| | 459 | {} as Electron.KeyboardEvent, |
| | 460 | ); |
| | 461 | byLabel("Save Image")?.click?.( |
| | 462 | {} as Electron.MenuItem, |
| | 463 | fakeWindow.window, |
| | 464 | {} as Electron.KeyboardEvent, |
| | 465 | ); |
| | 466 | byLabel("Copy Image")?.click?.( |
| | 467 | {} as Electron.MenuItem, |
| | 468 | fakeWindow.window, |
| | 469 | {} as Electron.KeyboardEvent, |
| | 470 | ); |
| | 471 | yield* Effect.promise(() => Promise.resolve()); |
| | 472 | |
| | 473 | assert.deepEqual(copiedTexts, [imageUrl]); |
| | 474 | assert.deepEqual(fakeWindow.downloadURL.mock.calls, [[imageUrl]]); |
| | 475 | assert.deepEqual(fakeWindow.copyImageAt.mock.calls, [[42, 84]]); |
| | 476 | }).pipe(Effect.provide(layer)); |
| | 477 | }), |
| | 478 | ); |
| | 479 | |
| 401 | it("leaves fullscreen before concealing a pending quit", () => { | 480 | it("leaves fullscreen before concealing a pending quit", () => { |
| 402 | const fakeWindow = makeFakeBrowserWindow(); | 481 | const fakeWindow = makeFakeBrowserWindow(); |
| 403 | | 482 | |