orca/src/main/browser/browser-manager.test.ts

2958 lines
98 KiB
TypeScript

/* oxlint-disable max-lines */
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
openPopupWithOriginBarMock,
appGetPathMock,
shellOpenExternalMock,
browserWindowFromWebContentsMock,
menuBuildFromTemplateMock,
guestOffMock,
guestOnMock,
guestSetBackgroundThrottlingMock,
guestSetWindowOpenHandlerMock,
guestOpenDevToolsMock,
webContentsFromIdMock,
screenGetCursorScreenPointMock
} = vi.hoisted(() => ({
appGetPathMock: vi.fn(() => '/downloads'),
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 })),
openPopupWithOriginBarMock: vi.fn()
}))
vi.mock('electron', () => ({
app: {
getPath: appGetPathMock
},
BrowserWindow: {
fromWebContents: browserWindowFromWebContentsMock
},
clipboard: { writeText: vi.fn() },
shell: { openExternal: shellOpenExternalMock },
Menu: {
buildFromTemplate: menuBuildFromTemplateMock
},
screen: {
getCursorScreenPoint: screenGetCursorScreenPointMock
},
webContents: {
fromId: webContentsFromIdMock
}
}))
vi.mock('./popup-origin-bar-window', () => ({
openPopupWithOriginBar: openPopupWithOriginBarMock
}))
import { browserManager } from './browser-manager'
describe('browserManager', () => {
const rendererWebContentsId = 5001
type DownloadItemHandlerState = 'progressing' | 'interrupted' | 'completed' | 'cancelled'
type DownloadItemHandler = (event: Electron.Event, state: DownloadItemHandlerState) => void
function createDownloadItem(
overrides: Partial<Electron.DownloadItem> = {}
): Electron.DownloadItem {
return {
setSavePath: vi.fn(),
on: vi.fn(),
once: vi.fn(),
off: vi.fn(),
cancel: vi.fn(),
getFilename: vi.fn(() => 'report.csv'),
getTotalBytes: vi.fn(() => 2048),
getMimeType: vi.fn(() => 'text/csv'),
getURL: vi.fn(() => 'https://example.com/report.csv'),
getReceivedBytes: vi.fn(() => 0),
...overrides
} as unknown as Electron.DownloadItem
}
function getDownloadItemEventHandler(
item: Electron.DownloadItem,
method: 'on' | 'once',
eventName: string
): DownloadItemHandler | undefined {
const eventMock = item[method] as unknown as {
mock: { calls: [string, DownloadItemHandler][] }
}
return eventMock.mock.calls.find(([event]) => event === eventName)?.[1]
}
beforeEach(() => {
appGetPathMock.mockReset()
appGetPathMock.mockReturnValue('/downloads')
shellOpenExternalMock.mockReset()
browserWindowFromWebContentsMock.mockReset()
menuBuildFromTemplateMock.mockReset()
guestOffMock.mockReset()
guestOnMock.mockReset()
guestSetBackgroundThrottlingMock.mockReset()
guestSetWindowOpenHandlerMock.mockReset()
guestOpenDevToolsMock.mockReset()
webContentsFromIdMock.mockReset()
openPopupWithOriginBarMock.mockReset()
browserManager.unregisterAll()
browserManager.setBrowserGuestStateChangedListener(null)
browserManager.setDictationShortcutForwardingPredicate(null)
browserManager.setSettingsResolver(() => ({}))
})
afterEach(() => {
vi.useRealTimers()
})
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('allows registered safe popup URLs as Electron child windows', () => {
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
features?: string
}) => {
action: 'allow' | 'deny'
overrideBrowserWindowOptions?: unknown
createWindow?: unknown
}
expect(handler({ url: 'about:blank' })).toMatchObject({ action: 'allow' })
const response = handler({
url: 'https://example.com/login',
features: 'alwaysOnTop=yes,frame=no,fullscreen=yes,kiosk=yes,modal=yes,transparent=yes'
})
expect(response.action).toBe('allow')
expect(response.overrideBrowserWindowOptions).toEqual({
alwaysOnTop: false,
closable: true,
focusable: true,
frame: true,
fullscreen: false,
kiosk: false,
modal: false,
movable: true,
opacity: 1,
show: true,
simpleFullscreen: false,
skipTaskbar: false,
titleBarStyle: 'default',
transparent: false,
webPreferences: {
allowRunningInsecureContent: false,
contextIsolation: true,
nodeIntegration: false,
nodeIntegrationInSubFrames: false,
sandbox: true,
webviewTag: false
}
})
// Why: the custom createWindow is what swaps the chrome-less native child
// for Orca's origin-bar window without losing the popup contents.
expect(typeof response.createWindow).toBe('function')
expect(shellOpenExternalMock).not.toHaveBeenCalled()
expect(rendererSendMock).not.toHaveBeenCalled()
})
it('keeps featureless window.open popups in-app for every disposition', () => {
// Regression guard for the reverted #8332: gating the allow on
// disposition === 'new-window' silently broke featureless window.open()
// OAuth flows (disposition 'foreground-tab'), whose returned handle must
// stay live. Disposition is a UX hint, not a trust signal.
const guest = {
id: 140,
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: guest.id,
rendererWebContentsId
})
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
url: string
frameName: string
features: string
disposition: string
}) => { action: 'allow' | 'deny' }
for (const disposition of ['foreground-tab', 'background-tab', 'new-window']) {
expect(
handler({ url: 'https://sso.example.com/auth', frameName: '', features: '', disposition })
).toMatchObject({ action: 'allow' })
}
expect(shellOpenExternalMock).not.toHaveBeenCalled()
})
it('keeps plain links current and routes explicit new-tab gestures to Orca tabs', async () => {
const rendererSendMock = vi.fn()
const executeJavaScriptInIsolatedWorldMock = vi.fn().mockResolvedValue(undefined)
const guest = {
id: 141,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
executeJavaScriptInIsolatedWorld: executeJavaScriptInIsolatedWorldMock
}
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 domReadyHandler = guestOnMock.mock.calls.find(([event]) => event === 'dom-ready')?.[1] as
| (() => void)
| undefined
domReadyHandler?.()
await vi.waitFor(() => expect(executeJavaScriptInIsolatedWorldMock).toHaveBeenCalledTimes(1))
const managerState = browserManager as unknown as {
clickedLinkFrameNameByGuestId: Map<number, string>
}
const clickedLinkFrameName = managerState.clickedLinkFrameNameByGuestId.get(guest.id)
if (!clickedLinkFrameName) {
throw new Error('Expected a private clicked-link frame name')
}
expect(clickedLinkFrameName).toMatch(/^__orca_clicked_link_foreground_/)
expect(executeJavaScriptInIsolatedWorldMock).toHaveBeenCalledWith(
expect.any(Number),
[
expect.objectContaining({
code: expect.stringContaining(
`${JSON.stringify(clickedLinkFrameName)},${process.platform === 'darwin'})`
)
})
],
false
)
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
url: string
frameName: string
}) => { action: 'allow' | 'deny' }
expect(
handler({
url: 'https://docs.example.com/guide',
frameName: clickedLinkFrameName
})
).toEqual({ action: 'deny' })
expect(rendererSendMock).toHaveBeenCalledWith('browser:open-link-in-orca-tab', {
browserPageId: 'browser-1',
url: 'https://docs.example.com/guide'
})
expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', {
browserPageId: 'browser-1',
origin: 'https://docs.example.com',
action: 'opened-in-orca'
})
expect(openPopupWithOriginBarMock).not.toHaveBeenCalled()
expect(shellOpenExternalMock).not.toHaveBeenCalled()
})
it('routes child-frame gestures with one-use tokens', async () => {
const rendererSendMock = vi.fn()
const executeJavaScriptMock = vi.fn().mockResolvedValue(undefined)
const frameOnceMock = vi.fn()
const frame = {
parent: {},
isDestroyed: vi.fn(() => false),
executeJavaScript: executeJavaScriptMock,
once: frameOnceMock,
off: vi.fn()
}
const guest = {
id: 142,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
executeJavaScriptInIsolatedWorld: vi.fn().mockResolvedValue(undefined)
}
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-frame',
webContentsId: guest.id,
rendererWebContentsId
})
const frameCreatedHandler = guestOnMock.mock.calls.find(
([event]) => event === 'frame-created'
)?.[1] as ((event: Electron.Event, details: Electron.FrameCreatedDetails) => void) | undefined
frameCreatedHandler?.({} as Electron.Event, { frame } as never)
const frameDomReadyHandler = frameOnceMock.mock.calls.find(
([event]) => event === 'dom-ready'
)?.[1] as (() => void) | undefined
frameDomReadyHandler?.()
await vi.waitFor(() => expect(executeJavaScriptMock).toHaveBeenCalledTimes(1))
const firstScript = executeJavaScriptMock.mock.calls[0][0] as string
const foregroundFrameName = firstScript.match(
/__orca_clicked_link_iframe_foreground_[0-9a-f-]+/
)?.[0]
if (!foregroundFrameName) {
throw new Error('Expected a private child-frame routing token')
}
expect(firstScript).toContain('installBrowserIframeClickedLinkRouting')
expect(executeJavaScriptMock).toHaveBeenCalledWith(firstScript, false)
const popupHandler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
url: string
frameName: string
}) => { action: string }
expect(
popupHandler({
url: 'https://docs.example.com/from-frame',
frameName: foregroundFrameName
})
).toEqual({ action: 'deny' })
expect(rendererSendMock).toHaveBeenCalledWith('browser:open-link-in-orca-tab', {
browserPageId: 'browser-frame',
url: 'https://docs.example.com/from-frame'
})
await vi.waitFor(() => expect(executeJavaScriptMock).toHaveBeenCalledTimes(2))
const secondScript = executeJavaScriptMock.mock.calls[1][0] as string
expect(secondScript).not.toContain(foregroundFrameName)
})
it('hosts allowed popups in an origin-bar window with inherited guest policies', () => {
const rendererSendMock = vi.fn()
const guestOnceMock = vi.fn()
const guest = {
id: 150,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
once: guestOnceMock,
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 popupContents = {
id: 151,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'window'),
setBackgroundThrottling: vi.fn(),
setWindowOpenHandler: vi.fn(),
on: vi.fn(),
once: vi.fn(),
off: vi.fn()
}
const popupCloseMock = vi.fn()
const popupOnClosedMock = vi.fn()
openPopupWithOriginBarMock.mockReturnValue({
contentWebContents: popupContents,
close: popupCloseMock,
onClosed: popupOnClosedMock
})
const handler = guestSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
url: string
}) => {
action: string
createWindow: (options: Record<string, unknown>) => unknown
}
const response = handler({ url: 'https://sso.example.com/auth?code=SECRET' })
const preCreatedContents = { id: 152 }
const options = { webContents: preCreatedContents, width: 500, height: 600 }
const returned = response.createWindow(options)
expect(openPopupWithOriginBarMock).toHaveBeenCalledWith(
options,
'https://sso.example.com/auth?code=SECRET'
)
expect(returned).toBe(popupContents)
// did-create-window does not fire for createWindow-created children, so
// the popup must get guest policies (nav guards, recursive popup handling)
// attached directly here.
expect(popupContents.setWindowOpenHandler).toHaveBeenCalledTimes(1)
expect(popupContents.setBackgroundThrottling).toHaveBeenCalledWith(false)
expect(popupContents.on.mock.calls.some(([event]) => event === 'dom-ready')).toBe(false)
// The renderer notice carries only the sanitized origin, never the URL.
expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', {
browserPageId: 'browser-1',
origin: 'https://sso.example.com',
action: 'opened-in-orca'
})
// Opener-lifecycle parity: destroying the owning guest closes the popup.
const destroyedCall = guestOnceMock.mock.calls.find(([event]) => event === 'destroyed')
expect(destroyedCall).toBeDefined()
;(destroyedCall as [string, () => void])[1]()
expect(popupCloseMock).toHaveBeenCalledTimes(1)
// Popup windows opened by the popup itself keep the owner context, so the
// recursive handler still routes to the owning browser tab.
const popupHandler = popupContents.setWindowOpenHandler.mock.calls[0][0] as (details: {
url: string
}) => { action: string }
openPopupWithOriginBarMock.mockReturnValue({
contentWebContents: { ...popupContents, id: 153 },
close: vi.fn(),
onClosed: vi.fn()
})
expect(popupHandler({ url: 'https://sso.example.com/step2' })).toMatchObject({
action: 'allow'
})
})
it('blocks unsafe popup URLs for registered guests', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 106,
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: 'allow' | 'deny' }
expect(handler({ url: 'javascript:alert(1)' })).toEqual({ action: 'deny' })
expect(handler({ url: 'file:///etc/passwd' })).toEqual({ action: 'deny' })
expect(handler({ url: 'file:///C:/Users/example/.ssh/id_rsa' })).toEqual({ action: 'deny' })
expect(handler({ url: 'file://server/share/private.txt' })).toEqual({ action: 'deny' })
expect(shellOpenExternalMock).not.toHaveBeenCalled()
expect(rendererSendMock).not.toHaveBeenCalledWith(
'browser:open-link-in-orca-tab',
expect.anything()
)
expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', {
browserPageId: 'browser-1',
origin: 'null',
action: 'blocked'
})
})
it('remembers the registered session profile for a browser page', () => {
const guest = {
id: 104,
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',
workspaceId: 'workspace-1',
worktreeId: 'wt-1',
webContentsId: guest.id,
rendererWebContentsId,
sessionProfileId: 'work'
})
expect(browserManager.getSessionProfileIdForTab('browser-1')).toBe('work')
})
it('tracks offscreen load failures for the owning worktree snapshot', () => {
const stateChanged = vi.fn()
const offscreenGuest = {
id: 605,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'window'),
setBackgroundThrottling: vi.fn(),
setWindowOpenHandler: vi.fn(),
on: vi.fn(),
off: vi.fn(),
getURL: vi.fn(() => 'https://localhost:3443/')
}
webContentsFromIdMock.mockReturnValue(offscreenGuest)
browserManager.setBrowserGuestStateChangedListener(stateChanged)
browserManager.registerOffscreenGuest({
browserPageId: 'offscreen-page',
worktreeId: 'remote-worktree',
webContentsId: offscreenGuest.id
})
const didFailLoad = offscreenGuest.on.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as (
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void
didFailLoad(null, -202, 'Certificate authority invalid', 'https://localhost:3443/', true)
expect(browserManager.getBrowserPageLoadError('offscreen-page')).toEqual({
code: -202,
description: 'Certificate authority invalid',
validatedUrl: 'https://localhost:3443/'
})
expect(stateChanged).toHaveBeenCalledWith('remote-worktree')
})
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('acquires renderer automation visibility without changing active browser state', async () => {
const rendererExecuteJavaScriptMock = vi
.fn()
.mockResolvedValueOnce('lease-1')
.mockResolvedValueOnce(true)
const guest = {
id: 1707,
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-automation',
workspaceId: 'workspace-1',
worktreeId: 'wt-1',
webContentsId: guest.id,
rendererWebContentsId
})
const restore = await browserManager.acquireAutomationVisibility(guest.id)
const acquireScript = rendererExecuteJavaScriptMock.mock.calls[0]?.[0]
expect(acquireScript).toContain('__orcaBrowserAutomationVisibility')
expect(acquireScript).toContain('bridge.acquire("page-automation")')
expect(acquireScript).not.toContain('setActiveBrowserTab')
expect(acquireScript).not.toContain('setActiveTabType')
restore()
const releaseScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0]
expect(releaseScript).toContain('bridge.release("lease-1")')
})
it('returns a no-op automation visibility restore when renderer acquire hangs', async () => {
vi.useFakeTimers()
const rendererExecuteJavaScriptMock = vi.fn().mockReturnValueOnce(new Promise(() => {}))
const guest = {
id: 1708,
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-hung-acquire',
workspaceId: 'workspace-1',
worktreeId: 'wt-1',
webContentsId: guest.id,
rendererWebContentsId
})
const restorePromise = browserManager.acquireAutomationVisibility(guest.id)
await vi.advanceTimersByTimeAsync(2_000)
const restore = await restorePromise
restore()
expect(rendererExecuteJavaScriptMock).toHaveBeenCalledTimes(1)
})
it('releases a delayed automation visibility token after acquire timeout', async () => {
vi.useFakeTimers()
let resolveAcquire: (token: string) => void = () => {}
const acquirePromise = new Promise<string>((resolve) => {
resolveAcquire = resolve
})
const rendererExecuteJavaScriptMock = vi
.fn()
.mockReturnValueOnce(acquirePromise)
.mockResolvedValueOnce(true)
const guest = {
id: 1709,
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-delayed-acquire',
workspaceId: 'workspace-1',
worktreeId: 'wt-1',
webContentsId: guest.id,
rendererWebContentsId
})
const restorePromise = browserManager.acquireAutomationVisibility(guest.id)
await vi.advanceTimersByTimeAsync(2_000)
const restore = await restorePromise
restore()
expect(rendererExecuteJavaScriptMock).toHaveBeenCalledTimes(1)
resolveAcquire('late-lease-1')
await Promise.resolve()
await Promise.resolve()
expect(rendererExecuteJavaScriptMock).toHaveBeenCalledTimes(2)
const releaseScript = rendererExecuteJavaScriptMock.mock.calls[1]?.[0]
expect(releaseScript).toContain('bridge.release("late-lease-1")')
})
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'),
navigationHistory: {
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
})
// Why: attach-before-registration teardown skips unregisterGuest, so global
// cleanup must independently release the private click-routing token.
browserManager.attachGuestPolicies({ ...guest, id: 103 } as never)
browserManager.unregisterAll()
expect(browserManager.getGuestWebContentsId('browser-1')).toBeNull()
expect(browserManager.getGuestWebContentsId('browser-2')).toBeNull()
expect(guestOffMock).toHaveBeenCalled()
const managerState = browserManager as unknown as {
clickedLinkFrameNameByGuestId: Map<number, unknown>
}
expect(managerState.clickedLinkFrameNameByGuestId.size).toBe(0)
})
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)
expect(guestOnMock.mock.calls.filter(([event]) => event === 'did-create-window')).toHaveLength(
1
)
})
it('attaches guest policies to created popup child windows', () => {
const rendererSendMock = vi.fn()
const childSetBackgroundThrottlingMock = vi.fn()
const childSetWindowOpenHandlerMock = vi.fn()
const childOnMock = vi.fn()
const childOffMock = vi.fn()
const childOpenDevToolsMock = vi.fn()
const childGuest = {
id: 4040,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: childSetBackgroundThrottlingMock,
setWindowOpenHandler: childSetWindowOpenHandlerMock,
on: childOnMock,
off: childOffMock,
openDevTools: childOpenDevToolsMock
}
const guest = {
id: 404,
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 didCreateWindowHandler = guestOnMock.mock.calls.find(
([event]) => event === 'did-create-window'
)?.[1] as ((window: { webContents: typeof childGuest }) => void) | undefined
expect(didCreateWindowHandler).toBeTypeOf('function')
didCreateWindowHandler?.({ webContents: childGuest })
expect(childSetBackgroundThrottlingMock).toHaveBeenCalledWith(false)
expect(childSetWindowOpenHandlerMock).toHaveBeenCalledTimes(1)
expect(childOnMock.mock.calls.filter(([event]) => event === 'did-create-window')).toHaveLength(
1
)
expect(childOnMock.mock.calls.filter(([event]) => event === 'will-navigate')).toHaveLength(1)
expect(childOnMock.mock.calls.filter(([event]) => event === 'will-redirect')).toHaveLength(1)
const childWindowOpenHandler = childSetWindowOpenHandlerMock.mock.calls[0][0] as (details: {
url: string
}) => { action: 'allow' | 'deny' }
expect(childWindowOpenHandler({ url: 'https://identity.example.com/login' })).toMatchObject({
action: 'allow'
})
expect(childWindowOpenHandler({ url: 'file:///etc/passwd' })).toEqual({ action: 'deny' })
expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', {
browserPageId: 'browser-1',
origin: 'null',
action: 'blocked'
})
browserManager.notifyPermissionDenied({
guestWebContentsId: childGuest.id,
permission: 'notifications',
rawUrl: 'https://identity.example.com/login'
})
expect(rendererSendMock).toHaveBeenCalledWith('browser:permission-denied', {
browserPageId: 'browser-1',
permission: 'notifications',
origin: 'https://identity.example.com'
})
const childDidFailLoadHandler = childOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as
| ((
event: Electron.Event,
errorCode: number,
errorDescription: string,
validatedURL: string,
isMainFrame: boolean
) => void)
| undefined
childDidFailLoadHandler?.(
{} as Electron.Event,
-105,
'Name not resolved',
'https://identity.example.com/unavailable',
true
)
expect(rendererSendMock).not.toHaveBeenCalledWith(
'browser:guest-load-failed',
expect.anything()
)
const childDownloadItem = createDownloadItem()
browserManager.handleGuestWillDownload({
guestWebContentsId: childGuest.id,
item: childDownloadItem
})
expect(rendererSendMock).toHaveBeenCalledWith(
'browser:download-requested',
expect.objectContaining({ browserPageId: 'browser-1' })
)
const childDownloadDoneHandler = getDownloadItemEventHandler(childDownloadItem, 'once', 'done')
childDownloadDoneHandler?.({} as Electron.Event, 'completed')
const managerState = browserManager as unknown as {
popupOwnerContextByGuestId: Map<number, unknown>
}
expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(true)
const cleanupChildOnMock = vi.fn()
const cleanupChildGuest = {
...childGuest,
id: 4041,
on: cleanupChildOnMock,
off: vi.fn(),
setBackgroundThrottling: vi.fn(),
setWindowOpenHandler: vi.fn()
}
const childDidCreateWindowHandler = childOnMock.mock.calls.find(
([event]) => event === 'did-create-window'
)?.[1] as ((window: { webContents: typeof cleanupChildGuest }) => void) | undefined
childDidCreateWindowHandler?.({ webContents: cleanupChildGuest })
expect(managerState.popupOwnerContextByGuestId.has(cleanupChildGuest.id)).toBe(true)
const cleanupChildWindowOpenHandler = cleanupChildGuest.setWindowOpenHandler.mock
.calls[0][0] as (details: { url: string }) => { action: 'allow' | 'deny' }
expect(
cleanupChildWindowOpenHandler({ url: 'https://identity.example.com/continue' })
).toMatchObject({ action: 'allow' })
const cleanupChildDestroyedHandler = cleanupChildOnMock.mock.calls.find(
([event]) => event === 'destroyed'
)?.[1] as (() => void) | undefined
cleanupChildDestroyedHandler?.()
expect(managerState.popupOwnerContextByGuestId.has(cleanupChildGuest.id)).toBe(false)
const replacementGuest = {
...guest,
id: 405,
on: vi.fn(),
off: vi.fn(),
setBackgroundThrottling: vi.fn(),
setWindowOpenHandler: vi.fn()
}
webContentsFromIdMock.mockImplementation((id: number) => {
if (id === guest.id) {
return guest
}
if (id === replacementGuest.id) {
return replacementGuest
}
if (id === rendererWebContentsId) {
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
}
return null
})
browserManager.attachGuestPolicies(replacementGuest as never)
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: replacementGuest.id,
rendererWebContentsId
})
expect(childWindowOpenHandler({ url: 'https://identity.example.com/next' })).toEqual({
action: 'deny'
})
expect(shellOpenExternalMock).toHaveBeenCalledWith('https://identity.example.com/next')
expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(false)
const childDestroyedHandler = childOnMock.mock.calls.find(
([event]) => event === 'destroyed'
)?.[1] as (() => void) | undefined
childDestroyedHandler?.()
expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(false)
browserManager.unregisterAll()
expect(childOffMock).toHaveBeenCalledWith('did-create-window', expect.any(Function))
expect(childOffMock).toHaveBeenCalledWith('will-navigate', expect.any(Function))
expect(childOffMock).toHaveBeenCalledWith('will-redirect', expect.any(Function))
})
it('cleans attached guest policy state when a guest is destroyed before registration', () => {
const guest = {
id: 304,
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 destroyedHandler = guestOnMock.mock.calls.find(
([event]) => event === 'destroyed'
)?.[1] as (() => void) | undefined
expect(destroyedHandler).toBeTypeOf('function')
destroyedHandler?.()
browserManager.registerGuest({
browserPageId: 'browser-destroyed-before-register',
webContentsId: guest.id,
rendererWebContentsId
})
expect(browserManager.getGuestWebContentsId('browser-destroyed-before-register')).toBeNull()
})
it('fully unregisters stale guests discovered during authorization', () => {
const guest = {
id: 305,
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-stale',
workspaceId: 'workspace-stale',
worktreeId: 'worktree-stale',
sessionProfileId: 'profile-stale',
webContentsId: guest.id,
rendererWebContentsId
})
const internals = browserManager as unknown as {
rendererWebContentsIdByTabId: Map<string, number>
workspaceIdByPageId: Map<string, string>
sessionProfileIdByPageId: Map<string, string | null>
worktreeIdByTabId: Map<string, string>
contextMenuCleanupByTabId: Map<string, () => void>
grabShortcutCleanupByTabId: Map<string, () => void>
shortcutForwardingCleanupByTabId: Map<string, () => void>
}
expect(internals.rendererWebContentsIdByTabId.has('browser-stale')).toBe(true)
expect(internals.workspaceIdByPageId.has('browser-stale')).toBe(true)
expect(internals.sessionProfileIdByPageId.has('browser-stale')).toBe(true)
expect(internals.worktreeIdByTabId.has('browser-stale')).toBe(true)
expect(internals.contextMenuCleanupByTabId.has('browser-stale')).toBe(true)
expect(internals.grabShortcutCleanupByTabId.has('browser-stale')).toBe(true)
expect(internals.shortcutForwardingCleanupByTabId.has('browser-stale')).toBe(true)
webContentsFromIdMock.mockReturnValue(null)
expect(browserManager.getAuthorizedGuest('browser-stale', rendererWebContentsId)).toBeNull()
expect(browserManager.getGuestWebContentsId('browser-stale')).toBeNull()
expect(internals.rendererWebContentsIdByTabId.has('browser-stale')).toBe(false)
expect(internals.workspaceIdByPageId.has('browser-stale')).toBe(false)
expect(internals.sessionProfileIdByPageId.has('browser-stale')).toBe(false)
expect(internals.worktreeIdByTabId.has('browser-stale')).toBe(false)
expect(internals.contextMenuCleanupByTabId.has('browser-stale')).toBe(false)
expect(internals.grabShortcutCleanupByTabId.has('browser-stale')).toBe(false)
expect(internals.shortcutForwardingCleanupByTabId.has('browser-stale')).toBe(false)
expect(guestOffMock).toHaveBeenCalled()
})
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/'
}
})
expect(browserManager.getBrowserPageLoadError('browser-1')).toEqual({
code: -105,
description: 'Name not resolved',
validatedUrl: 'http://localhost:3000/'
})
const didStartNavigationHandler = guestOnMock.mock.calls.find(
([event]) => event === 'did-start-navigation'
)?.[1] as
| ((event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void)
| undefined
didStartNavigationHandler?.(null, 'http://localhost:3000/retry', false, true)
expect(browserManager.getBrowserPageLoadError('browser-1')).toBeNull()
})
it('drops a queued failure when a replacement navigation starts before registration', () => {
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,
getURL: vi.fn(() => 'https://example.com/')
}
webContentsFromIdMock.mockImplementation((id: number) =>
id === guest.id
? guest
: id === rendererWebContentsId
? { isDestroyed: vi.fn(() => false), send: rendererSendMock }
: null
)
browserManager.attachGuestPolicies(guest as never)
const didFailLoad = guestOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as (
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void
const didStartNavigation = guestOnMock.mock.calls.find(
([event]) => event === 'did-start-navigation'
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
didFailLoad(null, -202, 'Certificate authority invalid', 'https://localhost:3443/', true)
didStartNavigation(null, 'https://example.com/', false, true)
expect(
browserManager.registerGuest({
browserPageId: 'browser-late-registration',
webContentsId: guest.id,
rendererWebContentsId
})
).toBe(true)
expect(rendererSendMock).not.toHaveBeenCalledWith(
'browser:guest-load-failed',
expect.anything()
)
expect(browserManager.getBrowserPageLoadError('browser-late-registration')).toBeNull()
})
it('keeps a certificate failure until a real main-frame navigation starts', () => {
const guest = {
id: 405,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
send: vi.fn(),
getURL: vi.fn(() => 'chrome-error://chromewebdata/')
}
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
const didStartNavigation = guestOnMock.mock.calls.find(
([event]) => event === 'did-start-navigation'
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
const didFailLoad = guestOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as (
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void
browserManager.registerGuest({
browserPageId: 'browser-certificate-page',
webContentsId: guest.id,
rendererWebContentsId
})
didFailLoad(null, -202, 'Certificate authority invalid', 'https://localhost:3443/', true)
didStartNavigation(null, 'chrome-error://chromewebdata/', false, true)
expect(browserManager.getBrowserPageLoadError('browser-certificate-page')?.code).toBe(-202)
didStartNavigation(null, 'https://localhost:3443/', false, true)
expect(browserManager.getBrowserPageLoadError('browser-certificate-page')).toBeNull()
})
it('restores a certificate failure when a retry navigation aborts before committing', () => {
const guest = {
id: 406,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
send: vi.fn(),
getURL: vi.fn(() => 'chrome-error://chromewebdata/')
}
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
const didStartNavigation = guestOnMock.mock.calls.find(
([event]) => event === 'did-start-navigation'
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
const didFailLoad = guestOnMock.mock.calls.find(
([event]) => event === 'did-fail-load'
)?.[1] as (
event: unknown,
errorCode: number,
errorDescription: string,
validatedUrl: string,
isMainFrame: boolean
) => void
browserManager.registerGuest({
browserPageId: 'browser-retry-abort-page',
webContentsId: guest.id,
rendererWebContentsId
})
didFailLoad(null, -202, 'Certificate authority invalid', 'https://localhost:3443/', true)
// Retry: the new navigation starts (overlay optimistically cleared)...
didStartNavigation(null, 'https://localhost:3443/', false, true)
expect(browserManager.getBrowserPageLoadError('browser-retry-abort-page')).toBeNull()
// ...then aborts (ERR_ABORTED) before committing, so the error is restored.
didFailLoad(null, -3, 'Aborted', 'https://localhost:3443/', true)
expect(browserManager.getBrowserPageLoadError('browser-retry-abort-page')?.code).toBe(-202)
// A fresh navigation from a non-errored state drops the stash, so a later
// abort cannot resurrect the old error.
didStartNavigation(null, 'https://localhost:3443/', false, true) // stashes -202, clears active
didStartNavigation(null, 'https://example.com/', false, true) // active empty -> drops stash
didFailLoad(null, -3, 'Aborted', 'https://example.com/', true)
expect(browserManager.getBrowserPageLoadError('browser-retry-abort-page')).toBeNull()
})
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 = {
setSavePath: vi.fn(),
on: vi.fn(),
once: vi.fn(),
off: vi.fn(),
cancel: vi.fn(),
getFilename: vi.fn(() => 'report.csv'),
getTotalBytes: vi.fn(() => 2048),
getMimeType: vi.fn(() => 'text/csv'),
getURL: vi.fn(() => 'https://example.com/report.csv'),
getReceivedBytes: vi.fn(() => 0)
}
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',
savePath: join('/downloads', 'report.csv'),
status: 'downloading'
})
)
expect(item.setSavePath).toHaveBeenCalledWith(join('/downloads', 'report.csv'))
})
it('sets the download save path immediately and reports progress and completion', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 408,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock
}
const item = createDownloadItem()
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
})
browserManager.handleGuestWillDownload({ guestWebContentsId: guest.id, item })
expect(item.setSavePath).toHaveBeenCalledWith(join('/downloads', 'report.csv'))
expect(item.on).toHaveBeenCalledWith('updated', expect.any(Function))
expect(item.once).toHaveBeenCalledWith('done', expect.any(Function))
expect(rendererSendMock).toHaveBeenCalledWith(
'browser:download-requested',
expect.objectContaining({
browserPageId: 'browser-1',
filename: 'report.csv',
savePath: join('/downloads', 'report.csv'),
status: 'downloading'
})
)
vi.mocked(item.getReceivedBytes).mockReturnValue(1024)
const updatedHandler = getDownloadItemEventHandler(item, 'on', 'updated')
updatedHandler?.({} as Electron.Event, 'progressing')
expect(rendererSendMock).toHaveBeenCalledWith('browser:download-progress', {
browserPageId: 'browser-1',
downloadId: expect.any(String),
receivedBytes: 1024,
totalBytes: 2048,
state: 'progressing'
})
const doneHandler = getDownloadItemEventHandler(item, 'once', 'done')
doneHandler?.({} as Electron.Event, 'completed')
expect(rendererSendMock).toHaveBeenCalledWith(
'browser:download-finished',
expect.objectContaining({
browserPageId: 'browser-1',
status: 'completed',
savePath: join('/downloads', 'report.csv'),
error: null
})
)
})
it('flushes started and terminal snapshots for downloads that finish before registration', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 409,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock
}
const item = createDownloadItem()
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.handleGuestWillDownload({ guestWebContentsId: guest.id, item })
const doneHandler = getDownloadItemEventHandler(item, 'once', 'done')
doneHandler?.({} as Electron.Event, 'completed')
expect(rendererSendMock).not.toHaveBeenCalled()
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: guest.id,
rendererWebContentsId
})
expect(rendererSendMock.mock.calls.map(([channel]) => channel)).toEqual([
'browser:download-requested',
'browser:download-finished'
])
})
it('drops pending download records when an unregistered guest is destroyed', () => {
const guest = {
id: 412,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock
}
const item = createDownloadItem()
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.handleGuestWillDownload({ guestWebContentsId: guest.id, item })
const managerState = browserManager as unknown as { downloadsById: Map<string, unknown> }
expect(managerState.downloadsById.size).toBe(1)
const destroyedHandler = guestOnMock.mock.calls.find(
([event]) => event === 'destroyed'
)?.[1] as (() => void) | undefined
destroyedHandler?.()
expect(item.cancel).toHaveBeenCalledTimes(1)
expect(managerState.downloadsById.size).toBe(0)
})
it('cancels active downloads when the owning browser tab closes', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 410,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock
}
const item = createDownloadItem()
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
})
browserManager.handleGuestWillDownload({ guestWebContentsId: guest.id, item })
browserManager.unregisterGuest('browser-1')
expect(item.cancel).toHaveBeenCalledTimes(1)
expect(rendererSendMock).toHaveBeenCalledWith(
'browser:download-finished',
expect.objectContaining({
browserPageId: 'browser-1',
status: 'canceled',
error: 'Tab closed before download completed.'
})
)
})
it('reports setSavePath failures without leaving a hidden download running', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 411,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock
}
const item = createDownloadItem({
setSavePath: vi.fn(() => {
throw new Error('cannot set path')
}) as never
})
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
})
browserManager.handleGuestWillDownload({ guestWebContentsId: guest.id, item })
expect(item.cancel).toHaveBeenCalledTimes(1)
expect(rendererSendMock.mock.calls.map(([channel]) => channel)).toEqual([
'browser:download-requested',
'browser:download-finished'
])
expect(rendererSendMock).toHaveBeenCalledWith(
'browser:download-finished',
expect.objectContaining({
status: 'failed',
error: 'Failed to set download destination.'
})
)
})
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.findLast(
([event]) => event === 'before-input-event'
)?.[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.findLast(
([event]) => event === 'before-input-event'
)?.[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: 'PageDown',
key: 'PageDown',
meta: false,
control: true,
alt: false,
shift: false
},
{
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:newTerminalTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:switchTab', 1)
expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:switchTerminalTab', 1)
expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:openQuickOpen')
expect(rendererSendMock).toHaveBeenNthCalledWith(7, 'ui:focusBrowserAddressBar')
expect(rendererSendMock).toHaveBeenNthCalledWith(8, 'ui:reloadBrowserPage')
expect(rendererSendMock).toHaveBeenNthCalledWith(9, 'ui:hardReloadBrowserPage')
})
it('uses customized keybindings when forwarding browser guest shortcuts', () => {
const isDarwin = process.platform === 'darwin'
const primary = { meta: isDarwin, control: !isDarwin }
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
}
webContentsFromIdMock.mockImplementation((id: number) => {
if (id === guest.id) {
return guest
}
if (id === rendererWebContentsId) {
return { isDestroyed: vi.fn(() => false), send: rendererSendMock }
}
return null
})
browserManager.setSettingsResolver(() => ({
keybindings: {
'tab.newBrowser': ['Mod+Alt+B'],
'worktree.quickOpen': ['Mod+Shift+O']
}
}))
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: guest.id,
rendererWebContentsId
})
const beforeInputHandler = guestOnMock.mock.calls.findLast(
([event]) => event === 'before-input-event'
)?.[1] as
| ((event: { preventDefault: () => void }, input: Record<string, unknown>) => void)
| undefined
expect(beforeInputHandler).toBeTypeOf('function')
const defaultBrowserPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: defaultBrowserPreventDefault },
{
type: 'keyDown',
code: 'KeyB',
key: 'b',
...primary,
alt: false,
shift: true
}
)
expect(defaultBrowserPreventDefault).not.toHaveBeenCalled()
const customBrowserPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: customBrowserPreventDefault },
{
type: 'keyDown',
code: 'KeyB',
key: 'b',
...primary,
alt: true,
shift: false
}
)
expect(customBrowserPreventDefault).toHaveBeenCalledTimes(1)
const defaultQuickOpenPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: defaultQuickOpenPreventDefault },
{
type: 'keyDown',
code: 'KeyP',
key: 'p',
...primary,
alt: false,
shift: false
}
)
expect(defaultQuickOpenPreventDefault).not.toHaveBeenCalled()
const customQuickOpenPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: customQuickOpenPreventDefault },
{
type: 'keyDown',
code: 'KeyO',
key: 'o',
...primary,
alt: false,
shift: true
}
)
expect(customQuickOpenPreventDefault).toHaveBeenCalledTimes(1)
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:newBrowserTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:openQuickOpen')
})
it('forwards browser guest Ctrl+Tab keydown and Ctrl release', () => {
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
}
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.findLast(
([event]) => event === 'before-input-event'
)?.[1] as
| ((event: { preventDefault: () => void }, input: Record<string, unknown>) => void)
| undefined
const keyDownPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: keyDownPreventDefault },
{
type: 'keyDown',
code: 'Tab',
key: 'Tab',
meta: false,
control: true,
alt: false,
shift: false
}
)
const keyUpPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: keyUpPreventDefault },
{
type: 'keyUp',
code: 'ControlRight',
key: 'Control',
meta: false,
control: false,
alt: false,
shift: false
}
)
expect(keyDownPreventDefault).toHaveBeenCalledTimes(1)
expect(keyUpPreventDefault).toHaveBeenCalledTimes(1)
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:ctrlTabKeyDown', { shiftKey: false })
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:ctrlTabKeyUp')
})
it('respects disabled browser guest tab-switch bindings', () => {
const rendererSendMock = vi.fn()
const guest = {
id: 408,
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.setSettingsResolver(() => ({
keybindings: {
'tab.previousRecent': [],
'tab.nextTerminal': [],
'tab.previousTerminal': []
}
}))
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'browser-1',
webContentsId: guest.id,
rendererWebContentsId
})
const beforeInputHandler = guestOnMock.mock.calls.findLast(
([event]) => event === 'before-input-event'
)?.[1] as
| ((event: { preventDefault: () => void }, input: Record<string, unknown>) => void)
| undefined
const ctrlTabPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: ctrlTabPreventDefault },
{
type: 'keyDown',
code: 'Tab',
key: 'Tab',
meta: false,
control: true,
alt: false,
shift: false
}
)
const terminalTabPreventDefault = vi.fn()
beforeInputHandler?.(
{ preventDefault: terminalTabPreventDefault },
{
type: 'keyDown',
code: 'PageDown',
key: 'PageDown',
meta: false,
control: true,
alt: false,
shift: false
}
)
expect(ctrlTabPreventDefault).not.toHaveBeenCalled()
expect(terminalTabPreventDefault).not.toHaveBeenCalled()
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:ctrlTabKeyDown', expect.anything())
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:switchTerminalTab', expect.anything())
})
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/'),
navigationHistory: {
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)
})
it('cancels pending anti-detection reattach timers when unregistering a guest', () => {
vi.useFakeTimers()
const debuggerHandlers = new Map<string, () => void>()
const debuggerAttachMock = vi.fn()
const guest = {
id: 809,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
getURL: vi.fn(() => 'https://example.com/'),
debugger: {
isAttached: vi.fn(() => false),
attach: debuggerAttachMock,
sendCommand: vi.fn().mockResolvedValue(undefined),
on: vi.fn((eventName: string, handler: () => void) => {
debuggerHandlers.set(eventName, handler)
}),
off: vi.fn((eventName: string, handler: () => void) => {
if (debuggerHandlers.get(eventName) === handler) {
debuggerHandlers.delete(eventName)
}
})
}
}
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'browser-reattach',
webContentsId: 809,
rendererWebContentsId
})
debuggerHandlers.get('detach')?.()
expect(vi.getTimerCount()).toBe(1)
browserManager.unregisterGuest('browser-reattach')
expect(vi.getTimerCount()).toBe(0)
vi.advanceTimersByTime(500)
expect(debuggerAttachMock).toHaveBeenCalledTimes(1)
})
describe('setViewportOverride', () => {
function makeGuest(id: number): {
guest: Record<string, unknown>
debuggerSendCommand: ReturnType<typeof vi.fn>
debuggerIsAttached: ReturnType<typeof vi.fn>
debuggerAttach: ReturnType<typeof vi.fn>
} {
const debuggerSendCommand = vi.fn().mockResolvedValue(undefined)
const debuggerIsAttached = vi.fn(() => true)
const debuggerAttach = vi.fn()
const guest = {
id,
isDestroyed: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
getUserAgent: vi.fn(
() =>
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/134.0.0.0 Electron/30.0.0 Safari/537.36'
),
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
on: guestOnMock,
off: guestOffMock,
openDevTools: guestOpenDevToolsMock,
executeJavaScriptInIsolatedWorld: vi.fn().mockResolvedValue(true),
debugger: {
isAttached: debuggerIsAttached,
attach: debuggerAttach,
sendCommand: debuggerSendCommand
}
}
return { guest, debuggerSendCommand, debuggerIsAttached, debuggerAttach }
}
it('returns false when the tab is not registered', async () => {
const result = await browserManager.setViewportOverride('missing', {
width: 375,
height: 667,
deviceScaleFactor: 2,
mobile: true
})
expect(result).toBe(false)
})
it('applies device metrics, touch emulation, and a mobile UA for mobile presets', async () => {
const { guest, debuggerSendCommand } = makeGuest(4242)
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'tab-mobile',
webContentsId: guest.id as number,
rendererWebContentsId
})
webContentsFromIdMock.mockReset()
webContentsFromIdMock.mockReturnValue(guest)
const ok = await browserManager.setViewportOverride('tab-mobile', {
width: 375,
height: 667,
deviceScaleFactor: 2,
mobile: true
})
expect(ok).toBe(true)
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setDeviceMetricsOverride', {
width: 375,
height: 667,
deviceScaleFactor: 2,
mobile: true
})
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setTouchEmulationEnabled', {
enabled: true,
maxTouchPoints: 5
})
const uaCall = debuggerSendCommand.mock.calls.find(
(call) => call[0] === 'Emulation.setUserAgentOverride'
)
expect(uaCall).toBeDefined()
expect(uaCall?.[1]).toMatchObject({
userAgent: expect.stringContaining('iPhone')
})
// Chrome major from the guest UA should be spliced into the mobile UA.
expect(uaCall?.[1]).toMatchObject({
userAgent: expect.stringContaining('CriOS/134')
})
})
it('clears device metrics and disables touch for override=null', async () => {
const { guest, debuggerSendCommand } = makeGuest(4343)
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'tab-clear',
webContentsId: guest.id as number,
rendererWebContentsId
})
const ok = await browserManager.setViewportOverride('tab-clear', null)
expect(ok).toBe(true)
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.clearDeviceMetricsOverride', {})
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setTouchEmulationEnabled', {
enabled: false,
maxTouchPoints: 0
})
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', {
userAgent: ''
})
})
it('attaches the debugger if not already attached and does not detach after', async () => {
const { guest, debuggerSendCommand, debuggerAttach } = makeGuest(4444)
;(guest.debugger as { isAttached: ReturnType<typeof vi.fn> }).isAttached = vi.fn(() => false)
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'tab-attach',
webContentsId: guest.id as number,
rendererWebContentsId
})
await browserManager.setViewportOverride('tab-attach', {
width: 1024,
height: 768,
deviceScaleFactor: 1,
mobile: false
})
expect(debuggerAttach).toHaveBeenCalledWith('1.3')
expect(debuggerSendCommand).toHaveBeenCalled()
// Why: detaching would clear Page.addScriptToEvaluateOnNewDocument
// (anti-detection). Guard regression.
expect((guest.debugger as { detach?: unknown }).detach ?? undefined).toBeUndefined()
})
it('returns false when debugger.attach throws (e.g. DevTools already open)', async () => {
const { guest, debuggerSendCommand, debuggerAttach } = makeGuest(4545)
;(guest.debugger as { isAttached: ReturnType<typeof vi.fn> }).isAttached = vi.fn(() => false)
// Why: Electron throws from debugger.attach if another client (e.g. the
// user's open DevTools window) is already attached. setViewportOverride
// must surface this as a clean `false` rather than an unhandled rejection.
debuggerAttach.mockImplementation(() => {
throw new Error('Another debugger is already attached')
})
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'tab-attach-throws',
webContentsId: guest.id as number,
rendererWebContentsId
})
const ok = await browserManager.setViewportOverride('tab-attach-throws', {
width: 1024,
height: 768,
deviceScaleFactor: 1,
mobile: false
})
expect(ok).toBe(false)
expect(debuggerAttach).toHaveBeenCalledWith('1.3')
expect(debuggerSendCommand).not.toHaveBeenCalled()
})
it('installs annotation viewport bridge in an isolated world', async () => {
const { guest } = makeGuest(4646)
webContentsFromIdMock.mockReturnValue(guest)
browserManager.attachGuestPolicies(guest as never)
browserManager.registerGuest({
browserPageId: 'tab-annotations',
webContentsId: guest.id as number,
rendererWebContentsId
})
const ok = await browserManager.setAnnotationViewportBridge('tab-annotations', {
emitViewport: false,
enabled: true,
markers: [],
token: 'annotationviewporttoken'
})
expect(ok).toBe(true)
expect(guest.executeJavaScriptInIsolatedWorld).toHaveBeenCalledWith(
expect.any(Number),
[
expect.objectContaining({
code: expect.stringContaining('__orcaBrowserAnnotationViewportBridge')
})
],
false
)
})
})
})