241 lines · 10.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 * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option"; import type * as Electron from "electron"; import * as ElectronApp from "../electron/ElectronApp.ts";import * as ElectronDialog from "../electron/ElectronDialog.ts";import * as ElectronMenu from "../electron/ElectronMenu.ts";import * as DesktopApplicationMenu from "./DesktopApplicationMenu.ts";import * as DesktopConfig from "../app/DesktopConfig.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as DesktopUpdates from "../updates/DesktopUpdates.ts";import * as DesktopWindow from "./DesktopWindow.ts"; const environmentInput = {  dirname: "/repo/apps/desktop/dist-electron",  homeDirectory: "/Users/alice",  platform: "linux",  processArch: "arm64",  appVersion: "1.2.3",  appPath: "/repo",  isPackaged: false,  resourcesPath: "/repo/resources",  runningUnderArm64Translation: false,} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {  metadata: Effect.die("unexpected metadata read"),  name: Effect.succeed("T3 Code"),  whenReady: Effect.void,  quit: Effect.void,  exit: () => Effect.void,  relaunch: () => Effect.void,  setPath: () => Effect.void,  setName: () => Effect.void,  setAboutPanelOptions: () => Effect.void,  setAppUserModelId: () => Effect.void,  requestSingleInstanceLock: Effect.succeed(true),  isDefaultProtocolClient: () => Effect.succeed(false),  setAsDefaultProtocolClient: () => Effect.succeed(true),  setDesktopName: () => Effect.void,  setDockIcon: () => Effect.void,  appendCommandLineSwitch: () => Effect.void,  on: () => Effect.void,} satisfies ElectronApp.ElectronApp["Service"]); const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {  pickFolder: () => Effect.succeed(Option.none()),  confirm: () => Effect.succeed(false),  showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),  showErrorBox: () => Effect.void,} satisfies ElectronDialog.ElectronDialog["Service"]); const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {  getState: Effect.die("unexpected getState"),  emitState: Effect.void,  disabledReason: Effect.succeed(Option.none()),  configure: Effect.void,  setChannel: () => Effect.die("unexpected setChannel"),  check: () => Effect.die("unexpected check"),  download: Effect.die("unexpected download"),  install: Effect.die("unexpected install"),} satisfies DesktopUpdates.DesktopUpdates["Service"]); const makeDesktopWindowLayer = (  selectedAction: Deferred.Deferred<string>,  mainWindowActionsHandled: boolean,) =>  Layer.succeed(DesktopWindow.DesktopWindow, {    createMain: Effect.die("unexpected createMain"),    ensureMain: Effect.die("unexpected ensureMain"),    revealOrCreateMain: Effect.die("unexpected revealOrCreateMain"),    activate: Effect.void,    createMainIfBackendReady: Effect.void,    showConnectingSplash: Effect.void,    handleBackendReady: () => Effect.void,    handleBackendNotReady: Effect.void,    flushMainWindowBounds: Effect.void,    dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),    dispatchMenuActionToMainWindow: (_window, action) =>      mainWindowActionsHandled        ? Deferred.succeed(selectedAction, action).pipe(Effect.as(true))        : Effect.succeed(false),    syncAppearance: Effect.void,  } satisfies DesktopWindow.DesktopWindow["Service"]); const makeElectronMenuLayer = (  applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,  nativeActions: string[] = [],) =>  Layer.succeed(ElectronMenu.ElectronMenu, {    setApplicationMenu: (template) =>      Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),    sendActionToFirstResponder: (action) =>      Effect.sync(() => {        nativeActions.push(action);      }),    popupTemplate: () => Effect.void,    showContextMenu: () => Effect.succeed(Option.none()),  } satisfies ElectronMenu.ElectronMenu["Service"]); const configureMenu = (  platform: "darwin" | "linux",  selectedAction: Deferred.Deferred<string>,  applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,  nativeActions: string[] = [],  mainWindowActionsHandled = true,) =>  Effect.gen(function* () {    const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;    yield* menu.configure;  }).pipe(    Effect.provide(      DesktopApplicationMenu.layer.pipe(        Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate, nativeActions)),        Layer.provideMerge(makeDesktopWindowLayer(selectedAction, mainWindowActionsHandled)),        Layer.provideMerge(desktopUpdatesLayer),        Layer.provideMerge(electronDialogLayer),        Layer.provideMerge(electronAppLayer),        Layer.provideMerge(          DesktopEnvironment.layer({ ...environmentInput, platform }).pipe(            Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),          ),        ),      ),    ),  ); describe("DesktopApplicationMenu", () => {  it.effect("installs the native menu and routes Settings through DesktopWindow", () =>    Effect.gen(function* () {      const selectedAction = yield* Deferred.make<string>();      const applicationMenuTemplate =        yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();       yield* configureMenu("linux", selectedAction, applicationMenuTemplate);       const template = yield* Deferred.await(applicationMenuTemplate);      const fileMenu = template.find((item) => item.label === "File");      assert.isDefined(fileMenu);      if (!Array.isArray(fileMenu.submenu)) {        throw new Error("Expected File menu submenu to be an array.");      }      const settingsItem = fileMenu.submenu.find((item) => item.label === "Settings...");      assert.isDefined(settingsItem);      const settingsClick = settingsItem.click;      if (typeof settingsClick !== "function") {        throw new Error("Expected Settings menu item to have a click handler.");      }       settingsClick({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);      assert.equal(yield* Deferred.await(selectedAction), "open-settings");    }),  );   it.effect("routes the macOS close shortcut through the renderer", () =>    Effect.gen(function* () {      const selectedAction = yield* Deferred.make<string>();      const applicationMenuTemplate =        yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();       yield* configureMenu("darwin", selectedAction, applicationMenuTemplate);       const template = yield* Deferred.await(applicationMenuTemplate);      const fileMenu = template.find((item) => item.label === "File");      assert.isDefined(fileMenu);      if (!Array.isArray(fileMenu.submenu)) {        throw new Error("Expected File menu submenu to be an array.");      }      const closeItem = fileMenu.submenu.find((item) => item.label === "Close Window");      assert.isDefined(closeItem);      assert.equal(closeItem.accelerator, "Cmd+W");      const closeClick = closeItem.click;      if (typeof closeClick !== "function") {        throw new Error("Expected Close Window menu item to have a click handler.");      }       closeClick({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);      assert.equal(yield* Deferred.await(selectedAction), "close-window-or-right-panel");    }),  );   it.effect("preserves native close behavior when system UI owns focus", () =>    Effect.gen(function* () {      const selectedAction = yield* Deferred.make<string>();      const applicationMenuTemplate =        yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();      const nativeActions: string[] = [];       yield* configureMenu("darwin", selectedAction, applicationMenuTemplate, nativeActions);       const template = yield* Deferred.await(applicationMenuTemplate);      const fileMenu = template.find((item) => item.label === "File");      assert.isDefined(fileMenu);      if (!Array.isArray(fileMenu.submenu)) {        throw new Error("Expected File menu submenu to be an array.");      }      const closeItem = fileMenu.submenu.find((item) => item.label === "Close Window");      assert.isDefined(closeItem);      const closeClick = closeItem.click;      if (typeof closeClick !== "function") {        throw new Error("Expected Close Window menu item to have a click handler.");      }       closeClick({} as Electron.MenuItem, undefined, {} as KeyboardEvent);      assert.deepEqual(nativeActions, ["performClose:"]);    }),  );   it.effect("preserves native close behavior for a non-main browser window", () =>    Effect.gen(function* () {      const selectedAction = yield* Deferred.make<string>();      const applicationMenuTemplate =        yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();      const nativeActions: string[] = [];       yield* configureMenu("darwin", selectedAction, applicationMenuTemplate, nativeActions, false);       const template = yield* Deferred.await(applicationMenuTemplate);      const fileMenu = template.find((item) => item.label === "File");      assert.isDefined(fileMenu);      if (!Array.isArray(fileMenu.submenu)) {        throw new Error("Expected File menu submenu to be an array.");      }      const closeItem = fileMenu.submenu.find((item) => item.label === "Close Window");      assert.isDefined(closeItem);      const closeClick = closeItem.click;      if (typeof closeClick !== "function") {        throw new Error("Expected Close Window menu item to have a click handler.");      }       closeClick({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);      yield* Effect.yieldNow;      assert.deepEqual(nativeActions, ["performClose:"]);    }),  );});