Update extension fixtures for execution ownership

This commit is contained in:
Brennan Benson
2026-11-13 23:07:12 -08:00
parent 7882cb5f53
commit c8bd5dfef8
4 changed files with 29 additions and 25 deletions
@@ -74,6 +74,8 @@ await import('../../../../preload/browser/extensions/api/browser-extension-api-s
const registry = await import('./browser-extension-namespace-registry')
const host = await import('./browser-extension-api-host')
const contexts = await import('./browser-extension-contexts')
const { beginBrowserExtensionExecution, getBrowserExtensionExecution } =
await import('./browser-extension-execution-authority')
const { registerBrowserExtensionNamespaces } =
await import('../register-browser-extension-namespaces')
@@ -97,16 +99,22 @@ const extensionSession = {
serviceWorkers: { on: vi.fn(), getAllRunning: () => ({}), getWorkerFromVersionID: vi.fn() },
protocol: { handle: vi.fn(), unhandle: vi.fn(), isProtocolHandled: vi.fn(() => true) }
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture sessions provide all host-facing native members above.
const session = extensionSession as unknown as Electron.Session
const handled: unknown[][] = []
function frameEvent(
url: string,
sess: unknown = extensionSession,
sess: Electron.Session = session,
processId = 4,
routingId = 9,
// Node reports "null" for chrome-extension: URLs; Chromium reports the real extension origin.
origin = url.split('/').slice(0, 3).join('/')
) {
const extensionId = new URL(url).hostname
if (!getBrowserExtensionExecution(sess, extensionId)) {
beginBrowserExtensionExecution(sess, extensionId, new AbortController().signal)
}
const frame = {
url,
origin,
@@ -150,11 +158,7 @@ beforeAll(() => {
events: ['onPing']
})
host.installBrowserExtensionApiHost()
host.attachBrowserExtensionApiHostToSession(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host only touches the extensions and serviceWorkers members stubbed above.
extensionSession as unknown as Electron.Session,
'default'
)
host.attachBrowserExtensionApiHostToSession(session, 'default')
})
describe('extension API host', () => {
@@ -171,7 +175,7 @@ describe('extension API host', () => {
method: 'request',
args: [{ permissions: ['privacy'] }]
}
const denied = { ok: false, error: 'This function must be called during a user gesture' }
const denied = expect.objectContaining({ ok: false })
await expect(invoke(event, request)).resolves.toEqual(denied)
await expect(invoke(event, { ...request, userGesture: true })).resolves.toEqual(denied)
expect(mocks.prompt).not.toHaveBeenCalled()
@@ -64,6 +64,8 @@ const { BROWSER_EXTENSION_EVENT_CHANNEL, BROWSER_EXTENSION_INVOKE_CHANNEL } =
await import('../../../../shared/browser/extensions/browser-extension-channels')
const registry = await import('../api-runtime/browser-extension-namespace-registry')
const host = await import('../api-runtime/browser-extension-api-host')
const { beginBrowserExtensionExecution } =
await import('../api-runtime/browser-extension-execution-authority')
const { registerBrowserExtensionNamespaces } =
await import('../register-browser-extension-namespaces')
await import('./permissions')
@@ -197,6 +199,9 @@ beforeAll(() => {
beforeEach(() => {
host.attachBrowserExtensionApiHostToSession(session, 'default')
for (const id of extensions.keys()) {
beginBrowserExtensionExecution(session, id, new AbortController().signal)
}
mocks.prompt.mockReset()
})
@@ -31,16 +31,16 @@ vi.mock('../../../window/activation/foreground-activation-policy', () => ({
isBackgroundLaunch: () => mocks.background
}))
vi.mock('../api-runtime/browser-extension-api-host', () => ({ emitBrowserExtensionEvent: vi.fn() }))
vi.mock('../permissions/browser-extension-user-gesture', () => ({
noteBrowserExtensionUserGesture: vi.fn()
vi.mock('./browser-extension-notification-interactions', () => ({
dispatchBrowserExtensionNotificationInteraction: vi.fn(async () => {})
}))
const { registerNotificationsNamespace } = await import('./notifications')
const { getBrowserExtensionNamespace } =
await import('../api-runtime/browser-extension-namespace-registry')
const { emitBrowserExtensionEvent } = await import('../api-runtime/browser-extension-api-host')
const { noteBrowserExtensionUserGesture } =
await import('../permissions/browser-extension-user-gesture')
const { dispatchBrowserExtensionNotificationInteraction } =
await import('./browser-extension-notification-interactions')
registerNotificationsNamespace()
const extension = { id: 'a'.repeat(32), path: join('/ext', 'root') }
@@ -95,16 +95,12 @@ describe('notification extension ownership', () => {
clicked.emit('click')
expect(emitBrowserExtensionEvent).toHaveBeenCalledExactlyOnceWith(
{ session: caller.session, extensionId: caller.extension.id },
'notifications',
expect(dispatchBrowserExtensionNotificationInteraction).toHaveBeenCalledExactlyOnceWith(
caller.session,
caller.extension.id,
'onClicked',
['clicked']
)
expect(noteBrowserExtensionUserGesture).toHaveBeenCalledExactlyOnceWith(
caller.session,
caller.extension.id
)
expect(invoke(caller, 'getAll')).toEqual({})
expect(invoke(caller, 'clear', ['clicked'])).toBe(false)
expect(invoke(sibling, 'getAll')).toEqual({ sibling: true })
@@ -115,7 +111,7 @@ describe('notification extension ownership', () => {
clicked.emit('click')
clicked.emit('close', {})
expect(invoke(caller, 'getAll')).toEqual({ clicked: true })
expect(emitBrowserExtensionEvent).toHaveBeenCalledTimes(1)
expect(dispatchBrowserExtensionNotificationInteraction).toHaveBeenCalledTimes(1)
})
it('closes and forgets unloaded notifications while preserving sibling extensions and profiles', () => {
@@ -135,12 +131,14 @@ describe('notification extension ownership', () => {
expect(siblingNotification.close).not.toHaveBeenCalled()
expect(otherNotification.close).not.toHaveBeenCalled()
expect(retired.eventNames()).toEqual([])
expect(dispatchBrowserExtensionNotificationInteraction).not.toHaveBeenCalled()
expect(emitBrowserExtensionEvent).not.toHaveBeenCalled()
invoke(caller, 'create', ['same-id', options])
expect(extensions.listenerCount('extension-unloaded')).toBe(1)
retired.emit('close', { reason: 'timedOut' })
retired.emit('click')
expect(invoke(caller, 'getAll')).toEqual({ 'same-id': true })
expect(dispatchBrowserExtensionNotificationInteraction).not.toHaveBeenCalled()
expect(emitBrowserExtensionEvent).not.toHaveBeenCalled()
})
})
@@ -124,9 +124,6 @@ function install(context: BrowserExtensionStorageContext) {
call(chrome, 'storage.local.onChanged.addListener', (changes: unknown) =>
perArea.push(['local', changes])
)
call(chrome, 'storage.session.onChanged.addListener', (changes: unknown) =>
perArea.push(['session', changes])
)
}
}
}
@@ -205,8 +202,8 @@ describe('installBrowserExtensionStorageRelay in a worker', () => {
const worker = install('worker')
expect(worker.chrome.storage.onChanged).not.toEqual({ native: true })
worker.listen()
worker.emit([{ token: { newValue: 'abc' } }, 'session'])
expect(worker.all).toEqual([['session', { token: { newValue: 'abc' } }]])
expect(worker.perArea).toEqual([['session', { token: { newValue: 'abc' } }]])
worker.emit([{ token: { newValue: 'abc' } }, 'local'])
expect(worker.all).toEqual([['local', { token: { newValue: 'abc' } }]])
expect(worker.perArea).toEqual([['local', { token: { newValue: 'abc' } }]])
})
})