1340 lines · 52.4 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as References from "effect/References";import * as Schema from "effect/Schema";import * as TestClock from "effect/testing/TestClock"; import * as Electron from "electron";import { vi } from "vite-plus/test"; vi.mock("electron", async (importOriginal) => ({  ...(await importOriginal<typeof import("electron")>()),  session: {    fromPartition: vi.fn(() => ({      getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/1.2.3"),      setPermissionRequestHandler: vi.fn(),      setUserAgent: vi.fn(),    })),  },  screen: {    getAllDisplays: vi.fn(() => [      {        bounds: { x: 0, y: 0, width: 1920, height: 1080 },      },    ]),  },})); import * as DesktopAssets from "../app/DesktopAssets.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as DesktopState from "../app/DesktopState.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";import * as ElectronApp from "../electron/ElectronApp.ts";import * as ElectronMenu from "../electron/ElectronMenu.ts";import * as ElectronShell from "../electron/ElectronShell.ts";import * as ElectronTheme from "../electron/ElectronTheme.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts";import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";import * as DesktopWindow from "./DesktopWindow.ts";import * as PreviewManager from "../preview/Manager.ts"; const environmentInput = {  dirname: "/repo/apps/desktop/dist-electron",  homeDirectory: "/Users/alice",  platform: "darwin",  processArch: "arm64",  appVersion: "1.2.3",  appPath: "/repo",  isPackaged: false,  resourcesPath: "/repo/resources",  runningUnderArm64Translation: false,} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; function makeFakeBrowserWindow() {  const windowListeners = new Map<string, (...args: readonly unknown[]) => void>();  const webContentsListeners = new Map<string, (...args: readonly unknown[]) => void>();  let zoomLevel = 0;  const webContents = {    copyImageAt: vi.fn(),    downloadURL: vi.fn(),    getURL: vi.fn(() => "t3code-dev://app/"),    getZoomLevel: vi.fn(() => zoomLevel),    setZoomLevel: vi.fn((level: number) => {      zoomLevel = level;    }),    isLoadingMainFrame: vi.fn(() => false),    on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {      webContentsListeners.set(eventName, listener);    }),    once: vi.fn(),    openDevTools: vi.fn(),    reload: vi.fn(),    replaceMisspelling: vi.fn(),    send: vi.fn(),    setBackgroundThrottling: vi.fn(),    setWindowOpenHandler: vi.fn(),  };   const window = {    close: vi.fn(),    focus: vi.fn(),    getBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })),    getNormalBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })),    isDestroyed: vi.fn(() => false),    isFullScreen: vi.fn(() => false),    isMaximized: vi.fn(() => false),    isMinimized: vi.fn(() => false),    isVisible: vi.fn(() => true),    loadURL: vi.fn(() => Promise.resolve()),    maximize: vi.fn(),    on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {      windowListeners.set(eventName, listener);    }),    once: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {      windowListeners.set(eventName, listener);    }),    restore: vi.fn(),    setBackgroundColor: vi.fn(),    setAutoHideCursor: vi.fn(),    setFullScreen: vi.fn(),    setOpacity: vi.fn(),    setTitle: vi.fn(),    setTitleBarOverlay: vi.fn(),    show: vi.fn(),    webContents,  };   return {    window: window as unknown as Electron.BrowserWindow,    copyImageAt: webContents.copyImageAt,    downloadURL: webContents.downloadURL,    getBounds: window.getBounds,    getNormalBounds: window.getNormalBounds,    isDestroyed: window.isDestroyed,    isFullScreen: window.isFullScreen,    isMaximized: window.isMaximized,    isMinimized: window.isMinimized,    loadURL: window.loadURL,    maximize: window.maximize,    openDevTools: webContents.openDevTools,    reload: webContents.reload,    send: webContents.send,    setZoomLevel: webContents.setZoomLevel,    setBackgroundThrottling: webContents.setBackgroundThrottling,    setAutoHideCursor: window.setAutoHideCursor,    setFullScreen: window.setFullScreen,    setOpacity: window.setOpacity,    webContentsListeners,    windowListeners,  };} const desktopClientSettingsLayer = Layer.mock(DesktopClientSettings.DesktopClientSettings)({  get: Effect.succeed(Option.none()),}); const electronAppLayer = Layer.mock(ElectronApp.ElectronApp)({  quit: Effect.void,}); const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, {  iconPaths: Effect.succeed({    ico: Option.none<string>(),    icns: Option.none<string>(),    png: Option.none<string>(),  }),  resolveResourcePath: () => Effect.succeed(Option.none<string>()),} satisfies DesktopAssets.DesktopAssets["Service"]); const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {  getState: Effect.die("unexpected getState"),  backendConfig: Effect.succeed({    port: 3773,    bindHost: "127.0.0.1",    httpBaseUrl: new URL("http://127.0.0.1:3773"),    tailscaleServeEnabled: false,    tailscaleServePort: 443,  }),  configureFromSettings: () => Effect.die("unexpected configureFromSettings"),  setMode: () => Effect.die("unexpected setMode"),  setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),  getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"),} satisfies DesktopServerExposure.DesktopServerExposure["Service"]); const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {  setApplicationMenu: () => Effect.void,  popupTemplate: () => Effect.void,  showContextMenu: () => Effect.succeed(Option.none()),} satisfies ElectronMenu.ElectronMenu["Service"]); const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {  shouldUseDarkColors: Effect.succeed(false),  setSource: () => Effect.void,  onUpdated: () => Effect.void,} satisfies ElectronTheme.ElectronTheme["Service"]); const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe(  Layer.provide(    Layer.mergeAll(      NodeServices.layer,      DesktopConfig.layerTest({        T3CODE_PORT: "3773",        VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",      }),    ),  ),); const desktopWindowBoundsEquivalence = Schema.toEquivalence(  DesktopAppSettings.DesktopWindowBoundsSchema,); function makeTestLayer(input: {  readonly window: Electron.BrowserWindow;  readonly createCount: Ref.Ref<number>;  readonly mainWindow: Ref.Ref<Option.Option<Electron.BrowserWindow>>;  readonly createdWindowOptions?: Electron.BrowserWindowConstructorOptions[];  readonly desktopSettings?: DesktopAppSettings.DesktopSettings;  readonly mainWindowBoundsUpdates?: DesktopAppSettings.DesktopWindowBounds[];  readonly mainWindowMaximizedUpdates?: boolean[];  readonly beforeMainWindowBoundsUpdate?: (    bounds: DesktopAppSettings.DesktopWindowBounds,  ) => Effect.Effect<void>;  readonly openedExternalUrls?: unknown[];  readonly copiedTexts?: string[];  readonly popupTemplates?: Electron.MenuItemConstructorOptions[][];  readonly previewZoomReapplies?: number[];}) {  let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;  const desktopAppSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {    get: Effect.sync(() => desktopSettings),    load: Effect.sync(() => desktopSettings),    setMainWindowBounds: (bounds, isMaximized) =>      Effect.gen(function* () {        if (input.beforeMainWindowBoundsUpdate) {          yield* input.beforeMainWindowBoundsUpdate(bounds);        }        const changed =          desktopSettings.mainWindowBounds === null ||          !desktopWindowBoundsEquivalence(desktopSettings.mainWindowBounds, bounds) ||          desktopSettings.mainWindowMaximized !== isMaximized;        if (changed) {          desktopSettings = {            ...desktopSettings,            mainWindowBounds: bounds,            mainWindowMaximized: isMaximized,          };          input.mainWindowBoundsUpdates?.push(bounds);          input.mainWindowMaximizedUpdates?.push(isMaximized);        }        return { settings: desktopSettings, changed };      }),    setServerExposureMode: () => Effect.die("unexpected server exposure update"),    setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),    setUpdateChannel: () => Effect.die("unexpected update channel change"),    setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),    setWslDistro: () => Effect.die("unexpected WSL distro change"),    setWslOnly: () => Effect.die("unexpected WSL-only toggle"),    applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"),    applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"),  } satisfies DesktopAppSettings.DesktopAppSettings["Service"]);   const electronWindowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {    create: (options) =>      Effect.sync(() => {        input.createdWindowOptions?.push(options);      }).pipe(        Effect.andThen(Ref.update(input.createCount, (count) => count + 1)),        Effect.as(input.window),      ),    main: Ref.get(input.mainWindow),    currentMainOrFirst: Ref.get(input.mainWindow),    focusedMainOrFirst: Ref.get(input.mainWindow),    setMain: (window) => Ref.set(input.mainWindow, Option.some(window)),    clearMain: () => Ref.set(input.mainWindow, Option.none()),    reveal: () => Effect.void,    sendAll: () => Effect.void,    destroyAll: Effect.void,    syncAllAppearance: (sync) => sync(input.window),  } satisfies ElectronWindow.ElectronWindow["Service"]);   return DesktopWindow.layer.pipe(    Layer.provide(      Layer.mergeAll(        desktopAssetsLayer,        desktopEnvironmentLayer,        desktopAppSettingsLayer,        desktopClientSettingsLayer,        desktopServerExposureLayer,        DesktopState.layer,        electronAppLayer,        input.popupTemplates          ? Layer.succeed(ElectronMenu.ElectronMenu, {              setApplicationMenu: () => Effect.void,              popupTemplate: ({ template }) =>                Effect.sync(() => input.popupTemplates?.push([...template])),              showContextMenu: () => Effect.succeed(Option.none()),            } satisfies ElectronMenu.ElectronMenu["Service"])          : electronMenuLayer,        Layer.succeed(ElectronShell.ElectronShell, {          openExternal: (url) =>            Effect.sync(() => {              input.openedExternalUrls?.push(url);              return true;            }),          copyText: (text) =>            Effect.sync(() => {              input.copiedTexts?.push(text);            }),        } satisfies ElectronShell.ElectronShell["Service"]),        electronThemeLayer,        electronWindowLayer,        Layer.mock(PreviewManager.PreviewManager)({          getBrowserSession: () => Effect.succeed({} as Electron.Session),          setMainWindow: () => Effect.void,          isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"),          getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"),          reapplyZoom: () =>            Effect.sync(() => {              input.previewZoomReapplies?.push(input.window.webContents.getZoomLevel());            }),        }),      ),    ),  );} // Builds a DesktopWindow over a fake ElectronWindow whose `create` returns the// given outcomes in order (null => simulated open failure), and whose// currentMainOrFirst mirrors the real fallback to the first live window (the// splash, before any main is registered). Reveal targets are recorded so tests// can assert what activation actually surfaced.const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | null)[]) =>  Effect.gen(function* () {    const createdWindows = yield* Ref.make<Electron.BrowserWindow[]>([]);    const createCalls = yield* Ref.make(0);    const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());    const revealedWindows = yield* Ref.make<Electron.BrowserWindow[]>([]);    const fallbackWindow = createOutcomes.find(      (window): window is Electron.BrowserWindow => window !== null,    );     const currentMainOrFirst = Effect.gen(function* () {      const registered = yield* Ref.get(mainWindow);      if (Option.isSome(registered)) {        return registered;      }      const created = yield* Ref.get(createdWindows);      return Option.fromNullishOr(created[0] ?? null);    });     const electronWindowShape = {      create: () =>        Effect.gen(function* () {          const index = yield* Ref.getAndUpdate(createCalls, (count) => count + 1);          const outcome = createOutcomes[index] ?? null;          if (outcome === null) {            return yield* new ElectronWindow.ElectronWindowCreateError({              options: {                title: null,                width: null,                height: null,                minWidth: null,                minHeight: null,                show: null,                modal: null,                frame: null,                transparent: null,                backgroundColor: null,                webPreferences: {                  preload: null,                  partition: null,                  backgroundThrottling: null,                  sandbox: null,                  contextIsolation: null,                  nodeIntegration: null,                  webviewTag: null,                },              },              cause: new Error("simulated window-open failure"),            });          }          yield* Ref.update(createdWindows, (windows) => [...windows, outcome]);          return outcome;        }),      main: Ref.get(mainWindow),      currentMainOrFirst,      focusedMainOrFirst: currentMainOrFirst,      setMain: (window) => Ref.set(mainWindow, Option.some(window)),      clearMain: () => Ref.set(mainWindow, Option.none()),      reveal: (window) => Ref.update(revealedWindows, (windows) => [...windows, window]),      sendAll: () => Effect.void,      destroyAll: Effect.void,      syncAllAppearance: (sync) => (fallbackWindow ? sync(fallbackWindow) : Effect.void),    } satisfies ElectronWindow.ElectronWindow["Service"];     const layer = DesktopWindow.layer.pipe(      Layer.provide(        Layer.mergeAll(          desktopAssetsLayer,          desktopEnvironmentLayer,          DesktopAppSettings.layerTest(),          desktopClientSettingsLayer,          desktopServerExposureLayer,          electronAppLayer,          electronMenuLayer,          Layer.succeed(ElectronShell.ElectronShell, {            openExternal: () => Effect.succeed(true),            copyText: () => Effect.void,          } satisfies ElectronShell.ElectronShell["Service"]),          electronThemeLayer,          Layer.succeed(ElectronWindow.ElectronWindow, electronWindowShape),          Layer.mock(PreviewManager.PreviewManager)({            getBrowserSession: () => Effect.succeed({} as Electron.Session),            setMainWindow: () => Effect.void,            isBrowserPartition: (partition) => partition.startsWith("persist:t3code-preview-"),            getBrowserPartition: () => Effect.succeed("persist:t3code-preview-test"),          }),        ),      ),    );     return { layer, createCalls, mainWindow, revealedWindows } as const;  }); describe("DesktopWindow", () => {  it.effect("keeps copy, save, and URL actions native for external images", () =>    Effect.gen(function* () {      const fakeWindow = makeFakeBrowserWindow();      const createCount = yield* Ref.make(0);      const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());      const copiedTexts: string[] = [];      const popupTemplates: Electron.MenuItemConstructorOptions[][] = [];      const layer = makeTestLayer({        window: fakeWindow.window,        createCount,        mainWindow,        copiedTexts,        popupTemplates,      });      const imageUrl = "https://images.example/screenshot.png";       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));        const contextMenu = fakeWindow.webContentsListeners.get("context-menu");        if (!contextMenu) return yield* Effect.die("context-menu listener was not registered");         contextMenu({ preventDefault: vi.fn() }, {          dictionarySuggestions: [],          editFlags: { canCopy: false, canCut: false, canPaste: false, canSelectAll: false },          linkURL: "",          mediaType: "image",          misspelledWord: "",          srcURL: imageUrl,          x: 42,          y: 84,        } as unknown as Electron.ContextMenuParams);        yield* Effect.promise(() => Promise.resolve());         const template = popupTemplates.at(-1) ?? [];        const byLabel = (label: string) => template.find((item) => item.label === label);        assert.ok(byLabel("Copy URL"));        assert.ok(byLabel("Save Image"));        assert.ok(byLabel("Copy Image"));         byLabel("Copy URL")?.click?.(          {} as Electron.MenuItem,          fakeWindow.window,          {} as Electron.KeyboardEvent,        );        byLabel("Save Image")?.click?.(          {} as Electron.MenuItem,          fakeWindow.window,          {} as Electron.KeyboardEvent,        );        byLabel("Copy Image")?.click?.(          {} as Electron.MenuItem,          fakeWindow.window,          {} as Electron.KeyboardEvent,        );        yield* Effect.promise(() => Promise.resolve());         assert.deepEqual(copiedTexts, [imageUrl]);        assert.deepEqual(fakeWindow.downloadURL.mock.calls, [[imageUrl]]);        assert.deepEqual(fakeWindow.copyImageAt.mock.calls, [[42, 84]]);      }).pipe(Effect.provide(layer));    }),  );   it("leaves fullscreen before concealing a pending quit", () => {    const fakeWindow = makeFakeBrowserWindow();     DesktopWindow.concealPendingQuitWindow(fakeWindow.window);    assert.deepEqual(fakeWindow.setOpacity.mock.calls, [[0]]);     fakeWindow.setOpacity.mockClear();    fakeWindow.isFullScreen.mockReturnValue(true);    DesktopWindow.concealPendingQuitWindow(fakeWindow.window);    assert.deepEqual(fakeWindow.setFullScreen.mock.calls, [[false]]);    assert.deepEqual(fakeWindow.setOpacity.mock.calls, [[0]]);     fakeWindow.setOpacity.mockClear();    fakeWindow.isFullScreen.mockReturnValue(false);    fakeWindow.isDestroyed.mockReturnValue(true);    DesktopWindow.concealPendingQuitWindow(fakeWindow.window);    assert.equal(fakeWindow.setOpacity.mock.calls.length, 0);  });   it("restores bounds only when the window fits within a connected display", () => {    const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 };

Lines 1–500 of 1340.