189 lines · 7.8 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"),  systemLocale: Effect.succeed("en-US"),  whenReady: Effect.void,  quit: Effect.void,  exit: () => Effect.void,  relaunch: () => Effect.void,  setPath: () => Effect.void,  setName: () => Effect.void,  setAboutPanelOptions: () => Effect.void,  setAppUserModelId: () => Effect.void,  getAppMetrics: Effect.succeed([]),  isDefaultProtocolClient: () => Effect.succeed(false),  setAsDefaultProtocolClient: () => Effect.succeed(true),  setDesktopName: () => Effect.void,  setDockIcon: () => Effect.void,  appendCommandLineSwitch: () => Effect.void,  onBeforeQuitForUpdate: () => Effect.void,  removeCommandLineSwitch: () => Effect.void,  on: () => Effect.void,} satisfies ElectronApp.ElectronApp["Service"]); const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {  pickFolder: () => Effect.succeed(Option.none()),  pickFiles: () => Effect.succeed([]),  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"),  isActionActive: Effect.succeed(false),  isInstallActive: Effect.succeed(false),  subscribe: Effect.die("unexpected subscribe"),  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"),  installPrepared: () => Effect.die("unexpected installPrepared"),} satisfies DesktopUpdates.DesktopUpdates["Service"]); const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>  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,    prepareCaptureReveal: Effect.void,    dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),    dispatchSnapShotEvent: () => Effect.void,    zoomMain: (direction) =>      Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),    syncAppearance: Effect.void,  } satisfies DesktopWindow.DesktopWindow["Service"]); const makeElectronMenuLayer = (  applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,) =>  Layer.succeed(ElectronMenu.ElectronMenu, {    setApplicationMenu: (template) =>      Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),    popupTemplate: () => Effect.void,    showContextMenu: () => Effect.succeed(Option.none()),  } satisfies ElectronMenu.ElectronMenu["Service"]); const configureMenu = (  selectedAction: Deferred.Deferred<string>,  applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,) =>  Effect.gen(function* () {    const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;    yield* menu.configure;  }).pipe(    Effect.provide(      DesktopApplicationMenu.layer.pipe(        Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),        Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),        Layer.provideMerge(desktopUpdatesLayer),        Layer.provideMerge(electronDialogLayer),        Layer.provideMerge(electronAppLayer),        Layer.provideMerge(          DesktopEnvironment.layer(environmentInput).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(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");    }),  );   // Zoom must route through DesktopWindow.zoomMain instead of the Electron  // zoom roles: the roles zoom whichever webContents has focus, which breaks  // app zoom while an embedded preview WebContentsView holds focus.  it.effect("routes View menu zoom to the main window instead of zoom roles", () =>    Effect.gen(function* () {      const selectedAction = yield* Deferred.make<string>();      const applicationMenuTemplate =        yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();       yield* configureMenu(selectedAction, applicationMenuTemplate);       const template = yield* Deferred.await(applicationMenuTemplate);      const viewMenu = template.find((item) => item.label === "View");      assert.isDefined(viewMenu);      if (!Array.isArray(viewMenu.submenu)) {        throw new Error("Expected View menu submenu to be an array.");      }       assert.isUndefined(        viewMenu.submenu.find((item) => item.role?.toLowerCase().includes("zoom")),      );       const zoomIn = viewMenu.submenu.find((item) => item.label === "Zoom In");      assert.isDefined(zoomIn);      assert.equal(zoomIn.accelerator, "CmdOrCtrl+=");      if (typeof zoomIn.click !== "function") {        throw new Error("Expected Zoom In menu item to have a click handler.");      }       zoomIn.click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);      assert.equal(yield* Deferred.await(selectedAction), "zoom-in");    }),  );});