mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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}**`
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user