mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
1111 lines
35 KiB
TypeScript
1111 lines
35 KiB
TypeScript
/* oxlint-disable max-lines */
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const {
|
|
shellOpenExternalMock,
|
|
browserWindowFromWebContentsMock,
|
|
menuBuildFromTemplateMock,
|
|
guestOffMock,
|
|
guestOnMock,
|
|
guestSetBackgroundThrottlingMock,
|
|
guestSetWindowOpenHandlerMock,
|
|
guestOpenDevToolsMock,
|
|
webContentsFromIdMock,
|
|
screenGetCursorScreenPointMock
|
|
} = vi.hoisted(() => ({
|
|
shellOpenExternalMock: vi.fn(),
|
|
browserWindowFromWebContentsMock: vi.fn(),
|
|
menuBuildFromTemplateMock: vi.fn(),
|
|
guestOffMock: vi.fn(),
|
|
guestOnMock: vi.fn(),
|
|
guestSetBackgroundThrottlingMock: vi.fn(),
|
|
guestSetWindowOpenHandlerMock: vi.fn(),
|
|
guestOpenDevToolsMock: vi.fn(),
|
|
webContentsFromIdMock: vi.fn(),
|
|
screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 }))
|
|
}))
|
|
|
|
vi.mock('electron', () => ({
|
|
BrowserWindow: {
|
|
fromWebContents: browserWindowFromWebContentsMock
|
|
},
|
|
clipboard: { writeText: vi.fn() },
|
|
shell: { openExternal: shellOpenExternalMock },
|
|
Menu: {
|
|
buildFromTemplate: menuBuildFromTemplateMock
|
|
},
|
|
screen: {
|
|
getCursorScreenPoint: screenGetCursorScreenPointMock
|
|
},
|
|
webContents: {
|
|
fromId: webContentsFromIdMock
|
|
}
|
|
}))
|
|
|
|
import { browserManager } from './browser-manager'
|
|
|
|
describe('browserManager', () => {
|
|
const rendererWebContentsId = 5001
|
|
|
|
beforeEach(() => {
|
|
shellOpenExternalMock.mockReset()
|
|
browserWindowFromWebContentsMock.mockReset()
|
|
menuBuildFromTemplateMock.mockReset()
|
|
guestOffMock.mockReset()
|
|
guestOnMock.mockReset()
|
|
guestSetBackgroundThrottlingMock.mockReset()
|
|
guestSetWindowOpenHandlerMock.mockReset()
|
|
guestOpenDevToolsMock.mockReset()
|
|
webContentsFromIdMock.mockReset()
|
|
browserManager.unregisterAll()
|
|
})
|
|
|
|
it('validates popup URLs before opening externally', () => {
|
|
const guest = {
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
|
|
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
|
|
url: string
|
|
}) => { action: 'deny' }
|
|
|
|
expect(handler({ url: 'localhost:3000' })).toEqual({ action: 'deny' })
|
|
expect(handler({ url: 'file:///etc/passwd' })).toEqual({ action: 'deny' })
|
|
|
|
expect(shellOpenExternalMock).toHaveBeenCalledTimes(1)
|
|
expect(shellOpenExternalMock).toHaveBeenCalledWith('http://localhost:3000/')
|
|
})
|
|
|
|
it('routes safe popup URLs into a new Orca browser tab for the owning renderer', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 103,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
|
|
url: string
|
|
}) => { action: 'deny' }
|
|
expect(handler({ url: 'https://example.com/login' })).toEqual({ action: 'deny' })
|
|
|
|
expect(shellOpenExternalMock).not.toHaveBeenCalled()
|
|
expect(rendererSendMock).toHaveBeenCalledWith('browser:open-link-in-orca-tab', {
|
|
browserPageId: 'browser-1',
|
|
url: 'https://example.com/login'
|
|
})
|
|
expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', {
|
|
browserPageId: 'browser-1',
|
|
origin: 'https://example.com',
|
|
action: 'opened-in-orca'
|
|
})
|
|
})
|
|
|
|
it('falls back to opening popup URLs externally before a guest is registered', () => {
|
|
const guest = {
|
|
id: 105,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
|
|
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
|
|
url: string
|
|
}) => { action: 'deny' }
|
|
expect(handler({ url: 'https://example.com/login' })).toEqual({ action: 'deny' })
|
|
|
|
expect(shellOpenExternalMock).toHaveBeenCalledWith('https://example.com/login')
|
|
})
|
|
|
|
it('activates the owning browser workspace when ensuring a page-backed guest is visible', async () => {
|
|
const rendererExecuteJavaScriptMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
prevTabType: 'terminal',
|
|
prevActiveWorktreeId: 'wt-1',
|
|
prevActiveBrowserWorkspaceId: 'workspace-prev',
|
|
prevActiveBrowserPageId: 'page-prev',
|
|
prevFocusedGroupTabId: 'tab-prev',
|
|
targetWorktreeId: 'wt-1',
|
|
targetBrowserWorkspaceId: 'workspace-1',
|
|
targetBrowserPageId: 'page-1'
|
|
})
|
|
.mockResolvedValueOnce(undefined)
|
|
const guest = {
|
|
id: 707,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const renderer = {
|
|
id: rendererWebContentsId,
|
|
isDestroyed: vi.fn(() => false),
|
|
executeJavaScript: rendererExecuteJavaScriptMock
|
|
}
|
|
browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) })
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return renderer
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'page-1',
|
|
workspaceId: 'workspace-1',
|
|
worktreeId: 'wt-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const restore = await browserManager.ensureWebviewVisible(guest.id)
|
|
|
|
const activationScript = rendererExecuteJavaScriptMock.mock.calls[0]?.[0]
|
|
expect(activationScript).toContain('var browserWorkspaceId = "workspace-1";')
|
|
expect(activationScript).toContain('var browserPageId = "page-1";')
|
|
expect(activationScript).toContain('state.setActiveBrowserTab(browserWorkspaceId);')
|
|
expect(activationScript).toContain(
|
|
'state.setActiveBrowserPage(browserWorkspaceId, browserPageId);'
|
|
)
|
|
expect(activationScript).toContain('var targetWorktreeId = "wt-1";')
|
|
|
|
restore()
|
|
})
|
|
|
|
it('restores the previously focused browser workspace after screenshot prep changes tabs', async () => {
|
|
const rendererExecuteJavaScriptMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
prevTabType: 'browser',
|
|
prevActiveWorktreeId: 'wt-prev',
|
|
prevActiveBrowserWorkspaceId: 'workspace-prev',
|
|
prevActiveBrowserPageId: 'page-prev',
|
|
prevFocusedGroupTabId: 'tab-prev',
|
|
targetWorktreeId: 'wt-target',
|
|
targetBrowserWorkspaceId: 'workspace-target',
|
|
targetBrowserPageId: 'page-target'
|
|
})
|
|
.mockResolvedValueOnce(undefined)
|
|
const guest = {
|
|
id: 708,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const renderer = {
|
|
id: rendererWebContentsId,
|
|
isDestroyed: vi.fn(() => false),
|
|
executeJavaScript: rendererExecuteJavaScriptMock
|
|
}
|
|
browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) })
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return renderer
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'page-target',
|
|
workspaceId: 'workspace-target',
|
|
worktreeId: 'wt-target',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const restore = await browserManager.ensureWebviewVisible(guest.id)
|
|
restore()
|
|
|
|
const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0]
|
|
expect(restoreScript).toContain('state.setActiveWorktree("wt-prev");')
|
|
expect(restoreScript).toContain('state.setActiveBrowserTab("workspace-prev");')
|
|
})
|
|
|
|
it('restores the previously active page when screenshot prep switches pages inside one workspace', async () => {
|
|
const rendererExecuteJavaScriptMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
prevTabType: 'browser',
|
|
prevActiveWorktreeId: 'wt-target',
|
|
prevActiveBrowserWorkspaceId: 'workspace-target',
|
|
prevActiveBrowserPageId: 'page-prev',
|
|
prevFocusedGroupTabId: null,
|
|
targetWorktreeId: 'wt-target',
|
|
targetBrowserWorkspaceId: 'workspace-target',
|
|
targetBrowserPageId: 'page-target'
|
|
})
|
|
.mockResolvedValueOnce(undefined)
|
|
const guest = {
|
|
id: 709,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const renderer = {
|
|
id: rendererWebContentsId,
|
|
isDestroyed: vi.fn(() => false),
|
|
executeJavaScript: rendererExecuteJavaScriptMock
|
|
}
|
|
browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) })
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return renderer
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'page-target',
|
|
workspaceId: 'workspace-target',
|
|
worktreeId: 'wt-target',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const restore = await browserManager.ensureWebviewVisible(guest.id)
|
|
restore()
|
|
|
|
const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0]
|
|
expect(restoreScript).toContain('state.setActiveBrowserPage(')
|
|
expect(restoreScript).toContain('"workspace-target"')
|
|
expect(restoreScript).toContain('"page-prev"')
|
|
})
|
|
|
|
it('restores remembered browser workspace/page even when the visible pane was terminal', async () => {
|
|
const rendererExecuteJavaScriptMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
prevTabType: 'terminal',
|
|
prevActiveWorktreeId: 'wt-target',
|
|
prevActiveBrowserWorkspaceId: 'workspace-prev',
|
|
prevActiveBrowserPageId: 'page-prev',
|
|
prevFocusedGroupTabId: 'tab-prev',
|
|
targetWorktreeId: 'wt-target',
|
|
targetBrowserWorkspaceId: 'workspace-target',
|
|
targetBrowserPageId: 'page-target'
|
|
})
|
|
.mockResolvedValueOnce(undefined)
|
|
const guest = {
|
|
id: 7091,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const renderer = {
|
|
id: rendererWebContentsId,
|
|
isDestroyed: vi.fn(() => false),
|
|
executeJavaScript: rendererExecuteJavaScriptMock
|
|
}
|
|
browserWindowFromWebContentsMock.mockReturnValue({ isFocused: vi.fn(() => true) })
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return renderer
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'page-target',
|
|
workspaceId: 'workspace-target',
|
|
worktreeId: 'wt-target',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const restore = await browserManager.ensureWebviewVisible(guest.id)
|
|
restore()
|
|
|
|
const restoreScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0]
|
|
expect(restoreScript).toContain('state.setActiveBrowserTab("workspace-prev");')
|
|
expect(restoreScript).toContain('state.setActiveBrowserPage(')
|
|
expect(restoreScript).toContain('"workspace-prev"')
|
|
expect(restoreScript).toContain('"page-prev"')
|
|
expect(restoreScript).toContain('state.activateTab("tab-prev");')
|
|
expect(restoreScript).toContain('state.setActiveTabType("terminal");')
|
|
})
|
|
|
|
it('does not focus the Orca window while preparing a screenshot', async () => {
|
|
const rendererExecuteJavaScriptMock = vi.fn().mockResolvedValueOnce({
|
|
prevTabType: 'terminal',
|
|
prevActiveWorktreeId: 'wt-1',
|
|
prevActiveBrowserWorkspaceId: 'workspace-prev',
|
|
prevActiveBrowserPageId: 'page-prev',
|
|
prevFocusedGroupTabId: 'tab-prev',
|
|
targetWorktreeId: 'wt-1',
|
|
targetBrowserWorkspaceId: 'workspace-1',
|
|
targetBrowserPageId: 'page-1'
|
|
})
|
|
const guest = {
|
|
id: 710,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const renderer = {
|
|
id: rendererWebContentsId,
|
|
isDestroyed: vi.fn(() => false),
|
|
executeJavaScript: rendererExecuteJavaScriptMock
|
|
}
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return renderer
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'page-1',
|
|
workspaceId: 'workspace-1',
|
|
worktreeId: 'wt-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
await browserManager.ensureWebviewVisible(guest.id)
|
|
|
|
expect(browserWindowFromWebContentsMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('offers opening a link in another Orca browser tab from the guest context menu', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 104,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock,
|
|
getURL: vi.fn(() => 'https://example.com'),
|
|
canGoBack: vi.fn(() => false),
|
|
canGoForward: vi.fn(() => false),
|
|
reload: vi.fn()
|
|
}
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const contextMenuHandler = guestOnMock.mock.calls.find(
|
|
([event]) => event === 'context-menu'
|
|
)?.[1] as ((event: unknown, params: Electron.ContextMenuParams) => void) | undefined
|
|
|
|
contextMenuHandler?.({}, { linkURL: 'https://example.com/docs' } as Electron.ContextMenuParams)
|
|
|
|
expect(rendererSendMock).toHaveBeenCalledWith(
|
|
'browser:context-menu-requested',
|
|
expect.objectContaining({
|
|
browserPageId: 'browser-1',
|
|
pageUrl: 'https://example.com',
|
|
linkUrl: 'https://example.com/docs',
|
|
canGoBack: false,
|
|
canGoForward: false
|
|
})
|
|
)
|
|
})
|
|
|
|
it('blocks non-web guest navigations after attach', () => {
|
|
const guest = {
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
|
|
const willNavigateHandler = guestOnMock.mock.calls.find(
|
|
([event]) => event === 'will-navigate'
|
|
)?.[1] as ((event: { preventDefault: () => void }, url: string) => void) | undefined
|
|
|
|
expect(willNavigateHandler).toBeTypeOf('function')
|
|
const preventDefault = vi.fn()
|
|
willNavigateHandler?.({ preventDefault }, 'file:///etc/passwd')
|
|
expect(preventDefault).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('unregisterAll clears tracked guests and context-menu listeners', () => {
|
|
const guest = {
|
|
id: 101,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: 101,
|
|
// Why: registrations now record which renderer owns each guest so main
|
|
// can route load failures back to the correct window instead of dropping
|
|
// them once multiple renderers exist.
|
|
rendererWebContentsId
|
|
})
|
|
browserManager.attachGuestPolicies({ ...guest, id: 102 } as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-2',
|
|
webContentsId: 102,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
browserManager.unregisterAll()
|
|
|
|
expect(browserManager.getGuestWebContentsId('browser-1')).toBeNull()
|
|
expect(browserManager.getGuestWebContentsId('browser-2')).toBeNull()
|
|
expect(guestOffMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects non-webview guest types to prevent privilege escalation', () => {
|
|
// A compromised renderer could send the main window's own webContentsId.
|
|
// registerGuest must reject it because getType() would return 'window',
|
|
// not 'webview'.
|
|
const mainWindowContents = {
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'window'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(mainWindowContents)
|
|
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-evil',
|
|
webContentsId: 1,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
// The guest should NOT be registered
|
|
expect(browserManager.getGuestWebContentsId('browser-evil')).toBeNull()
|
|
// setWindowOpenHandler must NOT have been called on the main window's webContents
|
|
expect(guestSetWindowOpenHandlerMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects registration for guests that never received attach-time policy wiring', () => {
|
|
const guest = {
|
|
id: 777,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: 777,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
expect(browserManager.getGuestWebContentsId('browser-1')).toBeNull()
|
|
expect(menuBuildFromTemplateMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not duplicate guest policy listeners when attach is reported twice', () => {
|
|
const guest = {
|
|
id: 303,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
|
|
expect(guestSetBackgroundThrottlingMock).toHaveBeenCalledTimes(1)
|
|
expect(guestSetWindowOpenHandlerMock).toHaveBeenCalledTimes(1)
|
|
expect(guestOnMock.mock.calls.filter(([event]) => event === 'will-navigate')).toHaveLength(1)
|
|
expect(guestOnMock.mock.calls.filter(([event]) => event === 'will-redirect')).toHaveLength(1)
|
|
})
|
|
|
|
it('replays a queued main-frame load failure after the guest registers', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 404,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock,
|
|
getURL: vi.fn(() => 'http://localhost:3000/')
|
|
}
|
|
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === 404) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return {
|
|
isDestroyed: vi.fn(() => false),
|
|
send: rendererSendMock
|
|
}
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
|
|
const didFailLoadHandler = guestOnMock.mock.calls.find(
|
|
([event]) => event === 'did-fail-load'
|
|
)?.[1] as
|
|
| ((
|
|
event: unknown,
|
|
errorCode: number,
|
|
errorDescription: string,
|
|
validatedUrl: string,
|
|
isMainFrame: boolean
|
|
) => void)
|
|
| undefined
|
|
|
|
expect(didFailLoadHandler).toBeTypeOf('function')
|
|
didFailLoadHandler?.(null, -105, 'Name not resolved', 'http://localhost:3000/', true)
|
|
|
|
expect(rendererSendMock).not.toHaveBeenCalled()
|
|
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: 404,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
expect(rendererSendMock).toHaveBeenCalledTimes(1)
|
|
expect(rendererSendMock).toHaveBeenCalledWith('browser:guest-load-failed', {
|
|
browserPageId: 'browser-1',
|
|
loadError: {
|
|
code: -105,
|
|
description: 'Name not resolved',
|
|
validatedUrl: 'http://localhost:3000/'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('queues permission denials and download requests until the guest registers', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 407,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
const item = {
|
|
pause: vi.fn(),
|
|
getFilename: vi.fn(() => 'report.csv'),
|
|
getTotalBytes: vi.fn(() => 2048),
|
|
getMimeType: vi.fn(() => 'text/csv'),
|
|
getURL: vi.fn(() => 'https://example.com/report.csv')
|
|
}
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.notifyPermissionDenied({
|
|
guestWebContentsId: guest.id,
|
|
permission: 'media',
|
|
rawUrl: 'https://example.com/account'
|
|
})
|
|
browserManager.handleGuestWillDownload({ guestWebContentsId: guest.id, item: item as never })
|
|
|
|
expect(rendererSendMock).not.toHaveBeenCalled()
|
|
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
expect(rendererSendMock).toHaveBeenCalledWith('browser:permission-denied', {
|
|
browserPageId: 'browser-1',
|
|
permission: 'media',
|
|
origin: 'https://example.com'
|
|
})
|
|
expect(rendererSendMock).toHaveBeenCalledWith(
|
|
'browser:download-requested',
|
|
expect.objectContaining({
|
|
browserPageId: 'browser-1',
|
|
filename: 'report.csv',
|
|
origin: 'https://example.com',
|
|
totalBytes: 2048,
|
|
mimeType: 'text/csv'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('retires stale guest mappings when a page re-registers after a process swap', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const oldGuestOnMock = vi.fn()
|
|
const oldGuestOffMock = vi.fn()
|
|
const newGuestOnMock = vi.fn()
|
|
const newGuestOffMock = vi.fn()
|
|
const oldGuest = {
|
|
id: 501,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: oldGuestOnMock,
|
|
off: oldGuestOffMock,
|
|
openDevTools: guestOpenDevToolsMock,
|
|
getURL: vi.fn(() => 'https://old.example')
|
|
}
|
|
const newGuest = {
|
|
id: 502,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: newGuestOnMock,
|
|
off: newGuestOffMock,
|
|
openDevTools: guestOpenDevToolsMock,
|
|
getURL: vi.fn(() => 'https://new.example')
|
|
}
|
|
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === oldGuest.id) {
|
|
return oldGuest
|
|
}
|
|
if (id === newGuest.id) {
|
|
return newGuest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(oldGuest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: oldGuest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(newGuest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: newGuest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const oldDidFailLoadHandler = oldGuestOnMock.mock.calls.find(
|
|
([event]) => event === 'did-fail-load'
|
|
)?.[1] as
|
|
| ((
|
|
event: unknown,
|
|
errorCode: number,
|
|
errorDescription: string,
|
|
validatedUrl: string,
|
|
isMainFrame: boolean
|
|
) => void)
|
|
| undefined
|
|
const newDidFailLoadHandler = newGuestOnMock.mock.calls.find(
|
|
([event]) => event === 'did-fail-load'
|
|
)?.[1] as
|
|
| ((
|
|
event: unknown,
|
|
errorCode: number,
|
|
errorDescription: string,
|
|
validatedUrl: string,
|
|
isMainFrame: boolean
|
|
) => void)
|
|
| undefined
|
|
|
|
oldDidFailLoadHandler?.(null, -105, 'Old guest failed', 'https://old.example', true)
|
|
expect(rendererSendMock).not.toHaveBeenCalled()
|
|
|
|
newDidFailLoadHandler?.(null, -106, 'New guest failed', 'https://new.example', true)
|
|
expect(rendererSendMock).toHaveBeenCalledWith('browser:guest-load-failed', {
|
|
browserPageId: 'browser-1',
|
|
loadError: {
|
|
code: -106,
|
|
description: 'New guest failed',
|
|
validatedUrl: 'https://new.example'
|
|
}
|
|
})
|
|
expect(oldGuestOffMock).toHaveBeenCalled()
|
|
expect(browserManager.getGuestWebContentsId('browser-1')).toBe(newGuest.id)
|
|
})
|
|
|
|
it('does not forward ctrl/cmd+r or readline chords from browser guests', () => {
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 405,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const beforeInputHandler = guestOnMock.mock.calls
|
|
.filter(([event]) => event === 'before-input-event')
|
|
.at(-1)?.[1] as
|
|
| ((event: { preventDefault: () => void }, input: Record<string, unknown>) => void)
|
|
| undefined
|
|
|
|
expect(beforeInputHandler).toBeTypeOf('function')
|
|
|
|
// Why: on Linux, Ctrl is the shortcut modifier, so Ctrl+R is the reload
|
|
// shortcut (not a readline chord). Only test Ctrl+R as a readline passthrough
|
|
// on macOS where Cmd is the modifier and Ctrl+R is genuinely a readline chord.
|
|
const readlineChords = [
|
|
...(process.platform === 'darwin'
|
|
? [
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyR',
|
|
key: 'r',
|
|
meta: false,
|
|
control: true,
|
|
alt: false,
|
|
shift: false
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyU',
|
|
key: 'u',
|
|
meta: false,
|
|
control: true,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyE',
|
|
key: 'e',
|
|
meta: false,
|
|
control: true,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyJ',
|
|
key: 'j',
|
|
meta: false,
|
|
control: true,
|
|
alt: false,
|
|
shift: false
|
|
}
|
|
]
|
|
for (const input of readlineChords) {
|
|
const preventDefault = vi.fn()
|
|
beforeInputHandler?.({ preventDefault }, input)
|
|
expect(preventDefault).not.toHaveBeenCalled()
|
|
}
|
|
|
|
expect(rendererSendMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('forwards browser guest tab shortcuts alongside shared window shortcuts', () => {
|
|
const isDarwin = process.platform === 'darwin'
|
|
const rendererSendMock = vi.fn()
|
|
const guest = {
|
|
id: 406,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock
|
|
}
|
|
|
|
webContentsFromIdMock.mockImplementation((id: number) => {
|
|
if (id === guest.id) {
|
|
return guest
|
|
}
|
|
if (id === rendererWebContentsId) {
|
|
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
|
|
}
|
|
return null
|
|
})
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: guest.id,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
const beforeInputHandler = guestOnMock.mock.calls
|
|
.filter(([event]) => event === 'before-input-event')
|
|
.at(-1)?.[1] as
|
|
| ((event: { preventDefault: () => void }, input: Record<string, unknown>) => void)
|
|
| undefined
|
|
|
|
expect(beforeInputHandler).toBeTypeOf('function')
|
|
|
|
const inputs = [
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyB',
|
|
key: 'b',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: true
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyT',
|
|
key: 't',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyW',
|
|
key: 'w',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'BracketRight',
|
|
key: '}',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: true
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyP',
|
|
key: 'p',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyL',
|
|
key: 'l',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyR',
|
|
key: 'r',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: false
|
|
},
|
|
{
|
|
type: 'keyDown',
|
|
code: 'KeyR',
|
|
key: 'r',
|
|
meta: isDarwin,
|
|
control: !isDarwin,
|
|
alt: false,
|
|
shift: true
|
|
}
|
|
]
|
|
|
|
for (const input of inputs) {
|
|
const preventDefault = vi.fn()
|
|
beforeInputHandler?.({ preventDefault }, input)
|
|
expect(preventDefault).toHaveBeenCalledTimes(1)
|
|
}
|
|
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:newBrowserTab')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:newBrowserTab')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:switchTab', 1)
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:openQuickOpen')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:focusBrowserAddressBar')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(7, 'ui:reloadBrowserPage')
|
|
expect(rendererSendMock).toHaveBeenNthCalledWith(8, 'ui:hardReloadBrowserPage')
|
|
})
|
|
|
|
it('cleans up prior guest listeners before re-registering the same tab', () => {
|
|
const guest = {
|
|
id: 808,
|
|
isDestroyed: vi.fn(() => false),
|
|
getType: vi.fn(() => 'webview'),
|
|
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
|
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
|
on: guestOnMock,
|
|
off: guestOffMock,
|
|
openDevTools: guestOpenDevToolsMock,
|
|
getURL: vi.fn(() => 'https://example.com/'),
|
|
canGoBack: vi.fn(() => false),
|
|
canGoForward: vi.fn(() => false),
|
|
goBack: vi.fn(),
|
|
goForward: vi.fn(),
|
|
reload: vi.fn(),
|
|
executeJavaScript: vi.fn()
|
|
}
|
|
webContentsFromIdMock.mockReturnValue(guest)
|
|
|
|
browserManager.attachGuestPolicies(guest as never)
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: 808,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
guestOffMock.mockClear()
|
|
|
|
browserManager.registerGuest({
|
|
browserPageId: 'browser-1',
|
|
webContentsId: 808,
|
|
rendererWebContentsId
|
|
})
|
|
|
|
expect(guestOffMock).toHaveBeenCalledWith('context-menu', expect.any(Function))
|
|
expect(
|
|
guestOffMock.mock.calls.filter(([eventName]) => eventName === 'before-input-event')
|
|
).toHaveLength(2)
|
|
})
|
|
})
|