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"); }), );});
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");
}),
);
});