1539 lines · 61.0 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 { DesktopSnapShotId } from "@t3tools/contracts";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 Queue from "effect/Queue";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 * as NodeEvents from "node:events";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,  SNAP_SHOT_EVENT_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(),    focus: vi.fn(),    isDestroyed: vi.fn(() => false),    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<(eventName: string, listener: (...args: readonly unknown[]) => void) => void>(),    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,    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,    webContentsOnce: webContents.once,    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 onPopupTemplate?: (input: ElectronMenu.ElectronMenuTemplateInput) => Effect.Effect<void>;  readonly previewZoomReapplies?: number[];  readonly onReveal?: (window: Electron.BrowserWindow) => void;}) {  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()),    prepareReveal: () => Effect.succeed(false),    reveal: (window) => Effect.sync(() => input.onReveal?.(window)),    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,        Layer.succeed(ElectronMenu.ElectronMenu, {          setApplicationMenu: () => Effect.void,          showContextMenu: () => Effect.succeed(Option.none()),          popupTemplate: input.onPopupTemplate ?? (() => Effect.void),        }),        Layer.succeed(ElectronShell.ElectronShell, {          openExternal: (url) =>            Effect.sync(() => {              input.openedExternalUrls?.push(url);              return true;            }),          openSystemSettings: () => Effect.succeed(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()),      prepareReveal: () => Effect.succeed(false),      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),            openSystemSettings: () => 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;  }); const captureOne = DesktopSnapShotId.make("11111111-1111-4111-8111-111111111111");const captureTwo = DesktopSnapShotId.make("22222222-2222-4222-8222-222222222222"); describe("DesktopWindow", () => {  it.effect("shows native context menus for browser guests and sign-in popups", () =>    Effect.gen(function* () {      const host = makeFakeBrowserWindow();      const popup = makeFakeBrowserWindow();      let focusedContents: unknown = host.window.webContents;      const makeContents = () => {        const contents = Object.assign(new NodeEvents.EventEmitter(), {          isDestroyed: vi.fn(() => false),          focus: vi.fn(() => {            focusedContents = contents;          }),          copyImageAt: vi.fn(),          replaceMisspelling: vi.fn(),        });        return contents;      };      const guest = makeContents();      const popupContents = makeContents();      const popupWindow = { ...popup.window, webContents: popupContents };      const menus = yield* Queue.unbounded<{        input: ElectronMenu.ElectronMenuTemplateInput;        focusedContents: unknown;      }>();      const copiedTexts: string[] = [];      const layer = makeTestLayer({        window: host.window,        createCount: yield* Ref.make(0),        mainWindow: yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()),        copiedTexts,        onPopupTemplate: (input) =>          Queue.offer(menus, { input, focusedContents }).pipe(Effect.asVoid),      });       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));        const attach = host.webContentsListeners.get("did-attach-webview");        assert.isDefined(attach);        attach({}, guest);        attach({}, guest);        guest.emit("did-create-window", popupWindow);        guest.emit("did-create-window", popupWindow);         for (const [contents, owner] of [          [guest, host.window],          [popupContents, popupWindow],        ] as const) {          const frame = { routingId: 7 } as Electron.WebFrameMain;          const preventDefault = vi.fn();          const params = {            frame,            x: 12,            y: 34,            misspelledWord: "helo",            dictionarySuggestions: ["hello"],            linkURL: "",            mediaType: "none",            editFlags: { canCut: false, canCopy: true, canPaste: true, canSelectAll: true },          };          focusedContents = host.window.webContents;          contents.emit("context-menu", { preventDefault }, params);          const menu = yield* Queue.take(menus);          assert.strictEqual(menu.input.window, owner);          assert.strictEqual(menu.input.frame, frame);          assert.strictEqual(menu.focusedContents, contents);          assert.equal(preventDefault.mock.calls.length, 1);          assert.deepEqual(            menu.input.template.filter((item) => item.role),            [              { role: "cut", enabled: false },              { role: "copy", enabled: true },              { role: "paste", enabled: true },              { role: "selectAll", enabled: true },

Lines 1–500 of 1539.