mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
* fix(terminal): handle select-all in Kitty TUIs * fix(terminal): preserve popout native selection
330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const {
|
|
fromIdMock,
|
|
fromWebContentsMock,
|
|
getAllWebContentsMock,
|
|
getAllWindowsMock,
|
|
handleMock,
|
|
onMock,
|
|
removeAllListenersMock
|
|
} = vi.hoisted(() => ({
|
|
fromIdMock: vi.fn(),
|
|
fromWebContentsMock: vi.fn(),
|
|
getAllWebContentsMock: vi.fn(),
|
|
getAllWindowsMock: vi.fn(() => []),
|
|
handleMock: vi.fn(),
|
|
onMock: vi.fn(),
|
|
removeAllListenersMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('electron', () => ({
|
|
BrowserWindow: {
|
|
fromWebContents: fromWebContentsMock,
|
|
getAllWindows: getAllWindowsMock
|
|
},
|
|
ipcMain: {
|
|
handle: handleMock,
|
|
on: onMock,
|
|
removeAllListeners: removeAllListenersMock
|
|
},
|
|
webContents: {
|
|
fromId: fromIdMock,
|
|
getAllWebContents: getAllWebContentsMock
|
|
}
|
|
}))
|
|
|
|
import {
|
|
clearTrustedUIRendererWebContentsId,
|
|
getTrustedUIRendererWindow,
|
|
registerUIHandlers,
|
|
sendToTrustedUIRenderer,
|
|
setTrustedUIRendererWebContentsId
|
|
} from './ui'
|
|
|
|
function makeStore() {
|
|
return {
|
|
onUIChanged: vi.fn(),
|
|
getUI: vi.fn(() => ({})),
|
|
updateUI: vi.fn(),
|
|
recordFeatureInteraction: vi.fn()
|
|
}
|
|
}
|
|
|
|
function makeUIEvent(senderOverrides: Record<string, unknown> = {}): {
|
|
sender: Record<string, unknown>
|
|
} {
|
|
return {
|
|
sender: {
|
|
id: 17,
|
|
getType: () => 'window',
|
|
getURL: () => 'file:///orca/index.html',
|
|
isDestroyed: () => false,
|
|
...senderOverrides
|
|
}
|
|
}
|
|
}
|
|
|
|
function getNativePasteHandler():
|
|
| ((event: ReturnType<typeof makeUIEvent>, options?: { mode?: unknown }) => void)
|
|
| undefined {
|
|
return onMock.mock.calls.find(([channel]) => channel === 'ui:performNativePaste')?.[1]
|
|
}
|
|
|
|
function getNativeSelectionActionHandler():
|
|
| ((event: ReturnType<typeof makeUIEvent>, action: unknown) => void)
|
|
| undefined {
|
|
return onMock.mock.calls.find(([channel]) => channel === 'ui:performNativeSelectionAction')?.[1]
|
|
}
|
|
|
|
describe('UI IPC', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('ELECTRON_RENDERER_URL', '')
|
|
fromIdMock.mockReset()
|
|
fromWebContentsMock.mockReset()
|
|
getAllWebContentsMock.mockReset()
|
|
getAllWebContentsMock.mockReturnValue([])
|
|
getAllWindowsMock.mockReset()
|
|
getAllWindowsMock.mockReturnValue([])
|
|
handleMock.mockReset()
|
|
onMock.mockReset()
|
|
removeAllListenersMock.mockReset()
|
|
setTrustedUIRendererWebContentsId(null)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('sends app events once to the trusted renderer without waking 100 browser guests', () => {
|
|
const rendererSend = vi.fn()
|
|
const guestSends = Array.from({ length: 100 }, () => vi.fn())
|
|
getAllWebContentsMock.mockReturnValue(
|
|
guestSends.map((send, index) => ({
|
|
id: index + 100,
|
|
isDestroyed: () => false,
|
|
send
|
|
}))
|
|
)
|
|
fromIdMock.mockReturnValue({ id: 17, isDestroyed: () => false, send: rendererSend })
|
|
setTrustedUIRendererWebContentsId(17)
|
|
|
|
sendToTrustedUIRenderer('gh:prRefreshEvent', { sequence: 1 })
|
|
|
|
expect(fromIdMock).toHaveBeenCalledOnce()
|
|
expect(fromIdMock).toHaveBeenCalledWith(17)
|
|
expect(rendererSend).toHaveBeenCalledOnce()
|
|
expect(rendererSend).toHaveBeenCalledWith('gh:prRefreshEvent', { sequence: 1 })
|
|
expect(getAllWebContentsMock).not.toHaveBeenCalled()
|
|
expect(guestSends.reduce((total, send) => total + send.mock.calls.length, 0)).toBe(0)
|
|
})
|
|
|
|
it('resolves only the BrowserWindow that owns the trusted renderer', () => {
|
|
const renderer = { id: 17, isDestroyed: () => false }
|
|
const mainWindow = { id: 'main' }
|
|
fromIdMock.mockReturnValue(renderer)
|
|
fromWebContentsMock.mockReturnValue(mainWindow)
|
|
setTrustedUIRendererWebContentsId(17)
|
|
|
|
expect(getTrustedUIRendererWindow()).toBe(mainWindow)
|
|
expect(fromWebContentsMock).toHaveBeenCalledWith(renderer)
|
|
|
|
fromIdMock.mockReturnValue({ id: 17, isDestroyed: () => true })
|
|
expect(getTrustedUIRendererWindow()).toBeNull()
|
|
})
|
|
|
|
it('skips missing, destroyed, and originating renderers', () => {
|
|
const rendererSend = vi.fn()
|
|
setTrustedUIRendererWebContentsId(17)
|
|
|
|
fromIdMock.mockReturnValueOnce(undefined)
|
|
sendToTrustedUIRenderer('gh:workItemMutated', { number: 7 })
|
|
|
|
fromIdMock.mockReturnValueOnce({ id: 17, isDestroyed: () => true, send: rendererSend })
|
|
sendToTrustedUIRenderer('gh:workItemMutated', { number: 7 })
|
|
|
|
sendToTrustedUIRenderer('gh:workItemMutated', { number: 7 }, 17)
|
|
|
|
expect(fromIdMock).toHaveBeenCalledTimes(2)
|
|
expect(rendererSend).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('routes to a reopened window without retaining the closed renderer', () => {
|
|
const oldRendererSend = vi.fn()
|
|
const newRendererSend = vi.fn()
|
|
fromIdMock.mockImplementation((id) =>
|
|
id === 17
|
|
? { id, isDestroyed: () => false, send: oldRendererSend }
|
|
: { id, isDestroyed: () => false, send: newRendererSend }
|
|
)
|
|
|
|
setTrustedUIRendererWebContentsId(17)
|
|
sendToTrustedUIRenderer('gh:prRefreshEvent', { sequence: 1 })
|
|
|
|
setTrustedUIRendererWebContentsId(42)
|
|
clearTrustedUIRendererWebContentsId(17)
|
|
sendToTrustedUIRenderer('gh:prRefreshEvent', { sequence: 2 })
|
|
|
|
clearTrustedUIRendererWebContentsId(42)
|
|
sendToTrustedUIRenderer('gh:prRefreshEvent', { sequence: 3 })
|
|
|
|
expect(oldRendererSend).toHaveBeenCalledTimes(1)
|
|
expect(newRendererSend).toHaveBeenCalledTimes(1)
|
|
expect(newRendererSend).toHaveBeenCalledWith('gh:prRefreshEvent', { sequence: 2 })
|
|
})
|
|
|
|
it('routes native paste fallback to the requesting webContents only', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
const event = makeUIEvent()
|
|
const sender = event.sender
|
|
setTrustedUIRendererWebContentsId(17)
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
expect(removeAllListenersMock).toHaveBeenCalledWith('ui:performNativePaste')
|
|
const nativePasteHandler = getNativePasteHandler()
|
|
nativePasteHandler?.(event)
|
|
nativePasteHandler?.(event, { mode: 'paste-and-match-style' })
|
|
|
|
expect(fromWebContentsMock).toHaveBeenCalledWith(sender)
|
|
expect(paste).toHaveBeenCalledTimes(1)
|
|
expect(pasteAndMatchStyle).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('routes native selection fallbacks to the requesting webContents', () => {
|
|
const copy = vi.fn()
|
|
const selectAll = vi.fn()
|
|
const event = makeUIEvent()
|
|
setTrustedUIRendererWebContentsId(17)
|
|
fromWebContentsMock.mockReturnValue({ webContents: { copy, selectAll } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
expect(removeAllListenersMock).toHaveBeenCalledWith('ui:performNativeSelectionAction')
|
|
const handler = getNativeSelectionActionHandler()
|
|
handler?.(event, 'copy')
|
|
handler?.(event, 'select-all')
|
|
handler?.(event, 'invalid')
|
|
|
|
expect(copy).toHaveBeenCalledOnce()
|
|
expect(selectAll).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('allows native selection fallback from the exact dashboard popout renderer', () => {
|
|
const copy = vi.fn()
|
|
const selectAll = vi.fn()
|
|
const event = makeUIEvent({ id: 42 })
|
|
const isDashboardPopoutRenderer = vi.fn((sender: unknown) => sender === event.sender)
|
|
fromWebContentsMock.mockReturnValue({ webContents: { copy, selectAll } })
|
|
|
|
registerUIHandlers(makeStore() as never, { isDashboardPopoutRenderer })
|
|
|
|
const handler = getNativeSelectionActionHandler()
|
|
handler?.(event, 'copy')
|
|
handler?.(event, 'select-all')
|
|
handler?.(makeUIEvent({ id: 43 }), 'copy')
|
|
|
|
expect(isDashboardPopoutRenderer).toHaveBeenCalledWith(event.sender)
|
|
expect(copy).toHaveBeenCalledOnce()
|
|
expect(selectAll).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('ignores native paste fallback from stale or browser senders', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
setTrustedUIRendererWebContentsId(17)
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
const nativePasteHandler = getNativePasteHandler()
|
|
nativePasteHandler?.(makeUIEvent({ id: 42 }))
|
|
nativePasteHandler?.(makeUIEvent({ getType: () => 'webview' }))
|
|
|
|
expect(fromWebContentsMock).not.toHaveBeenCalled()
|
|
expect(paste).not.toHaveBeenCalled()
|
|
expect(pasteAndMatchStyle).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores native paste fallback from destroyed senders', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
const nativePasteHandler = getNativePasteHandler()
|
|
nativePasteHandler?.(makeUIEvent({ isDestroyed: () => true }))
|
|
|
|
expect(fromWebContentsMock).not.toHaveBeenCalled()
|
|
expect(paste).not.toHaveBeenCalled()
|
|
expect(pasteAndMatchStyle).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects packaged file-url senders until the main window id is registered', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
const event = makeUIEvent()
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
getNativePasteHandler()?.(event)
|
|
|
|
expect(fromWebContentsMock).not.toHaveBeenCalled()
|
|
expect(paste).not.toHaveBeenCalled()
|
|
expect(pasteAndMatchStyle).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('clears the trusted renderer id without clearing a newer window id', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
setTrustedUIRendererWebContentsId(17)
|
|
clearTrustedUIRendererWebContentsId(42)
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
getNativePasteHandler()?.(makeUIEvent())
|
|
|
|
expect(paste).toHaveBeenCalledTimes(1)
|
|
|
|
clearTrustedUIRendererWebContentsId(17)
|
|
fromWebContentsMock.mockClear()
|
|
paste.mockClear()
|
|
|
|
getNativePasteHandler()?.(makeUIEvent())
|
|
|
|
expect(fromWebContentsMock).not.toHaveBeenCalled()
|
|
expect(paste).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('allows native paste fallback only from the configured dev renderer origin', () => {
|
|
const paste = vi.fn()
|
|
const pasteAndMatchStyle = vi.fn()
|
|
const event = makeUIEvent({ getURL: () => 'http://localhost:5173/workspace' })
|
|
vi.stubEnv('ELECTRON_RENDERER_URL', 'http://localhost:5173')
|
|
fromWebContentsMock.mockReturnValue({ webContents: { paste, pasteAndMatchStyle } })
|
|
|
|
registerUIHandlers(makeStore() as never)
|
|
|
|
const nativePasteHandler = getNativePasteHandler()
|
|
nativePasteHandler?.(event)
|
|
|
|
expect(fromWebContentsMock).toHaveBeenCalledWith(event.sender)
|
|
expect(paste).toHaveBeenCalledTimes(1)
|
|
|
|
fromWebContentsMock.mockClear()
|
|
paste.mockClear()
|
|
nativePasteHandler?.(makeUIEvent({ getURL: () => 'http://127.0.0.1:5173/workspace' }))
|
|
nativePasteHandler?.(makeUIEvent({ getURL: () => 'file:///orca/index.html' }))
|
|
nativePasteHandler?.(makeUIEvent({ getURL: () => 'not a url' }))
|
|
|
|
expect(fromWebContentsMock).not.toHaveBeenCalled()
|
|
expect(paste).not.toHaveBeenCalled()
|
|
expect(pasteAndMatchStyle).not.toHaveBeenCalled()
|
|
})
|
|
})
|