| @@ -66,6 +66,7 @@ function makeFakeBrowserWindow() { · 65 unmodified lines |
| 66 | 66 | let zoomLevel = 0; |
| 67 | 67 | const webContents = { |
| 68 | 68 | copyImageAt: vi.fn(), |
| 69 | + downloadURL: vi.fn(), |
| 69 | 70 | getURL: vi.fn(() => "t3code-dev://app/"), |
| 70 | 71 | getZoomLevel: vi.fn(() => zoomLevel), |
| 71 | 72 | setZoomLevel: vi.fn((level: number) => { |
| @@ -115,6 +116,8 @@ function makeFakeBrowserWindow() { · 43 unmodified lines |
| 115 | 116 | |
| 116 | 117 | return { |
| 117 | 118 | window: window as unknown as Electron.BrowserWindow, |
| 119 | + copyImageAt: webContents.copyImageAt, |
| 120 | + downloadURL: webContents.downloadURL, |
| 118 | 121 | getBounds: window.getBounds, |
| 119 | 122 | getNormalBounds: window.getNormalBounds, |
| 120 | 123 | isDestroyed: window.isDestroyed, |
| @@ -208,6 +211,8 @@ function makeTestLayer(input: { · 87 unmodified lines |
| 208 | 211 | bounds: DesktopAppSettings.DesktopWindowBounds, |
| 209 | 212 | ) => Effect.Effect<void>; |
| 210 | 213 | readonly openedExternalUrls?: unknown[]; |
| 214 | + readonly copiedTexts?: string[]; |
| 215 | + readonly popupTemplates?: Electron.MenuItemConstructorOptions[][]; |
| 211 | 216 | readonly previewZoomReapplies?: number[]; |
| 212 | 217 | }) { |
| 213 | 218 | let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS; |
| @@ -273,14 +278,24 @@ function makeTestLayer(input: { · 59 unmodified lines |
| 273 | 278 | desktopServerExposureLayer, |
| 274 | 279 | DesktopState.layer, |
| 275 | 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 | 289 | Layer.succeed(ElectronShell.ElectronShell, { |
| 278 | 290 | openExternal: (url) => |
| 279 | 291 | Effect.sync(() => { |
| 280 | 292 | input.openedExternalUrls?.push(url); |
| 281 | 293 | return true; |
| 282 | 294 | }), |
| 283 | | − copyText: () => Effect.void, |
| 295 | + copyText: (text) => |
| 296 | + Effect.sync(() => { |
| 297 | + input.copiedTexts?.push(text); |
| 298 | + }), |
| 284 | 299 | } satisfies ElectronShell.ElectronShell["Service"]), |
| 285 | 300 | electronThemeLayer, |
| 286 | 301 | electronWindowLayer, |
| @@ -398,6 +413,70 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n · 111 unmodified lines |
| 398 | 413 | }); |
| 399 | 414 | |
| 400 | 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 | 480 | it("leaves fullscreen before concealing a pending quit", () => { |
| 402 | 481 | const fakeWindow = makeFakeBrowserWindow(); |
| 403 | 482 | |