Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function makePoolLayer(
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
dispatchMenuActionToMainWindow: () => Effect.die("unexpected menu action"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
),
Expand Down
52 changes: 45 additions & 7 deletions apps/desktop/src/electron/ElectronMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ 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(),
}),
);
const {
buildFromTemplateMock,
createFromNamedImageMock,
sendActionToFirstResponderMock,
setApplicationMenuMock,
} = vi.hoisted(() => ({
buildFromTemplateMock: vi.fn(),
createFromNamedImageMock: vi.fn(),
sendActionToFirstResponderMock: vi.fn(),
setApplicationMenuMock: vi.fn(),
}));

vi.mock("electron", () => ({
Menu: {
buildFromTemplate: buildFromTemplateMock,
sendActionToFirstResponder: sendActionToFirstResponderMock,
setApplicationMenu: setApplicationMenuMock,
},
nativeImage: {
Expand All @@ -41,6 +46,7 @@ describe("ElectronMenu", () => {
beforeEach(() => {
buildFromTemplateMock.mockReset();
createFromNamedImageMock.mockReset();
sendActionToFirstResponderMock.mockReset();
setApplicationMenuMock.mockReset();
});

Expand Down Expand Up @@ -160,6 +166,38 @@ describe("ElectronMenu", () => {
}).pipe(Effect.provide(TestLayer)),
);

it.effect("sends native actions to the first responder", () =>
Effect.gen(function* () {
const electronMenu = yield* ElectronMenu.ElectronMenu;
yield* electronMenu.sendActionToFirstResponder("performClose:");

assert.deepEqual(sendActionToFirstResponderMock.mock.calls, [["performClose:"]]);
}).pipe(Effect.provide(TestLayer)),
);

it.effect("preserves first-responder failures as structured defects", () =>
Effect.gen(function* () {
const cause = new Error("native action failed");
sendActionToFirstResponderMock.mockImplementationOnce(() => {
throw cause;
});

const electronMenu = yield* ElectronMenu.ElectronMenu;
const exit = yield* Effect.exit(electronMenu.sendActionToFirstResponder("performClose:"));

assert.equal(exit._tag, "Failure");
if (exit._tag === "Failure") {
const error = Cause.squash(exit.cause);
assert.instanceOf(error, ElectronMenu.ElectronMenuOperationError);
assert.equal(error.operation, "send-action-to-first-responder");
assert.equal(error.platform, "linux");
assert.isNull(error.windowId);
assert.equal(error.itemCount, 0);
assert.strictEqual(error.cause, cause);
}
}).pipe(Effect.provide(TestLayer)),
);

it.effect("preserves popup-template failures with window context", () =>
Effect.gen(function* () {
const cause = new Error("popup failed");
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/electron/ElectronMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ElectronMenuTemplateInput {

const ElectronMenuOperation = Schema.Literals([
"set-application-menu",
"send-action-to-first-responder",
"popup-template",
"show-context-menu",
]);
Expand All @@ -52,6 +53,7 @@ export class ElectronMenu extends Context.Service<
readonly setApplicationMenu: (
template: readonly Electron.MenuItemConstructorOptions[],
) => Effect.Effect<void>;
readonly sendActionToFirstResponder: (action: string) => Effect.Effect<void>;
readonly showContextMenu: (
input: ElectronMenuContextInput,
) => Effect.Effect<Option.Option<string>>;
Expand Down Expand Up @@ -185,6 +187,18 @@ export const make = Effect.gen(function* () {
cause,
}),
}).pipe(Effect.orDie),
sendActionToFirstResponder: (action) =>
Effect.try({
try: () => Electron.Menu.sendActionToFirstResponder(action),
catch: (cause) =>
new ElectronMenuOperationError({
operation: "send-action-to-first-responder",
platform,
windowId: null,
itemCount: 0,
cause,
}),
}).pipe(Effect.orDie),
popupTemplate: (input) =>
input.template.length === 0
? Effect.void
Expand Down
174 changes: 174 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,180 @@ const layer = PreviewManager.layer.pipe(
);
const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError);

describe("resolvePreviewShortcutForwarding", () => {
it("routes macOS Cmd+W through the native close request instead of reinjecting it", () => {
expect(
PreviewManager.resolvePreviewShortcutForwarding("darwin", {
type: "keyDown",
key: "w",
meta: true,
control: false,
shift: false,
alt: false,
code: "KeyW",
}),
).toBe("native-close");
});

it("recognizes physical Cmd+W when the logical key differs", () => {
expect(
PreviewManager.resolvePreviewShortcutForwarding("darwin", {
type: "keyDown",
key: "z",
code: "KeyW",
meta: true,
control: false,
shift: false,
alt: false,
}),
).toBe("native-close");
});

it("forwards platform mod shortcuts to the renderer on every desktop platform", () => {
expect(
PreviewManager.resolvePreviewShortcutForwarding("darwin", {
type: "keyDown",
key: "k",
meta: true,
control: false,
shift: false,
alt: false,
}),
).toBe("renderer");
expect(
PreviewManager.resolvePreviewShortcutForwarding("linux", {
type: "keyDown",
key: "w",
meta: false,
control: true,
shift: false,
alt: false,
}),
).toBe("renderer");
expect(
PreviewManager.resolvePreviewShortcutForwarding("win32", {
type: "keyDown",
key: "w",
meta: false,
control: true,
shift: false,
alt: false,
}),
).toBe("renderer");
});

it("ignores wrong-modifier, modified, and key-up events", () => {
expect(
PreviewManager.resolvePreviewShortcutForwarding("linux", {
type: "keyDown",
key: "w",
meta: true,
control: false,
shift: false,
alt: false,
}),
).toBeNull();
expect(
PreviewManager.resolvePreviewShortcutForwarding("darwin", {
type: "keyDown",
key: "w",
meta: true,
control: false,
shift: false,
alt: true,
}),
).toBeNull();
expect(
PreviewManager.resolvePreviewShortcutForwarding("darwin", {
type: "keyUp",
key: "w",
meta: true,
control: false,
shift: false,
alt: false,
}),
).toBeNull();
});
});

describe("forwardPreviewShortcut", () => {
const macCloseInput = {
type: "keyDown",
key: "w",
code: "KeyW",
meta: true,
control: false,
shift: false,
alt: false,
};

it("prevents and dispatches macOS preview close without synthetic reinjection", () => {
const preventDefault = vi.fn();
const dispatchCloseAction = vi.fn();
const closeWindow = vi.fn();
const sendInputEvent = vi.fn();

expect(
PreviewManager.forwardPreviewShortcut(
{ platform: "darwin", input: macCloseInput, rendererReady: true },
{ preventDefault, dispatchCloseAction, closeWindow, sendInputEvent },
),
).toBe(true);
expect(preventDefault).toHaveBeenCalledOnce();
expect(dispatchCloseAction).toHaveBeenCalledOnce();
expect(closeWindow).not.toHaveBeenCalled();
expect(sendInputEvent).not.toHaveBeenCalled();
});

it("falls back to native window close when the renderer is not ready", () => {
const preventDefault = vi.fn();
const dispatchCloseAction = vi.fn();
const closeWindow = vi.fn();
const sendInputEvent = vi.fn();

expect(
PreviewManager.forwardPreviewShortcut(
{ platform: "darwin", input: macCloseInput, rendererReady: false },
{ preventDefault, dispatchCloseAction, closeWindow, sendInputEvent },
),
).toBe(true);
expect(preventDefault).toHaveBeenCalledOnce();
expect(dispatchCloseAction).not.toHaveBeenCalled();
expect(closeWindow).toHaveBeenCalledOnce();
expect(sendInputEvent).not.toHaveBeenCalled();
});

it("reinjects a physical Ctrl+W as the close key on non-QWERTY layouts", () => {
const sendInputEvent = vi.fn();

expect(
PreviewManager.forwardPreviewShortcut(
{
platform: "linux",
input: {
...macCloseInput,
key: "z",
meta: false,
control: true,
},
rendererReady: true,
},
{
preventDefault: vi.fn(),
dispatchCloseAction: vi.fn(),
closeWindow: vi.fn(),
sendInputEvent,
},
),
).toBe(true);
expect(sendInputEvent).toHaveBeenCalledWith({
type: "keyDown",
keyCode: "w",
modifiers: ["control"],
});
});
});

const withManager = <A>(
use: (
manager: PreviewManager.PreviewManager["Service"],
Expand Down
Loading
Loading