fix(browser): let This-device pages open popups and new tabs

The client-hosted webview had no allowpopups, so Electron returned null from
window.open and dropped target=_blank before any handler ran. With popups
allowed, the route guest's popup handler sent every popup to a child window,
so tab-shaped opens now go through the manager's new-tab policy and land as
This-device tabs. OAuth-style popups keep their opener in a child window.
This commit is contained in:
Kelvin Amoaba
2026-11-13 10:24:19 +00:00
parent 67223adb26
commit 49becf1c15
11 changed files with 269 additions and 317 deletions
@@ -3,11 +3,15 @@ import {
normalizeExternalBrowserUrl,
redactKagiSessionToken
} from '../../../../../shared/browser-url'
import { trackBrowserGuestInputGesture } from '../../input/browser-guest-input-gesture'
import {
trackBrowserGuestInputGesture,
type ObservedBrowserGuestGesture
} from '../../input/browser-guest-input-gesture'
import {
isNewBrowserTabPopupIntent,
isBrowserTabPopupRequest,
normalizeBrowserPopupUrl
normalizeBrowserPopupUrl,
type NewBrowserTabPopupRequest
} from '../../popups/browser-popup-new-tab-intent'
import { SAFE_POPUP_WINDOW_OPTIONS } from '../../popups/presentation/browser-popup-window-policy'
import { safeOrigin } from '../../browser-event-origin'
@@ -37,45 +41,16 @@ export abstract class BrowserManagerGuestPopupPolicy extends BrowserManagerNavig
guest.setWindowOpenHandler(({ url, frameName, disposition, postBody, features, referrer }) => {
// Spend activation even on popups so it cannot fund a later tab.
const observed = gesture.consume()
if (this.openNewTabPopup(guest, { url, frameName, disposition, postBody }, observed)) {
return { action: 'deny' }
}
const clicked = observed !== null
// Only the desktop user's own click may bring a popup forward; agents and late opens wait.
const showWhenAppActive = observed?.byAgent !== false
const activateTab = disposition !== 'background-tab'
const agentOpened = observed?.byAgent !== false && this.isAgentUsingGuest(guest.id)
const ownerContext = this.resolvePopupOwnerContext(guest.id)
const browserTabId = ownerContext?.browserTabId ?? null
const browserTabId = this.resolvePopupOwnerContext(guest.id)?.browserTabId ?? null
const externalUrl = normalizeExternalBrowserUrl(url)
const newTabUrl = externalUrl ?? (isLocalFileLinkFromLocalPage(guest, url) ? url : null)
// Opener-dependent requests retain a child window instead of becoming tabs.
if (
ownerContext &&
newTabUrl &&
isNewBrowserTabPopupIntent({ url, frameName, disposition, postBody })
) {
// Scripted opens persist as tabs, so opens without observed input consume a budget.
if (!clicked && !this.tryConsumePageInitiatedTab(ownerContext.rootGuestWebContentsId)) {
this.forwardOrQueuePopupEvent(guest.id, {
origin: safeOrigin(newTabUrl),
action: 'blocked'
})
return { action: 'deny' }
}
// A new tab's download would miss the agent's `download` claim, so save the link here.
if (this.isAwaitingDownloadFrom(guest.id, ownerContext.rootGuestWebContentsId)) {
guest.downloadURL(newTabUrl)
return { action: 'deny' }
}
if (
this.openLinkInOrcaTab(ownerContext.browserTabId, newTabUrl, activateTab, agentOpened)
) {
this.forwardOrQueuePopupEvent(guest.id, {
origin: safeOrigin(newTabUrl),
action: 'opened-in-orca'
})
}
// A vanished renderer must not turn new-tab intent into a native popup.
return { action: 'deny' }
}
// Native popups must not target local files.
const canOpenAsChild = normalizeBrowserPopupUrl(url) !== null
@@ -148,6 +123,47 @@ export abstract class BrowserManagerGuestPopupPolicy extends BrowserManagerNavig
}
}
/** Answers a new-tab request as an Orca tab; false leaves opener-dependent requests to a child window. */
openNewTabPopup(
guest: Electron.WebContents,
request: NewBrowserTabPopupRequest,
observed: ObservedBrowserGuestGesture | null
): boolean {
const ownerContext = this.resolvePopupOwnerContext(guest.id)
const newTabUrl =
normalizeExternalBrowserUrl(request.url) ??
(isLocalFileLinkFromLocalPage(guest, request.url) ? request.url : null)
if (!ownerContext || !newTabUrl || !isNewBrowserTabPopupIntent(request)) {
return false
}
// Scripted opens persist as tabs, so opens without observed input consume a budget.
if (!observed && !this.tryConsumePageInitiatedTab(ownerContext.rootGuestWebContentsId)) {
this.forwardOrQueuePopupEvent(guest.id, { origin: safeOrigin(newTabUrl), action: 'blocked' })
return true
}
// A new tab's download would miss the agent's `download` claim, so save the link here.
if (this.isAwaitingDownloadFrom(guest.id, ownerContext.rootGuestWebContentsId)) {
guest.downloadURL(newTabUrl)
return true
}
const agentOpened = observed?.byAgent !== false && this.isAgentUsingGuest(guest.id)
if (
this.openLinkInOrcaTab(
ownerContext.browserTabId,
newTabUrl,
request.disposition !== 'background-tab',
agentOpened
)
) {
this.forwardOrQueuePopupEvent(guest.id, {
origin: safeOrigin(newTabUrl),
action: 'opened-in-orca'
})
}
// A vanished renderer must not turn new-tab intent into a native popup.
return true
}
requestExternalAppLaunch(
guest: Electron.WebContents,
rawUrl: string | undefined,
@@ -1,10 +1,12 @@
// Keep named and OAuth popups attached to their opener; tab conversion returns null to the page.
export function isNewBrowserTabPopupIntent(details: {
export type NewBrowserTabPopupRequest = {
url: string
frameName: string
disposition: string
postBody?: Electron.PostBody | null
}): boolean {
}
// Keep named and OAuth popups attached to their opener; tab conversion returns null to the page.
export function isNewBrowserTabPopupIntent(details: NewBrowserTabPopupRequest): boolean {
return (
isBrowserTabPopupRequest(details) &&
!isBlankBrowserPopupUrl(details.url) &&
@@ -3,11 +3,15 @@ import '../../../../shared/constants'
import { ORCA_BROWSER_BLANK_URL } from '../../../../shared/browser/browser-blank-page'
import {
isBlankBrowserPopupUrl,
normalizeBrowserPopupUrl
normalizeBrowserPopupUrl,
type NewBrowserTabPopupRequest
} from '../../guest-lifecycle/popups/browser-popup-new-tab-intent'
import { enforceBrowserRouteWebRtcPolicy } from '../../network/egress/browser-route-webrtc-policy'
import { trackBrowserGuestInputGesture } from '../../guest-lifecycle/input/browser-guest-input-gesture'
import type { BrowserGuestInputGesture } from '../../guest-lifecycle/input/browser-guest-input-gesture'
import type {
BrowserGuestInputGesture,
ObservedBrowserGuestGesture
} from '../../guest-lifecycle/input/browser-guest-input-gesture'
import {
registerBrowserRouteGuestPopup,
releaseBrowserRouteGuestPopup
@@ -28,6 +32,12 @@ export type BrowserRouteGuestPopupDependencies = {
getPartitionForSession: (session: Session) => string | null
/** Route guests replace the manager's popup handler, so a denial is otherwise entirely silent. */
reportBlockedPopup?: (input: { openerWebContentsId: number; url: string }) => void
/** The manager's new-tab policy; true when it answered the request. */
openNewTabPopup?: (
source: WebContents,
request: NewBrowserTabPopupRequest,
observed: ObservedBrowserGuestGesture | null
) => boolean
openPopupWindow?: (
options: PopupChildWindowOptions,
targetUrl: string,
@@ -172,13 +182,23 @@ export function createBrowserRouteGuestPopupController(input: {
return (details) => {
const observed = gesture.consume()
const normalized = normalizeBrowserPopupUrl(details?.url)
const permitted = !disposed && normalized !== null && navigationAllowed(normalized)
if (
disposed ||
!normalized ||
popups.size >= MAX_BROWSER_ROUTE_GUEST_POPUPS ||
!navigationAllowed(normalized) ||
!observed
permitted &&
input.dependencies.openNewTabPopup?.(
source,
{
url: details?.url ?? normalized,
frameName: details?.frameName ?? '',
disposition: details?.disposition ?? '',
postBody: details?.postBody
},
observed
)
) {
return { action: 'deny' }
}
if (!permitted || popups.size >= MAX_BROWSER_ROUTE_GUEST_POPUPS || !observed) {
if (!disposed) {
reportBlockedPopup(details?.url)
}
@@ -4,6 +4,7 @@ import type {
BrowserRoutePageOwnerIdentity
} from './browser-route-page-authority'
import type { BrowserRouteSessionRekey } from '../sessions/browser-route-session-state'
import type { BrowserRouteGuestPopupDependencies } from './browser-route-guest-popups'
export type BrowserRouteWebContentsRegistryDependencies = {
getPartitionForSession(session: Session): string | null
@@ -16,5 +17,6 @@ export type BrowserRouteWebContentsRegistryDependencies = {
retirePreparedPagesOwnedByRenderer(rendererWebContentsId: number): number
/** Route-guest popup denials need an explicit notice on the owning page. */
reportBlockedPopup?(input: { openerWebContentsId: number; url: string }): void
openNewTabPopup?: BrowserRouteGuestPopupDependencies['openNewTabPopup']
maxGuests?: number
}
@@ -73,6 +73,7 @@ export class BrowserRouteWebContentsRegistry {
getPartitionForSession: (routeSession) =>
this.dependencies.getPartitionForSession(routeSession),
reportBlockedPopup: (blocked) => this.dependencies.reportBlockedPopup?.(blocked),
openNewTabPopup: this.dependencies.openNewTabPopup,
isPopupNavigationAllowed: (url) => this.navigationAllowed(state, url, false)
})
} catch {
@@ -88,7 +88,9 @@ export const browserRouteWebContentsRegistry = new BrowserRouteWebContentsRegist
retirePreparedPage: (page) => browserRouteSessionRegistry.retirePreparedPage(page),
retirePreparedPagesOwnedByRenderer: (rendererWebContentsId) =>
browserRouteSessionRegistry.retirePreparedPagesOwnedByRenderer(rendererWebContentsId),
reportBlockedPopup: (blocked) => browserManager.reportRouteGuestPopupBlocked(blocked)
reportBlockedPopup: (blocked) => browserManager.reportRouteGuestPopupBlocked(blocked),
openNewTabPopup: (source, request, observed) =>
browserManager.openNewTabPopup(source, request, observed)
})
routeWebContentsRegistryRef.current = browserRouteWebContentsRegistry
@@ -66,6 +66,7 @@ export function createBrowserClientPageWebview(options: {
const webview =
options.createWebview?.() ?? (options.document.createElement('webview') as Electron.WebviewTag)
webview.setAttribute('partition', options.identity.partition)
webview.setAttribute('allowpopups', '')
webview.setAttribute('webpreferences', ORCA_BROWSER_PAGE_GUEST_WEB_PREFERENCES_ATTRIBUTE)
webview.setAttribute('src', 'about:blank')
Object.assign(webview.style, { display: 'flex', width: '100%', height: '100%' })
@@ -1,157 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
createBrowserTabMock,
createWebRuntimeSessionBrowserTabMock,
dispatchEventMock,
runtimeOwner,
storeState
} = vi.hoisted(() => ({
createBrowserTabMock: vi.fn(),
createWebRuntimeSessionBrowserTabMock: vi.fn(),
dispatchEventMock: vi.fn(),
runtimeOwner: { value: null as string | null },
storeState: { value: {} as Record<string, unknown> }
}))
vi.mock('../../store', () => ({
useAppStore: { getState: () => storeState.value }
}))
vi.mock('@/store/index', () => ({
useAppStore: { getState: () => storeState.value }
}))
vi.mock('@/lib/workspace/ownership/worktree-runtime-owner', () => ({
runtimeEnvironmentIdOrLocalUnlessTwin: () => runtimeOwner.value
}))
vi.mock('@/runtime/browser/creation/web-runtime-browser-creation', () => ({
createWebRuntimeSessionBrowserTab: createWebRuntimeSessionBrowserTabMock
}))
vi.mock('@/lib/client-creation-action-error', () => ({
showClientCreationActionError: vi.fn()
}))
vi.mock('@/components/browser-pane/describe-page/live-browser-url-registry', () => ({
rememberLiveBrowserUrl: vi.fn()
}))
vi.mock('./browser-automation-bootstrap-lease', () => ({
acquireBrowserAutomationBootstrapLease: vi.fn()
}))
import { registerBrowserStateIpcBridge } from './browser-state-ipc-bridge'
const noopUnsubscribe = (): void => {}
function captureOpenLinkHandler(): (event: {
browserPageId: string
url: string
activate?: boolean
}) => void {
let handler:
| ((event: { browserPageId: string; url: string; activate?: boolean }) => void)
| null = null
const browserApi = new Proxy(
{
onOpenLinkInOrcaTab: (
callback: (event: { browserPageId: string; url: string; activate?: boolean }) => void
) => {
handler = callback
return noopUnsubscribe
}
} as Record<string, unknown>,
{
get: (target, property) =>
property in target ? target[property as string] : () => noopUnsubscribe
}
)
const api = new Proxy({ browser: browserApi } as Record<string, unknown>, {
get: (target, property) =>
property in target
? target[property as string]
: new Proxy({}, { get: () => () => noopUnsubscribe })
})
vi.stubGlobal('window', { api, dispatchEvent: dispatchEventMock })
registerBrowserStateIpcBridge([], () => false)
if (!handler) {
throw new Error('Expected the bridge to subscribe to browser:open-link-in-orca-tab')
}
return handler
}
function clientHostedStore(handle: Record<string, unknown>): void {
storeState.value = {
browserPagesByWorkspace: {
opener: [
{
id: 'page-1',
workspaceId: 'opener',
worktreeId: 'worktree-1'
}
]
},
browserTabsByWorktree: {
'worktree-1': [{ id: 'opener', sessionProfileId: 'profile-a', sessionPartition: 'persist:a' }]
},
unifiedTabsByWorktree: {
'worktree-1': [
{ id: 'unified-browser', groupId: 'source', contentType: 'browser', entityId: 'opener' }
]
},
groupsByWorktree: {
'worktree-1': [{ id: 'source', tabOrder: ['unified-browser'] }]
},
remoteBrowserPageHandlesByPageId: { 'page-1': handle },
createBrowserTab: createBrowserTabMock,
focusGroup: vi.fn()
}
}
describe('client-hosted link opens on a paired server', () => {
beforeEach(() => {
createBrowserTabMock.mockReset()
createWebRuntimeSessionBrowserTabMock.mockReset()
createWebRuntimeSessionBrowserTabMock.mockResolvedValue(true)
dispatchEventMock.mockReset()
runtimeOwner.value = 'env-paired'
})
it('opens a This device page link through the paired creator instead of dropping it', () => {
clientHostedStore({
environmentId: 'env-paired',
remotePageId: 'remote-1',
placement: { kind: 'client', browserHostClientId: 'this-desktop' }
})
captureOpenLinkHandler()({
browserPageId: 'page-1',
url: 'https://docs.example.com/next',
activate: true
})
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(createWebRuntimeSessionBrowserTabMock).toHaveBeenCalledWith(
expect.objectContaining({
worktreeId: 'worktree-1',
environmentId: 'env-paired',
url: 'https://docs.example.com/next',
profileId: 'profile-a',
clientAfterTabId: 'unified-browser',
focusOnCreate: true,
selectWorktree: false
})
)
expect(createWebRuntimeSessionBrowserTabMock.mock.calls[0]?.[0]).not.toHaveProperty(
'placementPreference'
)
})
it('still drops a streamed remote page instead of creating a local tab', () => {
clientHostedStore({
environmentId: 'env-paired',
remotePageId: 'remote-1',
placement: { kind: 'server' }
})
captureOpenLinkHandler()({
browserPageId: 'page-1',
url: 'https://docs.example.com/streamed'
})
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled()
})
})
@@ -1,113 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { createBrowserTabMock, createWebRuntimeSessionBrowserTabMock, runtimeOwner, storeState } =
vi.hoisted(() => ({
createBrowserTabMock: vi.fn(),
createWebRuntimeSessionBrowserTabMock: vi.fn(),
runtimeOwner: { value: null as string | null },
storeState: { value: {} as Record<string, unknown> }
}))
vi.mock('@/store/index', () => ({
useAppStore: { getState: () => storeState.value }
}))
vi.mock('@/lib/workspace/ownership/worktree-runtime-owner', () => ({
runtimeEnvironmentIdOrLocalUnlessTwin: () => runtimeOwner.value
}))
vi.mock('@/runtime/browser/creation/web-runtime-browser-creation', () => ({
createWebRuntimeSessionBrowserTab: createWebRuntimeSessionBrowserTabMock
}))
vi.mock('@/lib/client-creation-action-error', () => ({
showClientCreationActionError: vi.fn()
}))
import { openLinkInNewBrowserTab } from './open-client-hosted-link-beside-page'
describe('openLinkInNewBrowserTab', () => {
beforeEach(() => {
createBrowserTabMock.mockReset()
createBrowserTabMock.mockReturnValue({ id: 'new-tab', activePageId: null })
createWebRuntimeSessionBrowserTabMock.mockReset()
createWebRuntimeSessionBrowserTabMock.mockResolvedValue(true)
runtimeOwner.value = null
})
it('opens a restored This device page in the background through the paired creator', () => {
runtimeOwner.value = 'env-paired'
storeState.value = {
browserPagesByWorkspace: {
opener: [{ id: 'page-1', workspaceId: 'opener', worktreeId: 'worktree-1' }]
},
browserTabsByWorktree: {
'worktree-1': [{ id: 'opener', sessionProfileId: 'profile-a' }]
},
unifiedTabsByWorktree: {
'worktree-1': [
{ id: 'unified-browser', groupId: 'source', contentType: 'browser', entityId: 'opener' }
]
},
groupsByWorktree: {
'worktree-1': [{ id: 'source', tabOrder: ['unified-browser'] }]
},
remoteBrowserPageHandlesByPageId: {
'page-1': {
environmentId: 'env-paired',
remotePageId: 'remote-1',
restoredClientHosted: true
}
},
createBrowserTab: createBrowserTabMock
}
openLinkInNewBrowserTab({
sourcePageId: 'page-1',
worktreeId: 'worktree-1',
url: 'https://docs.example.com/menu',
activate: false
})
expect(createBrowserTabMock).not.toHaveBeenCalled()
expect(createWebRuntimeSessionBrowserTabMock).toHaveBeenCalledWith(
expect.objectContaining({
worktreeId: 'worktree-1',
environmentId: 'env-paired',
url: 'https://docs.example.com/menu',
profileId: 'profile-a',
clientAfterTabId: 'unified-browser',
focusOnCreate: false,
selectWorktree: false
})
)
expect(createWebRuntimeSessionBrowserTabMock.mock.calls[0]?.[0]).not.toHaveProperty(
'placementPreference'
)
})
it('keeps a local page on createBrowserTab', () => {
storeState.value = {
browserPagesByWorkspace: {
opener: [{ id: 'page-1', workspaceId: 'opener', worktreeId: 'worktree-1' }]
},
browserTabsByWorktree: {
'worktree-1': [{ id: 'opener', sessionProfileId: 'profile-a' }]
},
unifiedTabsByWorktree: {},
groupsByWorktree: {},
remoteBrowserPageHandlesByPageId: {},
createBrowserTab: createBrowserTabMock
}
openLinkInNewBrowserTab({
sourcePageId: 'page-1',
worktreeId: 'worktree-1',
url: 'https://docs.example.com/local',
activate: false
})
expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled()
expect(createBrowserTabMock).toHaveBeenCalledWith(
'worktree-1',
'https://docs.example.com/local',
expect.objectContaining({
activate: false,
sessionProfileId: 'profile-a'
})
)
})
})
@@ -1,4 +1,4 @@
import { showClientCreationActionError } from '@/lib/client-creation-action-error'
import { showClientCreationActionError } from '@/lib/workspace/creation/surface-policy/client-creation-action-error'
import { runtimeEnvironmentIdOrLocalUnlessTwin } from '@/lib/workspace/ownership/worktree-runtime-owner'
import { createWebRuntimeSessionBrowserTab } from '@/runtime/browser/creation/web-runtime-browser-creation'
import { useAppStore } from '@/store/index'
@@ -0,0 +1,178 @@
import { createServer, type Server } from 'node:http'
import type { AddressInfo } from 'node:net'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
import {
launchPairedElectronClient,
type PairedElectronClient
} from './helpers/paired-electron-client'
import { waitForPairedClientWorktree } from './helpers/paired-client-host-session'
import { readHostBrowserPageUrls } from './helpers/host-session-tabs'
// A This-device page opens target=_blank links as This-device tabs and keeps OAuth popups attached to their opener.
const OPENER_HTML = `<!doctype html><title>Opener</title>
<style>body{margin:0}a,button{display:block;width:300px;height:60px;margin:10px}</style>
<a id="blank" href="/destination" target="_blank">blank link</a>
<button id="oauth" onclick="startOauth()">sign in</button>
<script>
window.__oauthMessages = []
addEventListener('message', (event) => window.__oauthMessages.push(String(event.data)))
function startOauth() {
const redirect = encodeURIComponent(location.origin + '/callback')
window.__oauthPopup = window.open('/authorize?client_id=e2e&redirect_uri=' + redirect + '&response_type=code', 'oauth', 'popup,width=480,height=640')
}
</script>`
const CALLBACK_HTML = `<!doctype html><title>Callback</title><script>
window.opener.postMessage('code=abc', '*')
setTimeout(() => window.close(), 100)
</script>`
async function startFixture(): Promise<{ server: Server; origin: string }> {
const server = createServer((request, response) => {
const path = new URL(request.url ?? '/', 'http://127.0.0.1').pathname
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
if (path === '/opener') {
response.end(OPENER_HTML)
} else if (path === '/authorize') {
response.end(`<script>location.replace('/callback?code=abc')</script>`)
} else if (path === '/callback') {
response.end(CALLBACK_HTML)
} else {
response.end('<!doctype html><title>Destination</title>')
}
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
return { server, origin: `http://127.0.0.1:${(server.address() as AddressInfo).port}` }
}
function readPagePlacements(page: Page, worktreeId: string): Promise<Record<string, string>> {
return page.evaluate((worktreeId) => {
const state = window.__store!.getState()
const placements: Record<string, string> = {}
for (const workspace of state.browserTabsByWorktree[worktreeId] ?? []) {
for (const browserPage of state.browserPagesByWorkspace[workspace.id] ?? []) {
const kind = state.remoteBrowserPageHandlesByPageId[browserPage.id]?.placement?.kind
placements[new URL(browserPage.url).pathname] = kind ?? 'none'
}
}
return placements
}, worktreeId)
}
async function showPage(page: Page, worktreeId: string, path: string): Promise<void> {
await page.evaluate(
({ worktreeId, path }) => {
const state = window.__store!.getState()
for (const workspace of state.browserTabsByWorktree[worktreeId] ?? []) {
for (const browserPage of state.browserPagesByWorkspace[workspace.id] ?? []) {
if (new URL(browserPage.url).pathname === path) {
state.focusBrowserTabInWorktree(worktreeId, browserPage.id, { surfacePane: true })
}
}
}
},
{ worktreeId, path }
)
}
function guestEval(client: PairedElectronClient, url: string, code: string): Promise<unknown> {
return client.app.evaluate(
({ webContents }, { url, code }) =>
webContents
.getAllWebContents()
.find((contents) => contents.getType() === 'webview' && contents.getURL() === url)
?.executeJavaScript(code) ?? null,
{ url, code }
)
}
// A real mouse click, so main sees the user's gesture as it would on a desktop.
async function clickInGuest(
client: PairedElectronClient,
url: string,
selector: string
): Promise<void> {
await expect
.poll(() => guestEval(client, url, 'innerWidth > 320 && innerHeight > 200'), {
timeout: 30_000
})
.toBe(true)
await client.app.evaluate(
async ({ webContents }, { url, selector }) => {
const guest = webContents
.getAllWebContents()
.find((contents) => contents.getType() === 'webview' && contents.getURL() === url)!
const { x, y } = (await guest.executeJavaScript(
`(() => { const r = document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect(); return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) } })()`
)) as { x: number; y: number }
guest.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1 })
guest.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1 })
},
{ url, selector }
)
}
test('a This-device page opens new tabs on this device and keeps OAuth popups working', async ({
testRepoPath
}, testInfo) => {
test.setTimeout(300_000)
const { server, origin } = await startFixture()
const host = await launchHeadlessPairedRuntimeHost()
let client: PairedElectronClient | null = null
try {
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
client = await launchPairedElectronClient(host.offer, testInfo, 'This-device popups')
const page = client.page
const worktreeId = await waitForPairedClientWorktree(page)
await page.evaluate(
({ environmentId, worktreeId }) =>
window.__store!.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`),
{ environmentId: client.environmentId, worktreeId }
)
const openerUrl = `${origin}/opener`
await page.evaluate(async (url) => {
const state = window.__store!.getState()
state.setBrowserDefaultUrl(url)
await state.openNewBrowserTabInActiveWorkspace(
state.activeGroupIdByWorktree[state.activeWorktreeId!]!
)
}, openerUrl)
await expect
.poll(() => readPagePlacements(page, worktreeId), { timeout: 60_000 })
.toEqual({ '/opener': 'client' })
await expect
.poll(() => guestEval(client!, openerUrl, 'document.readyState'), { timeout: 60_000 })
.toBe('complete')
await showPage(page, worktreeId, '/opener')
await clickInGuest(client, openerUrl, '#blank')
await expect
.poll(() => readPagePlacements(page, worktreeId), {
timeout: 30_000,
message: 'the target=_blank link did not open a This-device tab'
})
.toEqual({ '/opener': 'client', '/destination': 'client' })
await expect
.poll(() => readHostBrowserPageUrls(host.client, `path:${testRepoPath}`), { timeout: 30_000 })
.toContain(`${origin}/destination`)
await showPage(page, worktreeId, '/opener')
await clickInGuest(client, openerUrl, '#oauth')
await expect
.poll(() => guestEval(client!, openerUrl, 'window.__oauthMessages.join()'), {
timeout: 30_000,
message: 'the OAuth popup never reached its opener'
})
.toBe('code=abc')
await expect
.poll(() => guestEval(client!, openerUrl, 'window.__oauthPopup.closed'), { timeout: 10_000 })
.toBe(true)
} finally {
await client?.dispose()
await host.dispose()
server.close()
}
})