From 13d02f66195a0b9cda51e2c1d42a0deb829118d5 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Tue, 19 May 2026 20:19:07 +1000 Subject: [PATCH] Mark destructive menu icons as template images - fixes macOS context menu icon rendering - adds regression coverage --- .../desktop/src/electron/ElectronMenu.test.ts | 45 ++++++++++++++++++- apps/desktop/src/electron/ElectronMenu.ts | 1 + 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/electron/ElectronMenu.test.ts b/apps/desktop/src/electron/ElectronMenu.test.ts index 0e66c5a6f3f..8eaab585494 100644 --- a/apps/desktop/src/electron/ElectronMenu.test.ts +++ b/apps/desktop/src/electron/ElectronMenu.test.ts @@ -2,7 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import type * as Electron from "electron"; -import { beforeEach, vi } from "vitest"; +import { afterEach, beforeEach, vi } from "vitest"; const { buildFromTemplateMock, createFromNamedImageMock, setApplicationMenuMock } = vi.hoisted( () => ({ @@ -24,6 +24,8 @@ vi.mock("electron", () => ({ import * as ElectronMenu from "./ElectronMenu.ts"; +const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + describe("ElectronMenu", () => { beforeEach(() => { buildFromTemplateMock.mockReset(); @@ -31,6 +33,12 @@ describe("ElectronMenu", () => { setApplicationMenuMock.mockReset(); }); + afterEach(() => { + if (originalPlatformDescriptor) { + Object.defineProperty(process, "platform", originalPlatformDescriptor); + } + }); + it.effect("returns none without building a menu when there are no valid items", () => Effect.gen(function* () { const electronMenu = yield* ElectronMenu.ElectronMenu; @@ -116,4 +124,39 @@ describe("ElectronMenu", () => { assert.equal(popupMock.mock.calls.length, 1); }).pipe(Effect.provide(ElectronMenu.layer)), ); + + it.effect("uses template images for destructive macOS context menu icons", () => + Effect.gen(function* () { + Object.defineProperty(process, "platform", { + configurable: true, + value: "darwin", + }); + + const resizedIcon = { + isEmpty: vi.fn(() => false), + setTemplateImage: vi.fn((_template: boolean) => undefined), + }; + const sourceIcon = { + resize: vi.fn((_size: { width: number; height: number }) => resizedIcon), + }; + createFromNamedImageMock.mockReturnValue(sourceIcon); + buildFromTemplateMock.mockImplementation(() => ({ + popup: (options: Electron.PopupOptions) => { + options.callback?.(); + }, + })); + + const electronMenu = yield* ElectronMenu.ElectronMenu; + yield* electronMenu.showContextMenu({ + window: {} as Electron.BrowserWindow, + items: [{ id: "delete", label: "Remove project", destructive: true }], + position: Option.none(), + }); + + assert.equal(createFromNamedImageMock.mock.calls[0]?.[0], "trash"); + assert.deepEqual(sourceIcon.resize.mock.calls[0]?.[0], { width: 12, height: 12 }); + assert.deepEqual(resizedIcon.setTemplateImage.mock.calls[0], [true]); + assert.strictEqual(buildFromTemplateMock.mock.calls[0]?.[0][0]?.icon, resizedIcon); + }).pipe(Effect.provide(ElectronMenu.layer)), + ); }); diff --git a/apps/desktop/src/electron/ElectronMenu.ts b/apps/desktop/src/electron/ElectronMenu.ts index 7164fdb54c1..7b2d0d6f4c4 100644 --- a/apps/desktop/src/electron/ElectronMenu.ts +++ b/apps/desktop/src/electron/ElectronMenu.ts @@ -89,6 +89,7 @@ export const layer = Layer.sync(ElectronMenu, () => { width: 12, height: 12, }); + icon.setTemplateImage(true); destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon); } catch { destructiveMenuIconCache = Option.none();