diff --git a/src/main/menu/register-app-menu.test.ts b/src/main/menu/register-app-menu.test.ts index 8433fd93e..e771cae90 100644 --- a/src/main/menu/register-app-menu.test.ts +++ b/src/main/menu/register-app-menu.test.ts @@ -1,9 +1,15 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { buildFromTemplateMock, setApplicationMenuMock, getFocusedWindowMock } = vi.hoisted(() => ({ +const { + buildFromTemplateMock, + setApplicationMenuMock, + getFocusedWindowMock, + sendActionToFirstResponderMock +} = vi.hoisted(() => ({ buildFromTemplateMock: vi.fn(), setApplicationMenuMock: vi.fn(), - getFocusedWindowMock: vi.fn() + getFocusedWindowMock: vi.fn(), + sendActionToFirstResponderMock: vi.fn() })) vi.mock('electron', () => ({ @@ -12,7 +18,8 @@ vi.mock('electron', () => ({ }, Menu: { buildFromTemplate: buildFromTemplateMock, - setApplicationMenu: setApplicationMenuMock + setApplicationMenu: setApplicationMenuMock, + sendActionToFirstResponder: sendActionToFirstResponderMock }, app: { name: 'Orca' @@ -70,9 +77,14 @@ describe('registerAppMenu', () => { buildFromTemplateMock.mockReset() setApplicationMenuMock.mockReset() getFocusedWindowMock.mockReset() + sendActionToFirstResponderMock.mockReset() buildFromTemplateMock.mockImplementation((template) => ({ template })) }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('shows reload shortcuts as policy-routed menu hints', () => { registerAppMenu(buildMenuOptions()) @@ -214,23 +226,59 @@ describe('registerAppMenu', () => { expect(paletteItem?.accelerator).toBeUndefined() }) - it('routes Edit > Paste through Orca coordinated paste ownership', () => { - const send = vi.fn() - getFocusedWindowMock.mockReturnValue({ webContents: { send } }) + // Why: pin the platform on every case — CI runs this suite on Linux only, so an + // unpinned test leaves the other platforms' branches entirely uncovered. + it.each(['darwin', 'linux', 'win32'] as const)( + 'routes Edit > Paste through Orca coordinated paste ownership on %s', + (platform) => { + vi.spyOn(process, 'platform', 'get').mockReturnValue(platform) + const send = vi.fn() + getFocusedWindowMock.mockReturnValue({ webContents: { send } }) + registerAppMenu(buildMenuOptions()) + + const editSubmenu = getSubmenu(getTemplate(), 'Edit') + const pasteItem = editSubmenu.find((item) => item.label === 'Paste') + + expect(pasteItem).toBeDefined() + expect(pasteItem?.role).toBeUndefined() + expect(pasteItem?.accelerator).toBe('CmdOrCtrl+V') + + pasteItem?.click?.({} as never, {} as never, {} as never) + + expect(send).toHaveBeenCalledOnce() + expect(send).toHaveBeenCalledWith('ui:appMenuPaste') + expect(sendActionToFirstResponderMock).not.toHaveBeenCalled() + } + ) + + it('routes Edit > Paste to the native first responder once on macOS without a focused window', () => { + vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin') + getFocusedWindowMock.mockReturnValue(null) registerAppMenu(buildMenuOptions()) - const editSubmenu = getSubmenu(getTemplate(), 'Edit') - const pasteItem = editSubmenu.find((item) => item.label === 'Paste') - - expect(pasteItem).toBeDefined() - expect(pasteItem?.role).toBeUndefined() - expect(pasteItem?.accelerator).toBe('CmdOrCtrl+V') - + const pasteItem = getSubmenu(getTemplate(), 'Edit').find((item) => item.label === 'Paste') pasteItem?.click?.({} as never, {} as never, {} as never) - expect(send).toHaveBeenCalledWith('ui:appMenuPaste') + expect(sendActionToFirstResponderMock).toHaveBeenCalledOnce() + expect(sendActionToFirstResponderMock).toHaveBeenCalledWith('paste:') }) + it.each(['linux', 'win32'] as const)( + 'does not invoke the native paste responder on %s without a focused window', + (platform) => { + vi.spyOn(process, 'platform', 'get').mockReturnValue(platform) + getFocusedWindowMock.mockReturnValue(null) + registerAppMenu(buildMenuOptions()) + + const pasteItem = getSubmenu(getTemplate(), 'Edit').find((item) => item.label === 'Paste') + // Why: this case asserts only a negative, so it would pass green if the item vanished. + expect(pasteItem).toBeDefined() + pasteItem?.click?.({} as never, {} as never, {} as never) + + expect(sendActionToFirstResponderMock).not.toHaveBeenCalled() + } + ) + it.runIf(!isMac)('puts Settings and Exit under File on Windows/Linux', () => { registerAppMenu(buildMenuOptions()) diff --git a/src/main/menu/register-app-menu.ts b/src/main/menu/register-app-menu.ts index d1e43d4fb..3bca33268 100644 --- a/src/main/menu/register-app-menu.ts +++ b/src/main/menu/register-app-menu.ts @@ -180,7 +180,17 @@ function buildAndApplyMenu(options: RegisterAppMenuOptions): void { click: () => { // Why: a focused terminal/native-chat pane is not a native editable // control, so raw Electron paste cannot know which Orca surface owns it. - BrowserWindow.getFocusedWindow()?.webContents.send('ui:appMenuPaste') + const focusedWindow = BrowserWindow.getFocusedWindow() + if (focusedWindow) { + focusedWindow.webContents.send('ui:appMenuPaste') + return + } + + // Why: a macOS native panel (open/save, Go to Folder) leaves no focused + // BrowserWindow, so overriding the paste role would strand Cmd+V as a no-op. + if (isMac) { + Menu.sendActionToFirstResponder('paste:') + } } }, { role: 'selectAll' }