import { assert, describe, it } from "@effect/vitest";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Cause from "effect/Cause";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 { beforeEach, vi } from "vite-plus/test"; const { buildFromTemplateMock, createFromNamedImageMock, setApplicationMenuMock } = vi.hoisted( () => ({ buildFromTemplateMock: vi.fn(), createFromNamedImageMock: vi.fn(), setApplicationMenuMock: vi.fn(), }),); vi.mock("electron", () => ({ Menu: { buildFromTemplate: buildFromTemplateMock, setApplicationMenu: setApplicationMenuMock, }, nativeImage: { createFromNamedImage: createFromNamedImageMock, },})); import * as ElectronMenu from "./ElectronMenu.ts"; const TestLayer = ElectronMenu.layer.pipe( Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),); const makeWindow = (zoomFactor = 1): Electron.BrowserWindow => ({ id: 7, webContents: { getZoomFactor: () => zoomFactor }, }) as unknown as Electron.BrowserWindow; describe("ElectronMenu", () => { beforeEach(() => { buildFromTemplateMock.mockReset(); createFromNamedImageMock.mockReset(); setApplicationMenuMock.mockReset(); }); it.effect("returns none without building a menu when there are no valid items", () => Effect.gen(function* () { const electronMenu = yield* ElectronMenu.ElectronMenu; const selectedItemId = yield* electronMenu.showContextMenu({ window: {} as Electron.BrowserWindow, items: [], position: Option.none(), }); assert.isTrue(Option.isNone(selectedItemId)); assert.equal(buildFromTemplateMock.mock.calls.length, 0); }).pipe(Effect.provide(TestLayer)), ); it.effect("resolves with the clicked leaf item id", () => Effect.gen(function* () { buildFromTemplateMock.mockImplementation( (template: Electron.MenuItemConstructorOptions[]) => ({ popup: () => { const firstItem = template[0]; assert.isDefined(firstItem); const click = firstItem.click; if (!click) { throw new Error("Expected menu item to have a click handler."); } click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent); }, }), ); const electronMenu = yield* ElectronMenu.ElectronMenu; const selectedItemId = yield* electronMenu.showContextMenu({ window: makeWindow(), items: [{ id: "copy", label: "Copy" }], position: Option.none(), }); assert.equal(Option.getOrNull(selectedItemId), "copy"); }).pipe(Effect.provide(TestLayer)), ); it.effect("resolves with none when the menu closes without a click", () => Effect.gen(function* () { let popupOptions: Electron.PopupOptions | undefined; buildFromTemplateMock.mockImplementation(() => ({ popup: (options: Electron.PopupOptions) => { popupOptions = options; options.callback?.(); }, })); const electronMenu = yield* ElectronMenu.ElectronMenu; const selectedItemId = yield* electronMenu.showContextMenu({ window: makeWindow(2), items: [{ id: "copy", label: "Copy" }], position: Option.some({ x: 10.8, y: 20.2 }), }); assert.isTrue(Option.isNone(selectedItemId)); assert.equal(popupOptions?.x, 21); assert.equal(popupOptions?.y, 40); assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], { label: "Copy", enabled: true, click: buildFromTemplateMock.mock.calls[0]?.[0][0].click, }); }).pipe(Effect.provide(TestLayer)), ); it.effect("defers popupTemplate side effects until the returned Effect runs", () => Effect.gen(function* () { const popupMock = vi.fn(); buildFromTemplateMock.mockImplementation(() => ({ popup: popupMock })); const electronMenu = yield* ElectronMenu.ElectronMenu; const popup = electronMenu.popupTemplate({ window: {} as Electron.BrowserWindow, template: [{ label: "Copy" }], }); assert.equal(buildFromTemplateMock.mock.calls.length, 0); assert.equal(popupMock.mock.calls.length, 0); yield* popup; assert.equal(buildFromTemplateMock.mock.calls.length, 1); assert.equal(popupMock.mock.calls.length, 1); }).pipe(Effect.provide(TestLayer)), ); it.effect("preserves application-menu failures as structured defects", () => Effect.gen(function* () { const cause = new Error("application menu build failed"); buildFromTemplateMock.mockImplementationOnce(() => { throw cause; }); const electronMenu = yield* ElectronMenu.ElectronMenu; const exit = yield* Effect.exit( electronMenu.setApplicationMenu([{ label: "File" }, { label: "Edit" }]), ); assert.equal(exit._tag, "Failure"); if (exit._tag === "Failure") { const error = Cause.squash(exit.cause); assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError); assert.equal(error.operation, "set-application-menu"); assert.equal(error.platform, "linux"); assert.isNull(error.windowId); assert.equal(error.itemCount, 2); assert.strictEqual(error.cause, cause); assert.notInclude(error.message, cause.message); } }).pipe(Effect.provide(TestLayer)), ); it.effect("preserves popup-template failures with window context", () => Effect.gen(function* () { const cause = new Error("popup failed"); buildFromTemplateMock.mockReturnValueOnce({ popup: () => { throw cause; }, }); const electronMenu = yield* ElectronMenu.ElectronMenu; const exit = yield* Effect.exit( electronMenu.popupTemplate({ window: { id: 41 } as Electron.BrowserWindow, template: [{ label: "Copy" }], }), ); assert.equal(exit._tag, "Failure"); if (exit._tag === "Failure") { const error = Cause.squash(exit.cause); assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError); assert.equal(error.operation, "popup-template"); assert.equal(error.windowId, 41); assert.equal(error.itemCount, 1); assert.strictEqual(error.cause, cause); } }).pipe(Effect.provide(TestLayer)), ); it.effect("preserves context-menu failures with normalized item context", () => Effect.gen(function* () { const cause = new Error("context menu build failed"); buildFromTemplateMock.mockImplementationOnce(() => { throw cause; }); const electronMenu = yield* ElectronMenu.ElectronMenu; const exit = yield* Effect.exit( electronMenu.showContextMenu({ window: { id: 42 } as Electron.BrowserWindow, items: [{ id: "copy", label: "Copy" }], position: Option.none(), }), ); assert.equal(exit._tag, "Failure"); if (exit._tag === "Failure") { const error = Cause.squash(exit.cause); assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError); assert.equal(error.operation, "show-context-menu"); assert.equal(error.windowId, 42); assert.equal(error.itemCount, 1); assert.strictEqual(error.cause, cause); } }).pipe(Effect.provide(TestLayer)), );});
import { assert, describe, it } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Cause from "effect/Cause";
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 { beforeEach, vi } from "vite-plus/test";
const { buildFromTemplateMock, createFromNamedImageMock, setApplicationMenuMock } = vi.hoisted(
() => ({
buildFromTemplateMock: vi.fn(),
createFromNamedImageMock: vi.fn(),
setApplicationMenuMock: vi.fn(),
}),
);
vi.mock("electron", () => ({
Menu: {
buildFromTemplate: buildFromTemplateMock,
setApplicationMenu: setApplicationMenuMock,
},
nativeImage: {
createFromNamedImage: createFromNamedImageMock,
},
}));
import * as ElectronMenu from "./ElectronMenu.ts";
const TestLayer = ElectronMenu.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
);
const makeWindow = (zoomFactor = 1): Electron.BrowserWindow =>
({
id: 7,
webContents: { getZoomFactor: () => zoomFactor },
}) as unknown as Electron.BrowserWindow;
describe("ElectronMenu", () => {
beforeEach(() => {
buildFromTemplateMock.mockReset();
createFromNamedImageMock.mockReset();
setApplicationMenuMock.mockReset();
});
it.effect("returns none without building a menu when there are no valid items", () =>
Effect.gen(function* () {
const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
items: [],
position: Option.none(),
});
assert.isTrue(Option.isNone(selectedItemId));
assert.equal(buildFromTemplateMock.mock.calls.length, 0);
}).pipe(Effect.provide(TestLayer)),
);
it.effect("resolves with the clicked leaf item id", () =>
Effect.gen(function* () {
buildFromTemplateMock.mockImplementation(
(template: Electron.MenuItemConstructorOptions[]) => ({
popup: () => {
const firstItem = template[0];
assert.isDefined(firstItem);
const click = firstItem.click;
if (!click) {
throw new Error("Expected menu item to have a click handler.");
}
click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);
},
}),
);
const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: makeWindow(),
items: [{ id: "copy", label: "Copy" }],
position: Option.none(),
});
assert.equal(Option.getOrNull(selectedItemId), "copy");
}).pipe(Effect.provide(TestLayer)),
);
it.effect("resolves with none when the menu closes without a click", () =>
Effect.gen(function* () {
let popupOptions: Electron.PopupOptions | undefined;
buildFromTemplateMock.mockImplementation(() => ({
popup: (options: Electron.PopupOptions) => {
popupOptions = options;
options.callback?.();
},
}));
const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: makeWindow(2),
items: [{ id: "copy", label: "Copy" }],
position: Option.some({ x: 10.8, y: 20.2 }),
});
assert.isTrue(Option.isNone(selectedItemId));
assert.equal(popupOptions?.x, 21);
assert.equal(popupOptions?.y, 40);
assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], {
label: "Copy",
enabled: true,
click: buildFromTemplateMock.mock.calls[0]?.[0][0].click,
});
}).pipe(Effect.provide(TestLayer)),
);
it.effect("defers popupTemplate side effects until the returned Effect runs", () =>
Effect.gen(function* () {
const popupMock = vi.fn();
buildFromTemplateMock.mockImplementation(() => ({ popup: popupMock }));
const electronMenu = yield* ElectronMenu.ElectronMenu;
const popup = electronMenu.popupTemplate({
window: {} as Electron.BrowserWindow,
template: [{ label: "Copy" }],
});
assert.equal(buildFromTemplateMock.mock.calls.length, 0);
assert.equal(popupMock.mock.calls.length, 0);
yield* popup;
assert.equal(buildFromTemplateMock.mock.calls.length, 1);
assert.equal(popupMock.mock.calls.length, 1);
}).pipe(Effect.provide(TestLayer)),
);
it.effect("preserves application-menu failures as structured defects", () =>
Effect.gen(function* () {
const cause = new Error("application menu build failed");
buildFromTemplateMock.mockImplementationOnce(() => {
throw cause;
});
const electronMenu = yield* ElectronMenu.ElectronMenu;
const exit = yield* Effect.exit(
electronMenu.setApplicationMenu([{ label: "File" }, { label: "Edit" }]),
);
assert.equal(exit._tag, "Failure");
if (exit._tag === "Failure") {
const error = Cause.squash(exit.cause);
assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError);
assert.equal(error.operation, "set-application-menu");
assert.equal(error.platform, "linux");
assert.isNull(error.windowId);
assert.equal(error.itemCount, 2);
assert.strictEqual(error.cause, cause);
assert.notInclude(error.message, cause.message);
}
}).pipe(Effect.provide(TestLayer)),
);
it.effect("preserves popup-template failures with window context", () =>
Effect.gen(function* () {
const cause = new Error("popup failed");
buildFromTemplateMock.mockReturnValueOnce({
popup: () => {
throw cause;
},
});
const electronMenu = yield* ElectronMenu.ElectronMenu;
const exit = yield* Effect.exit(
electronMenu.popupTemplate({
window: { id: 41 } as Electron.BrowserWindow,
template: [{ label: "Copy" }],
}),
);
assert.equal(exit._tag, "Failure");
if (exit._tag === "Failure") {
const error = Cause.squash(exit.cause);
assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError);
assert.equal(error.operation, "popup-template");
assert.equal(error.windowId, 41);
assert.equal(error.itemCount, 1);
assert.strictEqual(error.cause, cause);
}
}).pipe(Effect.provide(TestLayer)),
);
it.effect("preserves context-menu failures with normalized item context", () =>
Effect.gen(function* () {
const cause = new Error("context menu build failed");
buildFromTemplateMock.mockImplementationOnce(() => {
throw cause;
});
const electronMenu = yield* ElectronMenu.ElectronMenu;
const exit = yield* Effect.exit(
electronMenu.showContextMenu({
window: { id: 42 } as Electron.BrowserWindow,
items: [{ id: "copy", label: "Copy" }],
position: Option.none(),
}),
);
assert.equal(exit._tag, "Failure");
if (exit._tag === "Failure") {
const error = Cause.squash(exit.cause);
assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError);
assert.equal(error.operation, "show-context-menu");
assert.equal(error.windowId, 42);
assert.equal(error.itemCount, 1);
assert.strictEqual(error.cause, cause);
}
}).pipe(Effect.provide(TestLayer)),
);
});