585 lines · 22.2 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as TestClock from "effect/testing/TestClock"; import type * 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(),    })),  },})); 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 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>();  const webContents = {    copyImageAt: vi.fn(),    getURL: vi.fn(() => "t3code-dev://app/"),    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(),    setWindowOpenHandler: vi.fn(),  };   const window = {    close: vi.fn(),    focus: vi.fn(),    isDestroyed: vi.fn(() => false),    isFullScreen: vi.fn(() => false),    isMinimized: vi.fn(() => false),    isVisible: vi.fn(() => true),    loadURL: vi.fn(() => Promise.resolve()),    on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {      windowListeners.set(eventName, listener);    }),    once: vi.fn(),    restore: vi.fn(),    setBackgroundColor: vi.fn(),    setAutoHideCursor: vi.fn(),    setTitle: vi.fn(),    setTitleBarOverlay: vi.fn(),    show: vi.fn(),    webContents,  };   return {    window: window as unknown as Electron.BrowserWindow,    loadURL: window.loadURL,    openDevTools: webContents.openDevTools,    reload: webContents.reload,    send: webContents.send,    setAutoHideCursor: window.setAutoHideCursor,    webContentsListeners,    windowListeners,  };} 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",      }),    ),  ),); 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 openedExternalUrls?: unknown[];}) {  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,        desktopServerExposureLayer,        DesktopState.layer,        electronMenuLayer,        Layer.succeed(ElectronShell.ElectronShell, {          openExternal: (url) =>            Effect.sync(() => {              input.openedExternalUrls?.push(url);              return true;            }),          copyText: () => Effect.void,        } 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"),        }),      ),    ),  );} // 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,                  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,          desktopServerExposureLayer,          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("recognizes only same-origin renderer navigations", () => {    assert.isTrue(      DesktopWindow.isSameOriginRendererNavigation({        applicationUrl: "t3code://app/",        navigationUrl: "t3code://app/settings/connections",      }),    );    assert.isFalse(      DesktopWindow.isSameOriginRendererNavigation({        applicationUrl: "t3code://app/",        navigationUrl: "https://accounts.microsoft.com/oauth",      }),    );    assert.isFalse(      DesktopWindow.isSameOriginRendererNavigation({        applicationUrl: "t3code://app/",        navigationUrl: "not a url",      }),    );  });   it.effect("does not open a development window until the backend is ready", () =>    Effect.gen(function* () {      const fakeWindow = makeFakeBrowserWindow();      const createCount = yield* Ref.make(0);      const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());      const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];      const layer = makeTestLayer({        window: fakeWindow.window,        createCount,        mainWindow,        createdWindowOptions,      });       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.activate;        assert.equal(yield* Ref.get(createCount), 0);         yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));        assert.equal(yield* Ref.get(createCount), 1);        assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor);        assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]);        assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]);        assert.equal(fakeWindow.openDevTools.mock.calls.length, 1);      }).pipe(Effect.provide(layer));    }),  );   it.effect("publishes native macOS fullscreen changes to the renderer", () =>    Effect.gen(function* () {      const fakeWindow = makeFakeBrowserWindow();      const createCount = yield* Ref.make(0);      const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());      const layer = makeTestLayer({        window: fakeWindow.window,        createCount,        mainWindow,      });       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));         const enterFullscreen = fakeWindow.windowListeners.get("enter-full-screen");        const leaveFullscreen = fakeWindow.windowListeners.get("leave-full-screen");        if (!enterFullscreen || !leaveFullscreen) {          return yield* Effect.die("fullscreen listeners were not registered");        }         enterFullscreen();        leaveFullscreen();        assert.deepEqual(fakeWindow.send.mock.calls, [          [WINDOW_FULLSCREEN_STATE_CHANNEL, true],          [WINDOW_FULLSCREEN_STATE_CHANNEL, false],        ]);      }).pipe(Effect.provide(layer));    }),  );   it.effect("recovers when the development renderer is temporarily unreachable", () =>    Effect.gen(function* () {      const fakeWindow = makeFakeBrowserWindow();      const createCount = yield* Ref.make(0);      const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());      const layer = makeTestLayer({        window: fakeWindow.window,        createCount,        mainWindow,      });       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));         const didFailLoad = fakeWindow.webContentsListeners.get("did-fail-load");        const didFinishLoad = fakeWindow.webContentsListeners.get("did-finish-load");        if (!didFailLoad || !didFinishLoad) {          return yield* Effect.die("renderer load listeners were not registered");        }         didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true);        assert.equal(fakeWindow.loadURL.mock.calls.length, 1);         yield* TestClock.adjust(100);        assert.deepEqual(fakeWindow.loadURL.mock.calls, [          ["t3code-dev://app/"],          ["t3code-dev://app/"],        ]);        assert.equal(fakeWindow.reload.mock.calls.length, 0);         didFailLoad({}, -9, "ERR_UNEXPECTED", "t3code-dev://app/", true);        didFinishLoad();        yield* TestClock.adjust(250);        assert.equal(fakeWindow.loadURL.mock.calls.length, 2);        assert.equal(fakeWindow.reload.mock.calls.length, 0);      }).pipe(Effect.provide(layer));    }),  );   it("retries only transient failures for the development renderer", () => {    assert.isTrue(      DesktopWindow.isRetryableDevelopmentRendererLoadFailure({        applicationUrl: "t3code-dev://app/",        errorCode: -102,        isMainFrame: true,        validatedUrl: "t3code-dev://app/",      }),    );    assert.isFalse(      DesktopWindow.isRetryableDevelopmentRendererLoadFailure({        applicationUrl: "t3code-dev://app/",        errorCode: -3,        isMainFrame: true,        validatedUrl: "t3code-dev://app/",      }),    );    assert.isFalse(      DesktopWindow.isRetryableDevelopmentRendererLoadFailure({        applicationUrl: "t3code-dev://app/",        errorCode: -102,        isMainFrame: true,        validatedUrl: "https://example.com/",      }),    );  });   it.effect("opens safe off-origin renderer navigations in the system browser", () =>    Effect.gen(function* () {      const fakeWindow = makeFakeBrowserWindow();      const createCount = yield* Ref.make(0);      const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());      const openedExternalUrls: unknown[] = [];      const layer = makeTestLayer({        window: fakeWindow.window,        createCount,        mainWindow,        openedExternalUrls,      });       yield* Effect.gen(function* () {        const desktopWindow = yield* DesktopWindow.DesktopWindow;        yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));         const willNavigate = fakeWindow.webContentsListeners.get("will-navigate");        if (!willNavigate) {          return yield* Effect.die("will-navigate listener was not registered");        }        let prevented = false;        willNavigate(          {            preventDefault: () => {              prevented = true;            },          },          "https://accounts.microsoft.com/oauth",        );        yield* Effect.promise(() => Promise.resolve());         assert.isTrue(prevented);        assert.deepEqual(openedExternalUrls, ["https://accounts.microsoft.com/oauth"]);      }).pipe(Effect.provide(layer));    }),  );   it.effect(    "retries opening the real main on activate when a failed post-readiness open left only the splash",    () =>      Effect.gen(function* () {        const splash = makeFakeBrowserWindow();        const main = makeFakeBrowserWindow();        // create #1 -> splash, #2 -> fails (the pool swallows this post-readiness        // window-open error), #3 -> the real main on activate's retry.        const scenario = yield* makeSplashScenario([splash.window, null, main.window]);         yield* Effect.gen(function* () {          const desktopWindow = yield* DesktopWindow.DesktopWindow;           // 1. WSL-only boot shows the connecting splash.          yield* desktopWindow.showConnectingSplash;          assert.equal(yield* Ref.get(scenario.createCalls), 1);           // 2. Backend reports ready, but opening the real main fails. The pool          //    swallows that error in production, so handleBackendReady fails

Lines 1–500 of 585.