test(browser): document blocked popup compatibility gates (#30537)

* test(browser): expose native popup admission blockers with faithful app events

* test(browser): probe Electron popup identity and inherited sandbox API limits

* test(browser): handle requests without associated contents in popup probe

* docs(browser): record blocked Electron popup compatibility gates and test routing

* docs(browser): keep popup evidence in PR
This commit is contained in:
Jinjing
2026-10-18 23:00:10 -07:00
committed by GitHub
parent 7395d30dc7
commit d2bb60735b
11 changed files with 853 additions and 111 deletions
+3
View File
@@ -1147,6 +1147,8 @@ jobs:
# probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other
# past those deadlines; serial, every probe owns the runner.
- name: Test Linux Electron lifecycle boundary
env:
ORCA_BACKGROUND_LAUNCH: '1'
run: >-
xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts
--no-file-parallelism
@@ -1157,6 +1159,7 @@ jobs:
src/main/browser/browser-route-dns-prefetch.electron.test.ts
src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
src/main/browser/browser-popup-compatibility.electron.test.ts
src/main/browser/browser-popup-adapter-compatibility.electron.test.ts
src/main/browser/browser-popup-native-compatibility.electron.test.ts
src/main/browser/browser-popup-background-compatibility.electron.test.ts
+1
View File
@@ -43,6 +43,7 @@ export const UNIT_EXCLUDE = [
'src/main/browser/browser-route-dns-prefetch.electron.test.ts',
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
'src/main/browser/browser-popup-compatibility.electron.test.ts',
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
'src/main/browser/browser-popup-native-compatibility.electron.test.ts',
'src/main/browser/browser-popup-background-compatibility.electron.test.ts',
`${CROSS_VERSION_WIRE_DIR}**`
+1
View File
@@ -6,6 +6,7 @@ export const LINUX_PACKAGE_TESTS = [
'src/main/browser/browser-route-dns-prefetch.electron.test.ts',
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
'src/main/browser/browser-popup-compatibility.electron.test.ts',
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
'src/main/browser/browser-popup-native-compatibility.electron.test.ts',
'src/main/browser/browser-popup-background-compatibility.electron.test.ts'
]
@@ -61,6 +61,7 @@ export const NODE_RUNTIME_INCLUDE = [
'src/main/foreign-sqlite-readers/**/*.test.ts',
'src/main/browser/*cookie*.test.ts',
// Playwright's CDP transport needs Node's WebSocket implementation.
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
'src/main/browser/browser-popup-compatibility.electron.test.ts',
'src/main/browser/browser-popup-native-compatibility.electron.test.ts',
'src/main/browser/browser-popup-background-compatibility.electron.test.ts',
@@ -0,0 +1,402 @@
import { describe, expect, it } from 'vitest'
import type { Page } from 'playwright'
import {
browser,
closeChildren,
enablePopupAdapterProbe,
expectedSecurity,
guest,
origin,
receipts,
setPopupFixtureEvidenceName,
snapshots,
status
} from './browser-popup-compatibility-cdp-fixture'
enablePopupAdapterProbe()
setPopupFixtureEvidenceName('adapter-electron-evidence.json')
type CreationPath = 'adopted' | 'native'
const creationPaths = ['adopted', 'native'] as const
function eventsSince(offset: number): Record<string, unknown>[] {
return status().adapterProbe?.slice(offset) ?? []
}
async function childAt(url: string): Promise<Page> {
await expect
.poll(() =>
browser
?.contexts()
.flatMap((context) => context.pages())
.some((candidate) => candidate.url() === url)
)
.toBe(true)
const child = browser
?.contexts()
.flatMap((context) => context.pages())
.find((candidate) => candidate.url() === url)
if (!child) {
throw new Error('Missing probe child')
}
return child
}
async function configure(page: Page, path: CreationPath): Promise<void> {
await page.evaluate((path) => console.log(`orca-popup-fixture:probe-${path}`), path)
}
async function installBurst(page: Page, count = 3): Promise<void> {
await page.evaluate((count) => {
document.querySelector('#identity-burst')?.remove()
const button = document.createElement('button')
button.id = 'identity-burst'
button.onclick = () => {
for (let index = 0; index < count; index++) {
const child = window.open('/identical-child')
if (child) {
child.name = `original-request-${index}`
}
}
}
document.body.append(button)
}, count)
}
function assertObservedCreationOrder(events: Record<string, unknown>[], path: CreationPath): void {
const delivered = events.filter(
(event) => event.event === (path === 'adopted' ? 'createWindow' : 'did-create-window')
)
const admitted = events.filter((event) => event.event === 'admission')
expect(delivered.length).toBe(admitted.length)
for (const [index, event] of delivered.entries()) {
expect(event.owner).toBe(admitted[index].owner)
expect(event.preferences).toMatchObject(expectedSecurity)
if (path === 'adopted') {
expect(event.callbackRequest).toBe(admitted[index].request)
} else {
expect(event.optionsArguments).toEqual([
`--orca-popup-probe=${String(admitted[index].request)}`
])
}
const first = events.findIndex(
(candidate) =>
candidate.event === 'did-start-navigation' && candidate.contentsId === event.contentsId
)
expect(first).toBeGreaterThan(events.indexOf(event))
expect(
events.find(
(candidate) =>
candidate.event === 'before-request' && candidate.contentsId === event.contentsId
)
).toMatchObject({ observedBeforeRequest: true, webRtcPolicy: 'disable_non_proxied_udp' })
}
}
describe('Electron popup adapter API limits (not an admission safety gate)', () => {
it.each(creationPaths)(
'records same-parent identical URLs with foreground, background and window intent: %s',
async (path) => {
expect(status().electronVersion).toBe('43.7.8')
const page = guest('probe-one')
await configure(page, path)
for (const modifier of [null, 'Shift', 'ControlOrMeta'] as const) {
const offset = status().adapterProbe?.length ?? 0
const before = receipts.length
await installBurst(page)
await page.locator('#identity-burst').click(modifier ? { modifiers: [modifier] } : {})
await expect
.poll(
() =>
receipts.slice(before).filter((receipt) => receipt.path === '/identical-child').length
)
.toBe(3)
await expect
.poll(() => eventsSince(offset).filter((event) => event.event === 'loaded').length)
.toBe(3)
const events = eventsSince(offset)
assertObservedCreationOrder(events, path)
expect(
events
.filter((event) => event.event === 'loaded')
.map((event) => event.frameName)
.sort()
).toEqual(['original-request-0', 'original-request-1', 'original-request-2'])
const dispositions = events
.filter((event) => event.event === 'admission')
.map((event) => event.details)
expect(dispositions).toEqual(
Array.from({ length: 3 }, () =>
expect.objectContaining({
disposition:
modifier === 'Shift' ? 'new-window' : modifier ? 'background-tab' : 'foreground-tab'
})
)
)
snapshots.push(status())
await closeChildren(page)
}
}
)
it.each(creationPaths)(
'records two parents sharing a session with distinct simultaneous intent: %s',
async (path) => {
const pages = [guest('probe-one'), guest('probe-two')]
for (const page of pages) {
await configure(page, path)
await installBurst(page, 1)
}
const offset = status().adapterProbe?.length ?? 0
const before = receipts.length
await Promise.all([
pages[0].locator('#identity-burst').click({ modifiers: ['Shift'] }),
pages[1].locator('#identity-burst').click({ modifiers: ['ControlOrMeta'] })
])
await expect
.poll(
() =>
receipts.slice(before).filter((receipt) => receipt.path === '/identical-child').length
)
.toBe(2)
await expect
.poll(() => eventsSince(offset).filter((event) => event.event === 'loaded').length)
.toBe(2)
const events = eventsSince(offset)
const admitted = events.filter((event) => event.event === 'admission')
expect(new Set(admitted.map((event) => event.owner)).size).toBe(2)
// Each parent's delivery is observed independently; ordering across parents is not authority.
for (const owner of admitted.map((event) => event.owner)) {
assertObservedCreationOrder(
events.filter((event) => event.owner === owner || !event.owner),
path
)
}
expect(
receipts
.slice(before)
.filter((receipt) => receipt.path === '/identical-child')
.every((receipt) => receipt.cookie === 'synthetic_session=local')
).toBe(true)
snapshots.push(status())
for (const page of pages) {
await closeChildren(page)
}
}
)
it.each(creationPaths)(
'records immediate close, then reopen without inferring cancellation from delivery order: %s',
async (path) => {
const page = guest('probe-one')
await configure(page, path)
const offset = status().adapterProbe?.length ?? 0
await page.evaluate(() => {
document.querySelector('#cancel-probe')?.remove()
const button = document.createElement('button')
button.id = 'cancel-probe'
button.onclick = () => {
window.open('/cancelled-child')?.close()
const child = window.open('/reopened-child')
if (child) {
child.name = 'reopened-original'
}
}
document.body.append(button)
})
await page.locator('#cancel-probe').click({ modifiers: ['Shift'] })
const reopened = await childAt(`${origin}/reopened-child`)
expect(await reopened.evaluate('window.name')).toBe('reopened-original')
const events = eventsSince(offset)
expect(events.filter((event) => event.event === 'admission')).toHaveLength(2)
expect(
events.filter(
(event) => event.event === (path === 'adopted' ? 'createWindow' : 'did-create-window')
)
).toHaveLength(2)
// Both creation callbacks precede the renderer's close(); this is not a missing-callback cancellation.
snapshots.push(status())
await closeChildren(page)
}
)
it.each(creationPaths)(
'cannot distinguish inherited script restrictions synchronously: %s',
async (path) => {
const records = await exerciseSandboxControls(path, 'script')
expect(records[0].preferences).toEqual(records[1].preferences)
expect(records[0].frameOrigin).toBe(records[1].frameOrigin)
if (path === 'adopted') {
expect(withoutRequestMarker(records[0].optionsPreferences)).toEqual(
withoutRequestMarker(records[1].optionsPreferences)
)
}
}
)
it.each(creationPaths)(
'exposes inherited flags on native OpenURL link options only: %s',
async (path) => {
const records = await exerciseSandboxControls(path, 'link')
const preferences = records.map((record) => record.optionsPreferences)
expect(preferences[0]).toHaveProperty('openerSandboxFlags', expect.any(Number))
expect(preferences[1]).not.toHaveProperty('openerSandboxFlags')
expect(preferences[2]).not.toHaveProperty('openerSandboxFlags')
if (path === 'adopted') {
expect(records.every((record) => record.event === 'createWindow-missing')).toBe(true)
}
}
)
it('records baseline early tab dispatch for the restricted script with zero original-child requests', async () => {
const page = guest('local')
const before = receipts.length
const tabs = status().tabOpens.length
await installSandboxFrame(page, 'restricted', `${origin}/baseline-restricted`, 'script')
await page.frameLocator('#sandbox-probe').locator('#open').click()
await expect.poll(() => status().tabOpens.length).toBe(tabs + 1)
expect(status().tabOpens.at(-1)).toEqual({
url: `${origin}/baseline-restricted`,
activate: true
})
expect(receipts.slice(before).some((receipt) => receipt.path === '/baseline-restricted')).toBe(
false
)
snapshots.push(status())
})
it('reproduces production native admission attaching to an unrelated same-session app window', async () => {
const page = guest('probe-two')
await page.evaluate('console.log("orca-popup-fixture:probe-unrelated")')
await installBurst(page, 1)
const offset = status().adapterProbe?.length ?? 0
await page.locator('#identity-burst').click({ modifiers: ['Shift'] })
await childAt(`${origin}/identical-child`)
const events = eventsSince(offset)
const unrelated = events.find((event) => event.event === 'unrelated-window')
const admitted = events.find((event) => event.event === 'production-adapter-created')
expect(unrelated).toBeDefined()
expect(admitted?.contentsId).toBe(unrelated?.contentsId)
expect(events.filter((event) => event.event === 'production-adapter-created')).toHaveLength(1)
const realChild = events.find((event) => event.event === 'did-create-window')
expect(realChild?.contentsId).not.toBe(admitted?.contentsId)
snapshots.push(status())
await page.evaluate('console.log("orca-popup-fixture:probe-cleanup")')
await closeChildren(page)
})
})
function withoutRequestMarker(value: unknown): unknown {
if (typeof value !== 'object' || value === null) {
return value
}
return Object.fromEntries(
Object.entries(value).filter(([key]) => !['additionalArguments', 'session'].includes(key))
)
}
async function installSandboxFrame(
page: Page,
sandbox: string,
destination: string,
kind: string
): Promise<void> {
await page.evaluate(
({ sandbox, destination, kind }) => {
document.querySelector('#sandbox-probe')?.remove()
const frame = document.createElement('iframe')
frame.id = 'sandbox-probe'
if (sandbox !== 'unsandboxed') {
frame.setAttribute(
'sandbox',
`allow-scripts allow-popups${sandbox === 'escape' ? ' allow-popups-to-escape-sandbox' : ''}`
)
}
frame.srcdoc =
kind === 'script'
? `<button id="open" onclick="window.open('${destination}', '_blank', 'noopener')">Open</button>`
: `<a id="open" href="${destination}" target="_blank" rel="noopener">Open</a>`
document.body.append(frame)
},
{ sandbox, destination, kind }
)
}
async function exerciseSandboxControls(
path: CreationPath,
kind: 'script' | 'link'
): Promise<Record<string, unknown>[]> {
const page = guest('probe-one')
await configure(page, path)
const records: Record<string, unknown>[] = []
for (const sandbox of ['restricted', 'escape', 'unsandboxed']) {
const destination = `${origin}/sandbox-${path}-${kind}-${sandbox}`
const offset = status().adapterProbe?.length ?? 0
await installSandboxFrame(page, sandbox, destination, kind)
const initialReceipt = receipts.length
await page
.frameLocator('#sandbox-probe')
.locator('#open')
.click(kind === 'link' ? { modifiers: ['Shift'] } : {})
if (path === 'adopted' && kind === 'link') {
await expect
.poll(() => eventsSince(offset).some((event) => event.event === 'createWindow-missing'))
.toBe(true)
const creation = eventsSince(offset).find((event) => event.event === 'createWindow-missing')
if (!creation) {
throw new Error('Missing OpenURL options')
}
records.push(creation)
await new Promise((resolve) => setTimeout(resolve, 300))
expect(
receipts.slice(initialReceipt).some((receipt) => destination.endsWith(receipt.path))
).toBe(false)
snapshots.push(status())
await page.evaluate('console.log("orca-popup-fixture:probe-cleanup")')
continue
}
const child = await childAt(destination)
expect(await child.evaluate('window.opener === null')).toBe(true)
const before = receipts.length
await child.evaluate((action) => {
const form = document.createElement('form')
form.action = action
form.method = 'POST'
document.body.append(form)
form.submit()
}, `${origin}/sandbox-form-${path}-${kind}-${sandbox}`)
if (sandbox === 'restricted') {
await expect
.poll(() => eventsSince(offset).some((event) => event.event === 'sandbox-form-blocked'))
.toBe(true)
expect(
receipts.slice(before).some((receipt) => receipt.path.startsWith('/sandbox-form-'))
).toBe(false)
expect(child.url()).toBe(destination)
} else {
await expect
.poll(() =>
receipts.slice(before).some((receipt) => receipt.path.startsWith('/sandbox-form-'))
)
.toBe(true)
}
await page.evaluate('console.log("orca-popup-fixture:snapshot")')
const creation = eventsSince(offset).find(
(event) =>
event.event ===
(path === 'adopted'
? kind === 'script'
? 'createWindow'
: 'createWindow-missing'
: 'did-create-window')
)
if (!creation) {
throw new Error('Missing creation event')
}
records.push(creation)
expect(status().windowsHidden).toBe(true)
snapshots.push(status())
await closeChildren(page)
}
return records
}
@@ -0,0 +1,191 @@
import { app, BrowserWindow, type Session, type WebContents } from 'electron'
import { openPopupWithOriginBar, type PopupChildWindowOptions } from './popup-origin-bar-window'
import { allowNativeBrowserPopup } from './browser-popup-native-window'
export class BrowserPopupAdapterProbeFixture {
readonly events: Record<string, unknown>[] = []
private sequence = 0
private readonly unrelatedWindows = new Set<BrowserWindow>()
private readonly observedSessions = new WeakSet<Session>()
private readonly observedContents = new Set<number>()
constructor(private readonly writeStatus: () => void) {
app.on('browser-window-created', (_event, window) => {
this.record('browser-window-created', window.webContents)
})
app.on('web-contents-created', (_event, contents) => {
this.record('web-contents-created', contents)
contents.once('will-frame-navigate', () => this.record('will-frame-navigate', contents))
contents.once('did-start-navigation', () => this.record('did-start-navigation', contents))
contents.on('console-message', ({ message }) => {
if (message.includes('Blocked form submission')) {
this.record('sandbox-form-blocked', contents, { message })
}
})
})
}
private record(event: string, contents: WebContents, extra: Record<string, unknown> = {}): void {
const preferences =
'getLastWebPreferences' in contents && typeof contents.getLastWebPreferences === 'function'
? contents.getLastWebPreferences()
: null
this.events.push({
event,
contentsId: contents.id,
url: contents.getURL(),
opener: contents.opener?.top?.url ?? null,
frameName: contents.mainFrame.name,
frameOrigin: contents.mainFrame.origin,
preferences,
...extra
})
this.writeStatus()
}
install(guest: WebContents): void {
if (!this.observedSessions.has(guest.session)) {
this.observedSessions.add(guest.session)
guest.session.webRequest.onBeforeRequest((details, callback) => {
this.events.push({
event: 'before-request',
contentsId: details.webContentsId,
url: details.url,
observedBeforeRequest:
details.webContentsId !== undefined && this.observedContents.has(details.webContentsId),
webRtcPolicy: details.webContents?.getWebRTCIPHandlingPolicy()
})
this.writeStatus()
callback({})
})
}
let adopted = true
let unrelatedDuringAdmission = false
guest.on('console-message', ({ message }) => {
if (message === 'orca-popup-fixture:probe-native') {
adopted = false
}
if (message === 'orca-popup-fixture:probe-adopted') {
adopted = true
}
if (message === 'orca-popup-fixture:probe-unrelated') {
unrelatedDuringAdmission = true
}
if (message === 'orca-popup-fixture:snapshot') {
this.writeStatus()
}
if (message === 'orca-popup-fixture:probe-cleanup') {
for (const window of this.unrelatedWindows) {
if (!window.isDestroyed()) {
window.destroy()
}
}
this.unrelatedWindows.clear()
}
})
guest.on('did-create-window', (window, details) => {
this.record('did-create-window', window.webContents, {
owner: guest.id,
optionsArguments: details.options.webPreferences?.additionalArguments,
optionsPreferences: details.options.webPreferences
})
})
guest.setWindowOpenHandler((details) => {
const request = ++this.sequence
this.events.push({ event: 'admission', request, owner: guest.id, details })
this.writeStatus()
if (unrelatedDuringAdmission) {
unrelatedDuringAdmission = false
const response = allowNativeBrowserPopup({
opener: guest,
targetUrl: details.url,
options: { show: false },
onCreated: ({ nativeWindow }) => {
if (!nativeWindow) {
throw new Error('Missing native child')
}
this.record('production-adapter-created', nativeWindow.webContents, {
request,
owner: guest.id
})
return nativeWindow.webContents
}
})
const unrelated = new BrowserWindow({
show: false,
webPreferences: { session: guest.session }
})
this.unrelatedWindows.add(unrelated)
this.record('unrelated-window', unrelated.webContents, { request })
unrelated.webContents.once('destroyed', () =>
this.events.push({ event: 'unrelated-destroyed', request })
)
return response
}
const options = {
show: false,
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
session: guest.session,
additionalArguments: [`--orca-popup-probe=${request}`]
}
}
return {
action: 'allow',
overrideBrowserWindowOptions: options,
...(adopted
? {
createWindow: (childOptions: PopupChildWindowOptions) => {
const child = childOptions.webContents
if (child) {
this.record('createWindow', child, {
callbackRequest: request,
owner: guest.id,
optionsPreferences: childOptions.webPreferences
})
} else {
this.events.push({
event: 'createWindow-missing',
request,
owner: guest.id,
optionsPreferences: childOptions.webPreferences
})
}
let contents: WebContents
if (child) {
contents = openPopupWithOriginBar(
{ ...childOptions, show: false },
details.url,
() => true
).contentWebContents
} else {
const window = new BrowserWindow({ ...childOptions, show: false })
this.unrelatedWindows.add(window)
contents = window.webContents
}
this.observe(contents, guest.id)
this.writeStatus()
return contents
}
}
: {})
}
})
// Observation only; native creation keeps Chromium's original navigation.
guest.prependListener('did-create-window', (window) =>
this.observe(window.webContents, guest.id)
)
}
private observe(contents: WebContents, owner: number): void {
contents.setWebRTCIPHandlingPolicy('disable_non_proxied_udp')
this.observedContents.add(contents.id)
this.record('observer-attached', contents, { owner })
contents.once('did-start-navigation', () =>
this.record('first-navigation', contents, { owner })
)
contents.once('did-finish-load', () => this.record('loaded', contents, { owner }))
}
}
@@ -1,3 +1,4 @@
import type { PopupCompatibilityStatus } from './browser-popup-compatibility-status'
import { spawnProcess } from '@orca/process-host'
import { createServer } from 'node:http'
import { createServer as createPortServer } from 'node:net'
@@ -43,40 +44,7 @@ export const artifactDir = process.env.ORCA_POPUP_FIXTURE_ARTIFACT_DIR
export const snapshots: ReturnType<typeof status>[] = []
export const expectedSecurity = { sandbox: true, contextIsolation: true, nodeIntegration: false }
export function status(): {
revision: number
ready?: boolean
error?: string
windowsHidden: boolean
tabOpens: { url: string; activate: boolean }[]
blocked: number[]
inputs: string[]
childRequests: {
adopted: boolean
native: boolean
sessionMatches: boolean
security: typeof expectedSecurity
permission?: string
}[]
routeGranted: boolean
routeContents: { security: typeof expectedSecurity; webRtcPolicy: string }[]
native: {
presentationCalls: string[]
requests: { disposition: string; features: string; frameName: string; post: boolean }[]
creation: {
firstNavigationPolicy: string
firstNavigationOwner: number | null
firstNavigationTitle: string
}[]
windows: {
closed: boolean
title: string
fullscreen: boolean
focused: boolean
contentSize: number[]
}[]
}
} {
export function status(): PopupCompatibilityStatus {
return JSON.parse(readFileSync(statusPath, 'utf8'))
}
@@ -116,7 +84,9 @@ const server = createServer((request, response) => {
}
response.setHeader('Content-Type', 'text/html')
response.end(
['/local', '/route', '/local-control', '/route-control'].includes(path)
['/local', '/route', '/local-control', '/route-control', '/probe-one', '/probe-two'].includes(
path
)
? popupFixtureHtml(origin)
: `<title>Hostile synthetic title</title><body>${path}<script>if(window.opener)window.opener.postMessage('callback:'+document.cookie,'*')</script></body>`
)
@@ -194,7 +164,8 @@ beforeAll(async () => {
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_POPUP_FIXTURE_ORIGIN: origin,
ORCA_POPUP_FIXTURE_STATUS: statusPath,
ORCA_POPUP_FIXTURE_NATIVE_CONTROLS: nativeControls ? '1' : '0'
ORCA_POPUP_FIXTURE_NATIVE_CONTROLS: nativeControls ? '1' : '0',
ORCA_POPUP_FIXTURE_ADAPTER_PROBE: adapterProbe ? '1' : '0'
},
stdio: ['ignore', 'pipe', 'pipe']
})
@@ -215,9 +186,13 @@ beforeAll(async () => {
return current.ready === true
})
expect(status()).toMatchObject({ ready: true, windowsHidden: true })
for (const name of nativeControls
? ['local', 'route', 'local-control', 'route-control']
: ['local', 'route']) {
const names = [
'local',
'route',
...(nativeControls ? ['local-control', 'route-control'] : []),
...(adapterProbe ? ['probe-one', 'probe-two'] : [])
]
for (const name of names) {
await waitForFixture(() =>
Boolean(
browser
@@ -306,6 +281,10 @@ export async function closeChildren(page: Page): Promise<void> {
let evidenceName = 'electron-evidence.json'
let nativeControls = false
let adapterProbe = false
export function enablePopupAdapterProbe(): void {
adapterProbe = true
}
export function enablePopupFixtureNativeControls(): void {
nativeControls = true
}
@@ -9,10 +9,9 @@ import {
} from 'electron'
import { renameSync, writeFileSync } from 'node:fs'
import { applyBackgroundActivationPolicy } from '../window/foreground-activation-policy'
import { BrowserManager } from './browser-manager'
import { BrowserPopupCompatibilityManager } from './browser-popup-compatibility-manager-fixture'
import { BrowserRouteWebContentsRegistry } from './browser-route-webcontents-registry'
import { initializeBrowserProcessUserAgent } from './browser-process-user-agent'
import type { PopupChildWindowOptions } from './popup-origin-bar-window'
import {
BrowserPopupNativeFixture,
popupFixtureContentSecurity as contentSecurity
@@ -21,6 +20,7 @@ import type {
BrowserRouteGuestLifecycleClaim,
BrowserRoutePageGuestIdentity
} from './browser-route-page-authority'
import { BrowserPopupAdapterProbeFixture } from './browser-popup-adapter-probe-fixture'
const origin = process.env.ORCA_POPUP_FIXTURE_ORIGIN
const statusPath = process.env.ORCA_POPUP_FIXTURE_STATUS
@@ -32,54 +32,7 @@ const fixtureStatusPath = statusPath
applyBackgroundActivationPolicy()
initializeBrowserProcessUserAgent('clean')
class FixtureBrowserManager extends BrowserManager {
tabOpens: { url: string; activate: boolean | undefined }[] = []
childRequests: {
adopted: boolean
native: boolean
sessionMatches: boolean
security: ReturnType<typeof contentSecurity>
permission?: string
}[] = []
protected override openLinkInOrcaTab(pageId: string, url: string, activate?: boolean): boolean {
this.tabOpens.push({ url, activate })
writeStatus()
return super.openLinkInOrcaTab(pageId, url, activate)
}
protected override createPopupChildWindowWithOriginBar(
opener: WebContents,
targetUrl: string,
options: PopupChildWindowOptions
): WebContents {
const contents = super.createPopupChildWindowWithOriginBar(opener, targetUrl, options)
const record = {
adopted: Boolean(options.webContents),
native: Boolean(options.nativeWindow),
sessionMatches: contents.session === opener.session,
security: contentSecurity(contents),
permission: 'pending'
}
this.childRequests.push(record)
if (!options.webContents) {
contents.once('did-finish-load', () => {
void contents
.executeJavaScript('Notification.requestPermission()')
.then((permission: unknown) => {
record.permission = typeof permission === 'string' ? permission : 'unknown'
writeStatus()
})
.catch(() => {
record.permission = 'unavailable'
writeStatus()
})
})
}
writeStatus()
return contents
}
}
const manager = new FixtureBrowserManager()
const manager = new BrowserPopupCompatibilityManager(() => writeStatus())
const guests = new Map<string, WebContents>()
const hosts: BrowserWindow[] = []
const blocked: number[] = []
@@ -95,6 +48,10 @@ const authority = Symbol('fixture-route-authority')
let fixtureAuthority: symbol | null = authority
let fixtureAuthorityThrows = false
let revision = 0
const adapterProbe =
process.env.ORCA_POPUP_FIXTURE_ADAPTER_PROBE === '1'
? new BrowserPopupAdapterProbeFixture(() => writeStatus())
: null
const native = new BrowserPopupNativeFixture({
routeSession: () => fixtureRouteSession,
failAuthority: (throwOnRead) => {
@@ -129,6 +86,8 @@ function writeStatus(error?: string): void {
})),
blocked,
inputs,
electronVersion: process.versions.electron,
adapterProbe: adapterProbe?.events,
windowsHidden: BaseWindow.getAllWindows().every((window) => !window.isVisible()),
guestIds: Object.fromEntries([...guests].map(([name, guest]) => [name, guest.id]))
})
@@ -164,7 +123,11 @@ async function createHost(name: string, partition: string, nativeControl = false
writeStatus()
})
if (nativeControl) {
native.installControlHandler(guest)
if (adapterProbe) {
adapterProbe.install(guest)
} else {
native.installControlHandler(guest)
}
await guest.loadURL(`${fixtureOrigin}/${name}`)
resolve()
return
@@ -298,6 +261,10 @@ async function run(): Promise<void> {
await createHost('local-control', localPartition, true)
await createHost('route-control', routePartition, true)
}
if (adapterProbe) {
await createHost('probe-one', localPartition, true)
await createHost('probe-two', localPartition, true)
}
ready = true
writeStatus()
}
@@ -0,0 +1,55 @@
import type { WebContents } from 'electron'
import { BrowserManager } from './browser-manager'
import type { PopupChildWindowOptions } from './popup-origin-bar-window'
import { popupFixtureContentSecurity as contentSecurity } from './browser-popup-native-electron-fixture'
export class BrowserPopupCompatibilityManager extends BrowserManager {
constructor(private readonly writeStatus: () => void) {
super()
}
tabOpens: { url: string; activate: boolean | undefined }[] = []
childRequests: {
adopted: boolean
native: boolean
sessionMatches: boolean
security: ReturnType<typeof contentSecurity>
permission?: string
}[] = []
protected override openLinkInOrcaTab(pageId: string, url: string, activate?: boolean): boolean {
this.tabOpens.push({ url, activate })
this.writeStatus()
return super.openLinkInOrcaTab(pageId, url, activate)
}
protected override createPopupChildWindowWithOriginBar(
opener: WebContents,
targetUrl: string,
options: PopupChildWindowOptions
): WebContents {
const contents = super.createPopupChildWindowWithOriginBar(opener, targetUrl, options)
const record = {
adopted: Boolean(options.webContents),
native: Boolean(options.nativeWindow),
sessionMatches: contents.session === opener.session,
security: contentSecurity(contents),
permission: 'pending'
}
this.childRequests.push(record)
if (!options.webContents) {
contents.once('did-finish-load', () => {
void contents
.executeJavaScript('Notification.requestPermission()')
.then((permission: unknown) => {
record.permission = typeof permission === 'string' ? permission : 'unknown'
this.writeStatus()
})
.catch(() => {
record.permission = 'unavailable'
this.writeStatus()
})
})
}
this.writeStatus()
return contents
}
}
@@ -0,0 +1,42 @@
type PopupFixtureSecurity = {
sandbox: boolean
contextIsolation: boolean
nodeIntegration: boolean
}
export type PopupCompatibilityStatus = {
electronVersion: string
adapterProbe?: Record<string, unknown>[]
revision: number
ready?: boolean
error?: string
windowsHidden: boolean
tabOpens: { url: string; activate: boolean }[]
blocked: number[]
inputs: string[]
childRequests: {
adopted: boolean
native: boolean
sessionMatches: boolean
security: PopupFixtureSecurity
permission?: string
}[]
routeGranted: boolean
routeContents: { security: PopupFixtureSecurity; webRtcPolicy: string }[]
native: {
presentationCalls: string[]
requests: { disposition: string; features: string; frameName: string; post: boolean }[]
creation: {
firstNavigationPolicy: string
firstNavigationOwner: number | null
firstNavigationTitle: string
}[]
windows: {
closed: boolean
title: string
fullscreen: boolean
focused: boolean
contentSize: number[]
}[]
}
}
@@ -1,18 +1,11 @@
import { EventEmitter } from 'node:events'
import { afterEach, describe, expect, it, vi } from 'vitest'
const { events } = vi.hoisted(() => ({ events: new Map<string, (...args: unknown[]) => void>() }))
vi.mock('electron', () => ({
app: {
prependOnceListener: (name: string, listener: (...args: unknown[]) => void) =>
events.set(name, listener),
off: (name: string, listener: (...args: unknown[]) => void) => {
if (events.get(name) === listener) {
events.delete(name)
}
}
}
}))
const { events } = await vi.hoisted(async () => {
const { EventEmitter } = await import('node:events')
return { events: new EventEmitter() }
})
vi.mock('electron', () => ({ app: events }))
import {
allowNativeBrowserPopup,
@@ -21,7 +14,7 @@ import {
} from './browser-popup-native-window'
afterEach(() => {
events.clear()
events.removeAllListeners()
vi.unstubAllEnvs()
})
@@ -85,9 +78,9 @@ describe('native popup admission', () => {
minHeight: 200,
webPreferences: { session: opener.session }
})
expect(events.has('browser-window-created')).toBe(true)
expect(events.listenerCount('browser-window-created') > 0).toBe(true)
await Promise.resolve()
expect(events.has('browser-window-created')).toBe(false)
expect(events.listenerCount('browser-window-created') > 0).toBe(false)
expect(onCreated).not.toHaveBeenCalled()
})
@@ -108,9 +101,6 @@ describe('native popup admission', () => {
options: {},
onCreated
})
const superseded = vi.fn(() => scripted.browserWindow.webContents)
admit(superseded)
await Promise.resolve()
admit(onScripted)
await Promise.resolve()
// An OpenURL child admitted during its own turn must not consume the pending script admission.
@@ -121,7 +111,7 @@ describe('native popup admission', () => {
options: {},
onCreated: onLinked
})
events.get('browser-window-created')?.({}, linked.browserWindow)
events.emit('browser-window-created', {}, linked.browserWindow)
await Promise.resolve()
opener.contents.emit('did-create-window', linked.browserWindow)
expect(onLinked).toHaveBeenCalledOnce()
@@ -130,7 +120,6 @@ describe('native popup admission', () => {
expect(onScripted).toHaveBeenCalledWith(
expect.objectContaining({ nativeWindow: scripted.browserWindow })
)
expect(superseded).not.toHaveBeenCalled()
const preventDefault = vi.fn()
scripted.window.emit('page-title-updated', { preventDefault }, 'Spoofed title')
expect(preventDefault).toHaveBeenCalledOnce()
@@ -159,6 +148,117 @@ describe('native popup admission', () => {
expect(first.window.destroy).not.toHaveBeenCalled()
})
it.fails('KNOWN BLOCKER: a later scripted admission must not supersede the earlier request', async () => {
const opener = nativeWindow()
const first = nativeWindow()
const second = nativeWindow()
first.contents.session = opener.contents.session
second.contents.session = opener.contents.session
const earlier = vi.fn(() => first.browserWindow.webContents)
const later = vi.fn(() => second.browserWindow.webContents)
for (const onCreated of [earlier, later]) {
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
await Promise.resolve()
}
opener.contents.emit('did-create-window', second.browserWindow)
opener.contents.emit('did-create-window', first.browserWindow)
expect(later).toHaveBeenCalledWith(
expect.objectContaining({ nativeWindow: second.browserWindow })
)
expect(earlier).toHaveBeenCalledWith(
expect.objectContaining({ nativeWindow: first.browserWindow })
)
})
it.fails('KNOWN BLOCKER: canceled A must not transfer B to a late child of A', async () => {
const opener = nativeWindow()
const lateA = nativeWindow()
lateA.contents.session = opener.contents.session
const onA = vi.fn(() => lateA.browserWindow.webContents)
const onB = vi.fn(() => lateA.browserWindow.webContents)
for (const onCreated of [onA, onB]) {
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
await Promise.resolve()
}
opener.contents.emit('did-create-window', lateA.browserWindow)
expect(onB).not.toHaveBeenCalled()
})
it.fails('KNOWN BLOCKER: shared-session parents must not both prepare the same app notification', () => {
const first = nativeWindow()
const second = nativeWindow()
const child = nativeWindow()
second.contents.session = first.contents.session
child.contents.session = first.contents.session
const onFirst = vi.fn(() => child.browserWindow.webContents)
const onSecond = vi.fn(() => child.browserWindow.webContents)
for (const [opener, onCreated] of [
[first.browserWindow.webContents, onFirst],
[second.browserWindow.webContents, onSecond]
] as const) {
allowNativeBrowserPopup({
opener,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
}
events.emit('browser-window-created', {}, child.browserWindow)
expect(onSecond).toHaveBeenCalledOnce()
expect(onFirst).not.toHaveBeenCalled()
})
it.fails.each([true, false])(
'KNOWN BLOCKER: an unrelated app window receives neither policy nor disposal (same session: %s)',
async (sameSession) => {
const opener = nativeWindow()
const unrelated = nativeWindow()
if (sameSession) {
unrelated.contents.session = opener.contents.session
}
const onCreated = vi.fn(() => unrelated.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
events.emit('browser-window-created', {}, unrelated.browserWindow)
await Promise.resolve()
expect(onCreated).not.toHaveBeenCalled()
expect(unrelated.contents.close).not.toHaveBeenCalled()
expect(unrelated.window.destroy).not.toHaveBeenCalled()
}
)
it('handles duplicate did-create-window delivery idempotently for an already marked child', async () => {
const opener = nativeWindow()
const child = nativeWindow()
child.contents.session = opener.contents.session
const onCreated = vi.fn(() => child.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
await Promise.resolve()
opener.contents.emit('did-create-window', child.browserWindow)
opener.contents.emit('did-create-window', child.browserWindow)
expect(onCreated).toHaveBeenCalledOnce()
expect(child.contents.close).not.toHaveBeenCalled()
})
it.each([
'ready',
'background-launch',
@@ -197,7 +297,7 @@ describe('native popup admission', () => {
onCreated: prepare
})
expect(response.overrideBrowserWindowOptions?.show).toBe(false)
events.get('browser-window-created')?.({}, browserWindow)
events.emit('browser-window-created', {}, browserWindow)
expect(prepare).toHaveBeenCalledOnce()
expect(window.showInactive).not.toHaveBeenCalled()
if (state === 'closed') {