From ab3f7d397644c0f2b4b8973fbe593f29a4f66840 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Tue, 19 Jul 2022 17:10:44 +0530 Subject: [PATCH] feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge --- docs/api/context-bridge.md | 49 +- lib/renderer/api/context-bridge.ts | 27 +- .../api/electron_api_context_bridge.cc | 55 + spec-main/api-context-bridge-spec.ts | 3241 ++++++++++++----- 4 files changed, 2427 insertions(+), 945 deletions(-) diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md index 5d62220bd6070..966ab40f97c41 100644 --- a/docs/api/context-bridge.md +++ b/docs/api/context-bridge.md @@ -46,11 +46,17 @@ The `contextBridge` module has the following methods: * `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. * `api` any - Your API, more information on what this API can be and how it works is available below. +### `contextBridge.exposeInIsolatedWorld(worldId, apiKey, api)` + +* `worldId` Integer - The ID of the world to inject the API into. This has to be an existing world. +* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. +* `api` any - Your API, more information on what this API can be and how it works is available below. + ## Usage ### API -The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object +The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) and [`exposeInIsolatedWorld`](#contextbridgeexposeinisolatedworldworldid-apikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object whose keys are strings and values are a `Function`, `string`, `number`, `Array`, `boolean`, or another nested object that meets the same conditions. `Function` values are proxied to the other context and all other values are **copied** and **frozen**. Any data / primitives sent in @@ -84,6 +90,47 @@ contextBridge.exposeInMainWorld( ) ``` +An example of complex API exposed in an isolated world is shown below: + +```javascript + +const { contextBridge, webFrame } = require('electron') + +webFrame.setIsolatedWorldInfo(1005, { + name: 'Isolated World' +}) + +contextBridge.exposeInIsolatedWorld( + 1005, + 'electron', + { + doThing: () => ipcRenderer.send('do-a-thing'), + myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))], + anAsyncFunction: async () => 123, + data: { + myFlags: ['a', 'b', 'c'], + bootTime: 1234 + }, + nestedAPI: { + evenDeeper: { + youCanDoThisAsMuchAsYouWant: { + fn: () => ({ + returnData: 123 + }) + } + } + } + } +) + +// To call this API in isolated world, you can use `executeJavaScriptInIsolatedWorld` +webFrame.executeJavaScriptInIsolatedWorld(1005, [ + { + code: 'window.electron.doThing()' + } +]) +``` + ### API Functions `Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This diff --git a/lib/renderer/api/context-bridge.ts b/lib/renderer/api/context-bridge.ts index dd545767c93b1..566d6de9f21e9 100644 --- a/lib/renderer/api/context-bridge.ts +++ b/lib/renderer/api/context-bridge.ts @@ -1,13 +1,21 @@ const binding = process._linkedBinding('electron_renderer_context_bridge'); const checkContextIsolationEnabled = () => { - if (!process.contextIsolated) throw new Error('contextBridge API can only be used when contextIsolation is enabled'); + if (!process.contextIsolated) { + throw new Error( + 'contextBridge API can only be used when contextIsolation is enabled' + ); + } }; const contextBridge: Electron.ContextBridge = { exposeInMainWorld: (key: string, api: any) => { checkContextIsolationEnabled(); return binding.exposeAPIInMainWorld(key, api); + }, + exposeInIsolatedWorld: (worldId: number, key: string, api: any) => { + checkContextIsolationEnabled(); + return binding.exposeAPIInIsolatedWorld(worldId, key, api); } }; @@ -18,11 +26,22 @@ export const internalContextBridge = { overrideGlobalValueFromIsolatedWorld: (keys: string[], value: any) => { return binding._overrideGlobalValueFromIsolatedWorld(keys, value, false); }, - overrideGlobalValueWithDynamicPropsFromIsolatedWorld: (keys: string[], value: any) => { + overrideGlobalValueWithDynamicPropsFromIsolatedWorld: ( + keys: string[], + value: any + ) => { return binding._overrideGlobalValueFromIsolatedWorld(keys, value, true); }, - overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => { - return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null); + overrideGlobalPropertyFromIsolatedWorld: ( + keys: string[], + getter: Function, + setter?: Function + ) => { + return binding._overrideGlobalPropertyFromIsolatedWorld( + keys, + getter, + setter || null + ); }, isInMainWorld: () => binding._isCalledFromMainWorld() as boolean }; diff --git a/shell/renderer/api/electron_api_context_bridge.cc b/shell/renderer/api/electron_api_context_bridge.cc index 22f8c0adada0d..62fa0a7d13e8a 100644 --- a/shell/renderer/api/electron_api_context_bridge.cc +++ b/shell/renderer/api/electron_api_context_bridge.cc @@ -607,6 +607,59 @@ void ExposeAPIInMainWorld(v8::Isolate* isolate, } } +void ExposeAPIInIsolatedWorld(v8::Isolate* isolate, + const int world_id, + const std::string& key, + v8::Local api, + gin_helper::Arguments* args) { + TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInIsolatedWorld", "worldId", + world_id); + + auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); + CHECK(render_frame); + auto* frame = render_frame->GetWebFrame(); + CHECK(frame); + + v8::Local main_context = frame->MainWorldScriptContext(); + v8::Local isolated_context = + frame->GetScriptContextFromWorldId(isolate, world_id); + + gin_helper::Dictionary isolated(isolated_context->GetIsolate(), + isolated_context->Global()); + + if (isolated.Has(key)) { + args->ThrowError( + "Cannot bind an API on top of an existing property on the window " + "object"); + return; + } + + v8::Local isolated_context2 = frame->GetScriptContextFromWorldId( + args->isolate(), WorldIDs::ISOLATED_WORLD_ID); + + { + context_bridge::ObjectCache object_cache; + v8::Context::Scope main_context_scope(main_context); + + v8::MaybeLocal maybe_proxy = PassValueToOtherContext( + isolated_context2, isolated_context, api, &object_cache, false, 0); + if (maybe_proxy.IsEmpty()) + return; + auto proxy = maybe_proxy.ToLocalChecked(); + + if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) { + isolated.Set(key, proxy); + return; + } + + if (proxy->IsObject() && !proxy->IsTypedArray() && + !DeepFreeze(proxy.As(), isolated_context2)) + return; + + isolated.SetReadOnlyNonConfigurable(key, proxy); + } +} + gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start, const std::vector& key_path) { gin_helper::Dictionary current = start; @@ -717,6 +770,8 @@ void Initialize(v8::Local exports, v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld); + dict.SetMethod("exposeAPIInIsolatedWorld", + &electron::api::ExposeAPIInIsolatedWorld); dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", &electron::api::OverrideGlobalValueFromIsolatedWorld); dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", diff --git a/spec-main/api-context-bridge-spec.ts b/spec-main/api-context-bridge-spec.ts index dc3601dbf8841..5dd102de32df8 100644 --- a/spec-main/api-context-bridge-spec.ts +++ b/spec-main/api-context-bridge-spec.ts @@ -1,17 +1,23 @@ -import { BrowserWindow, ipcMain } from 'electron/main'; -import { contextBridge } from 'electron/renderer'; -import { expect } from 'chai'; +import * as cp from 'child_process'; import * as fs from 'fs-extra'; import * as http from 'http'; import * as os from 'os'; import * as path from 'path'; -import * as cp from 'child_process'; +import { BrowserWindow, ipcMain } from 'electron/main'; + +import { AddressInfo } from 'net'; import { closeWindow } from './window-helpers'; +import { contextBridge } from 'electron/renderer'; import { emittedOnce } from './events-helpers'; -import { AddressInfo } from 'net'; +import { expect } from 'chai'; -const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge'); +const fixturesPath = path.resolve( + __dirname, + 'fixtures', + 'api', + 'context-bridge' +); describe('contextBridge', () => { let w: BrowserWindow; @@ -23,11 +29,13 @@ describe('contextBridge', () => { res.setHeader('Content-Type', 'text/html'); res.end(''); }); - await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + await new Promise((resolve) => + server.listen(0, '127.0.0.1', resolve) + ); }); after(async () => { - if (server) await new Promise(resolve => server.close(resolve)); + if (server) await new Promise((resolve) => server.close(resolve)); server = null as any; }); @@ -44,7 +52,9 @@ describe('contextBridge', () => { preload: path.resolve(fixturesPath, 'can-bind-preload.js') } }); - const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html'))); + const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => + w.loadFile(path.resolve(fixturesPath, 'empty.html')) + ); expect(bound).to.equal(false); }); @@ -56,21 +66,45 @@ describe('contextBridge', () => { preload: path.resolve(fixturesPath, 'can-bind-preload.js') } }); - const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html'))); + const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => + w.loadFile(path.resolve(fixturesPath, 'empty.html')) + ); expect(bound).to.equal(true); }); const generateTests = (useSandbox: boolean) => { - describe(`with sandbox=${useSandbox}`, () => { + const getGCInfo = async (): Promise<{ + trackedValues: number; + }> => { + const [, info] = await emittedOnce(ipcMain, 'gc-info', () => + w.webContents.send('get-gc-info') + ); + return info; + }; + + const forceGCOnWindow = async () => { + w.webContents.debugger.attach(); + await w.webContents.debugger.sendCommand('HeapProfiler.enable'); + await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage'); + await w.webContents.debugger.sendCommand('HeapProfiler.disable'); + w.webContents.debugger.detach(); + }; + describe('exposeInMainWorld', () => { const makeBindingWindow = async (bindingCreator: Function) => { const preloadContent = `const renderer_1 = require('electron'); - ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc'); + ${ + useSandbox + ? '' + : `require('v8').setFlagsFromString('--expose_gc'); const gc=require('vm').runInNewContext('gc'); renderer_1.contextBridge.exposeInMainWorld('GCRunner', { run: () => gc() - });`} + });` + } (${bindingCreator.toString()})();`; - const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-')); + const tmpDir = await fs.mkdtemp( + path.resolve(os.tmpdir(), 'electron-spec-preload-') + ); dir = tmpDir; await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent); w = new BrowserWindow({ @@ -80,1066 +114,2373 @@ describe('contextBridge', () => { nodeIntegration: true, sandbox: useSandbox, preload: path.resolve(tmpDir, 'preload.js'), - additionalArguments: ['--unsafely-expose-electron-internals-for-testing'] + additionalArguments: [ + '--unsafely-expose-electron-internals-for-testing' + ] } }); - await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`); + await w.loadURL( + `http://127.0.0.1:${(server.address() as AddressInfo).port}` + ); }; - const callWithBindings = (fn: Function) => w.webContents.executeJavaScript(`(${fn.toString()})(window)`); - const getGCInfo = async (): Promise<{ - trackedValues: number; - }> => { - const [, info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info')); - return info; - }; - - const forceGCOnWindow = async () => { - w.webContents.debugger.attach(); - await w.webContents.debugger.sendCommand('HeapProfiler.enable'); - await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage'); - await w.webContents.debugger.sendCommand('HeapProfiler.disable'); - w.webContents.debugger.detach(); - }; - - it('should proxy numbers', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', 123); - }); - const result = await callWithBindings((root: any) => { - return root.example; - }); - expect(result).to.equal(123); - }); - - it('should make global properties read-only', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', 123); - }); - const result = await callWithBindings((root: any) => { - root.example = 456; - return root.example; - }); - expect(result).to.equal(123); - }); - - it('should proxy nested numbers', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myNumber: 123 + describe(`with sandbox=${useSandbox}`, () => { + it('should proxy numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', 123); }); - }); - const result = await callWithBindings((root: any) => { - return root.example.myNumber; - }); - expect(result).to.equal(123); - }); - - it('should make properties unwriteable', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myNumber: 123 + const result = await callWithBindings((root: any) => { + return root.example; }); + expect(result).to.equal(123); }); - const result = await callWithBindings((root: any) => { - root.example.myNumber = 456; - return root.example.myNumber; - }); - expect(result).to.equal(123); - }); - - it('should proxy strings', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', 'my-words'); - }); - const result = await callWithBindings((root: any) => { - return root.example; - }); - expect(result).to.equal('my-words'); - }); - it('should proxy nested strings', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myString: 'my-words' + it('should make global properties read-only', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', 123); }); - }); - const result = await callWithBindings((root: any) => { - return root.example.myString; - }); - expect(result).to.equal('my-words'); - }); - - it('should proxy arrays', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', [123, 'my-words']); - }); - const result = await callWithBindings((root: any) => { - return [root.example, Array.isArray(root.example)]; - }); - expect(result).to.deep.equal([[123, 'my-words'], true]); - }); - - it('should proxy nested arrays', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myArr: [123, 'my-words'] + const result = await callWithBindings((root: any) => { + root.example = 456; + return root.example; }); + expect(result).to.equal(123); }); - const result = await callWithBindings((root: any) => { - return root.example.myArr; - }); - expect(result).to.deep.equal([123, 'my-words']); - }); - it('should make arrays immutable', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', [123, 'my-words']); - }); - const immutable = await callWithBindings((root: any) => { - try { - root.example.push(456); - return false; - } catch { - return true; - } + it('should proxy nested numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myNumber: 123 + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myNumber; + }); + expect(result).to.equal(123); }); - expect(immutable).to.equal(true); - }); - it('should make nested arrays immutable', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myArr: [123, 'my-words'] + it('should make properties unwriteable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myNumber: 123 + }); }); + const result = await callWithBindings((root: any) => { + root.example.myNumber = 456; + return root.example.myNumber; + }); + expect(result).to.equal(123); }); - const immutable = await callWithBindings((root: any) => { - try { - root.example.myArr.push(456); - return false; - } catch { - return true; - } - }); - expect(immutable).to.equal(true); - }); - it('should proxy booleans', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', true); - }); - const result = await callWithBindings((root: any) => { - return root.example; + it('should proxy strings', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', 'my-words'); + }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal('my-words'); }); - expect(result).to.equal(true); - }); - it('should proxy nested booleans', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myBool: true + it('should proxy nested strings', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myString: 'my-words' + }); }); + const result = await callWithBindings((root: any) => { + return root.example.myString; + }); + expect(result).to.equal('my-words'); }); - const result = await callWithBindings((root: any) => { - return root.example.myBool; - }); - expect(result).to.equal(true); - }); - it('should proxy promises and resolve with the correct value', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', - Promise.resolve('i-resolved') - ); - }); - const result = await callWithBindings((root: any) => { - return root.example; + it('should proxy arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', [123, 'my-words']); + }); + const result = await callWithBindings((root: any) => { + return [root.example, Array.isArray(root.example)]; + }); + expect(result).to.deep.equal([[123, 'my-words'], true]); }); - expect(result).to.equal('i-resolved'); - }); - it('should proxy nested promises and resolve with the correct value', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myPromise: Promise.resolve('i-resolved') + it('should proxy nested arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myArr: [123, 'my-words'] + }); }); + const result = await callWithBindings((root: any) => { + return root.example.myArr; + }); + expect(result).to.deep.equal([123, 'my-words']); }); - const result = await callWithBindings((root: any) => { - return root.example.myPromise; - }); - expect(result).to.equal('i-resolved'); - }); - it('should proxy promises and reject with the correct value', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', Promise.reject(new Error('i-rejected'))); - }); - const result = await callWithBindings(async (root: any) => { - try { - await root.example; - return null; - } catch (err) { - return err; - } + it('should make arrays immutable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', [123, 'my-words']); + }); + const immutable = await callWithBindings((root: any) => { + try { + root.example.push(456); + return false; + } catch { + return true; + } + }); + expect(immutable).to.equal(true); }); - expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected'); - }); - it('should proxy nested promises and reject with the correct value', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myPromise: Promise.reject(new Error('i-rejected')) + it('should make nested arrays immutable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myArr: [123, 'my-words'] + }); }); + const immutable = await callWithBindings((root: any) => { + try { + root.example.myArr.push(456); + return false; + } catch { + return true; + } + }); + expect(immutable).to.equal(true); }); - const result = await callWithBindings(async (root: any) => { - try { - await root.example.myPromise; - return null; - } catch (err) { - return err; - } - }); - expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected'); - }); - it('should proxy promises and resolve with the correct value if it resolves later', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myPromise: () => new Promise(resolve => setTimeout(() => resolve('delayed'), 20)) + it('should proxy booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', true); }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal(true); }); - const result = await callWithBindings((root: any) => { - return root.example.myPromise(); - }); - expect(result).to.equal('delayed'); - }); - it('should proxy nested promises correctly', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - myPromise: () => new Promise(resolve => setTimeout(() => resolve(Promise.resolve(123)), 20)) + it('should proxy nested booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myBool: true + }); }); + const result = await callWithBindings((root: any) => { + return root.example.myBool; + }); + expect(result).to.equal(true); }); - const result = await callWithBindings((root: any) => { - return root.example.myPromise(); - }); - expect(result).to.equal(123); - }); - it('should proxy methods', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getNumber: () => 123, - getString: () => 'help', - getBoolean: () => false, - getPromise: async () => 'promise' + it('should proxy promises and resolve with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld( + 'example', + Promise.resolve('i-resolved') + ); }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal('i-resolved'); }); - const result = await callWithBindings(async (root: any) => { - return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()]; - }); - expect(result).to.deep.equal([123, 'help', false, 'promise']); - }); - it('should proxy functions', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', () => 'return-value'); - }); - const result = await callWithBindings(async (root: any) => { - return root.example(); + it('should proxy nested promises and resolve with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: Promise.resolve('i-resolved') + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise; + }); + expect(result).to.equal('i-resolved'); }); - expect(result).equal('return-value'); - }); - it('should not double-proxy functions when they are returned to their origin side of the bridge', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', (fn: any) => fn); - }); - const result = await callWithBindings(async (root: any) => { - const fn = () => null; - return root.example(fn) === fn; + it('should proxy promises and reject with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld( + 'example', + Promise.reject(new Error('i-rejected')) + ); + }); + const result = await callWithBindings(async (root: any) => { + try { + await root.example; + return null; + } catch (err) { + return err; + } + }); + expect(result) + .to.be.an.instanceOf(Error) + .with.property('message', 'i-rejected'); }); - expect(result).equal(true); - }); - it('should properly handle errors thrown in proxied functions', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); }); - }); - const result = await callWithBindings(async (root: any) => { - try { - root.example(); - } catch (e) { - return (e as Error).message; - } + it('should proxy nested promises and reject with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: Promise.reject(new Error('i-rejected')) + }); + }); + const result = await callWithBindings(async (root: any) => { + try { + await root.example.myPromise; + return null; + } catch (err) { + return err; + } + }); + expect(result) + .to.be.an.instanceOf(Error) + .with.property('message', 'i-rejected'); }); - expect(result).equal('oh no'); - }); - it('should proxy methods that are callable multiple times', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - doThing: () => 123 + it('should proxy promises and resolve with the correct value if it resolves later', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: () => + new Promise((resolve) => + setTimeout(() => resolve('delayed'), 20) + ) + }); }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise(); + }); + expect(result).to.equal('delayed'); }); - const result = await callWithBindings(async (root: any) => { - return [root.example.doThing(), root.example.doThing(), root.example.doThing()]; - }); - expect(result).to.deep.equal([123, 123, 123]); - }); - it('should proxy methods in the reverse direction', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - callWithNumber: (fn: any) => fn(123) + it('should proxy nested promises correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: () => + new Promise((resolve) => + setTimeout(() => resolve(Promise.resolve(123)), 20) + ) + }); }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise(); + }); + expect(result).to.equal(123); }); - const result = await callWithBindings(async (root: any) => { - return root.example.callWithNumber((n: number) => n + 1); - }); - expect(result).to.equal(124); - }); - it('should proxy promises in the reverse direction', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getPromiseValue: (p: Promise) => p + it('should proxy methods', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getNumber: () => 123, + getString: () => 'help', + getBoolean: () => false, + getPromise: async () => 'promise' + }); }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example.getNumber(), + root.example.getString(), + root.example.getBoolean(), + await root.example.getPromise() + ]; + }); + expect(result).to.deep.equal([123, 'help', false, 'promise']); }); - const result = await callWithBindings((root: any) => { - return root.example.getPromiseValue(Promise.resolve('my-proxied-value')); - }); - expect(result).to.equal('my-proxied-value'); - }); - it('should proxy objects with number keys', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - 1: 123, - 2: 456, - 3: 789 + it('should proxy functions', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', () => 'return-value'); }); + const result = await callWithBindings(async (root: any) => { + return root.example(); + }); + expect(result).equal('return-value'); }); - const result = await callWithBindings(async (root: any) => { - return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)]; - }); - expect(result).to.deep.equal([123, 456, 789, false]); - }); - it('it should proxy null', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', null); - }); - const result = await callWithBindings((root: any) => { - // Convert to strings as although the context bridge keeps the right value - // IPC does not - return `${root.example}`; + it('should not double-proxy functions when they are returned to their origin side of the bridge', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', (fn: any) => fn); + }); + const result = await callWithBindings(async (root: any) => { + const fn = () => null; + return root.example(fn) === fn; + }); + expect(result).equal(true); }); - expect(result).to.deep.equal('null'); - }); - it('it should proxy undefined', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', undefined); - }); - const result = await callWithBindings((root: any) => { - // Convert to strings as although the context bridge keeps the right value - // IPC does not - return `${root.example}`; + it('should properly handle errors thrown in proxied functions', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', () => { + throw new Error('oh no'); + }); + }); + const result = await callWithBindings(async (root: any) => { + try { + root.example(); + } catch (e) { + return (e as Error).message; + } + }); + expect(result).equal('oh no'); }); - expect(result).to.deep.equal('undefined'); - }); - it('it should proxy nested null and undefined correctly', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - values: [null, undefined] + it('should proxy methods that are callable multiple times', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + doThing: () => 123 + }); }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example.doThing(), + root.example.doThing(), + root.example.doThing() + ]; + }); + expect(result).to.deep.equal([123, 123, 123]); }); - const result = await callWithBindings((root: any) => { - // Convert to strings as although the context bridge keeps the right value - // IPC does not - return root.example.values.map((val: any) => `${val}`); - }); - expect(result).to.deep.equal(['null', 'undefined']); - }); - it('should proxy symbols', async () => { - await makeBindingWindow(() => { - const mySymbol = Symbol('unique'); - const isSymbol = (s: Symbol) => s === mySymbol; - contextBridge.exposeInMainWorld('symbol', mySymbol); - contextBridge.exposeInMainWorld('isSymbol', isSymbol); - }); - const result = await callWithBindings((root: any) => { - return root.isSymbol(root.symbol); + it('should proxy methods in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + callWithNumber: (fn: any) => fn(123) + }); + }); + const result = await callWithBindings(async (root: any) => { + return root.example.callWithNumber((n: number) => n + 1); + }); + expect(result).to.equal(124); }); - expect(result).to.equal(true, 'symbols should be equal across contexts'); - }); - it('should proxy symbols such that symbol equality works', async () => { - await makeBindingWindow(() => { - const mySymbol = Symbol('unique'); - contextBridge.exposeInMainWorld('example', { - getSymbol: () => mySymbol, - isSymbol: (s: Symbol) => s === mySymbol + it('should proxy promises in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getPromiseValue: (p: Promise) => p + }); }); + const result = await callWithBindings((root: any) => { + return root.example.getPromiseValue( + Promise.resolve('my-proxied-value') + ); + }); + expect(result).to.equal('my-proxied-value'); }); - const result = await callWithBindings((root: any) => { - return root.example.isSymbol(root.example.getSymbol()); - }); - expect(result).to.equal(true, 'symbols should be equal across contexts'); - }); - it('should proxy symbols such that symbol key lookup works', async () => { - await makeBindingWindow(() => { - const mySymbol = Symbol('unique'); - contextBridge.exposeInMainWorld('example', { - getSymbol: () => mySymbol, - getObject: () => ({ [mySymbol]: 123 }) + it('should proxy objects with number keys', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + 1: 123, + 2: 456, + 3: 789 + }); }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example[1], + root.example[2], + root.example[3], + Array.isArray(root.example) + ]; + }); + expect(result).to.deep.equal([123, 456, 789, false]); }); - const result = await callWithBindings((root: any) => { - return root.example.getObject()[root.example.getSymbol()]; - }); - expect(result).to.equal(123, 'symbols key lookup should work across contexts'); - }); - it('should proxy typed arrays', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', new Uint8Array(100)); - }); - const result = await callWithBindings((root: any) => { - return Object.getPrototypeOf(root.example) === Uint8Array.prototype; + it('it should proxy null', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', null); + }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return `${root.example}`; + }); + expect(result).to.deep.equal('null'); }); - expect(result).equal(true); - }); - it('should proxy regexps', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', /a/g); - }); - const result = await callWithBindings((root: any) => { - return Object.getPrototypeOf(root.example) === RegExp.prototype; + it('it should proxy undefined', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', undefined); + }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return `${root.example}`; + }); + expect(result).to.deep.equal('undefined'); }); - expect(result).equal(true); - }); - it('should proxy typed arrays and regexps through the serializer', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - arr: new Uint8Array(100), - regexp: /a/g + it('it should proxy nested null and undefined correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + values: [null, undefined] + }); }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return root.example.values.map((val: any) => `${val}`); + }); + expect(result).to.deep.equal(['null', 'undefined']); }); - const result = await callWithBindings((root: any) => { - return [ - Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype, - Object.getPrototypeOf(root.example.regexp) === RegExp.prototype - ]; - }); - expect(result).to.deep.equal([true, true]); - }); - it('should handle recursive objects', async () => { - await makeBindingWindow(() => { - const o: any = { value: 135 }; - o.o = o; - contextBridge.exposeInMainWorld('example', { - o + it('should proxy symbols', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + const isSymbol = (s: Symbol) => s === mySymbol; + contextBridge.exposeInMainWorld('symbol', mySymbol); + contextBridge.exposeInMainWorld('isSymbol', isSymbol); }); + const result = await callWithBindings((root: any) => { + return root.isSymbol(root.symbol); + }); + expect(result).to.equal( + true, + 'symbols should be equal across contexts' + ); }); - const result = await callWithBindings((root: any) => { - return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value]; - }); - expect(result).to.deep.equal([135, 135, 135]); - }); - it('should handle DOM elements', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getElem: () => document.body + it('should proxy symbols such that symbol equality works', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + contextBridge.exposeInMainWorld('example', { + getSymbol: () => mySymbol, + isSymbol: (s: Symbol) => s === mySymbol + }); }); + const result = await callWithBindings((root: any) => { + return root.example.isSymbol(root.example.getSymbol()); + }); + expect(result).to.equal( + true, + 'symbols should be equal across contexts' + ); }); - const result = await callWithBindings((root: any) => { - return [root.example.getElem().tagName, root.example.getElem().constructor.name, typeof root.example.getElem().querySelector]; - }); - expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); - }); - it('should handle DOM elements going backwards over the bridge', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getElemInfo: (fn: Function) => { - const elem = fn(); - return [elem.tagName, elem.constructor.name, typeof elem.querySelector]; - } + it('should proxy symbols such that symbol key lookup works', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + contextBridge.exposeInMainWorld('example', { + getSymbol: () => mySymbol, + getObject: () => ({ [mySymbol]: 123 }) + }); }); + const result = await callWithBindings((root: any) => { + return root.example.getObject()[root.example.getSymbol()]; + }); + expect(result).to.equal( + 123, + 'symbols key lookup should work across contexts' + ); }); - const result = await callWithBindings((root: any) => { - return root.example.getElemInfo(() => document.body); - }); - expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); - }); - it('should handle Blobs', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getBlob: () => new Blob(['ab', 'cd']) + it('should proxy typed arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', new Uint8Array(100)); }); + const result = await callWithBindings((root: any) => { + return Object.getPrototypeOf(root.example) === Uint8Array.prototype; + }); + expect(result).equal(true); }); - const result = await callWithBindings(async (root: any) => { - return [await root.example.getBlob().text()]; - }); - expect(result).to.deep.equal(['abcd']); - }); - it('should handle Blobs going backwards over the bridge', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - getBlobText: async (fn: Function) => { - const blob = fn(); - return [await blob.text()]; - } + it('should proxy regexps', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', /a/g); }); + const result = await callWithBindings((root: any) => { + return Object.getPrototypeOf(root.example) === RegExp.prototype; + }); + expect(result).equal(true); }); - const result = await callWithBindings((root: any) => { - return root.example.getBlobText(() => new Blob(['12', '45'])); - }); - expect(result).to.deep.equal(['1245']); - }); - // Can only run tests which use the GCRunner in non-sandboxed environments - if (!useSandbox) { - it('should release the global hold on methods sent across contexts', async () => { + it('should proxy typed arrays and regexps through the serializer', async () => { await makeBindingWindow(() => { - const trackedValues: WeakRef[] = []; - require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length })); contextBridge.exposeInMainWorld('example', { - getFunction: () => () => 123, - track: (value: object) => { trackedValues.push(new WeakRef(value)); } + arr: new Uint8Array(100), + regexp: /a/g }); }); - await callWithBindings(async (root: any) => { - root.GCRunner.run(); + const result = await callWithBindings((root: any) => { + return [ + Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype, + Object.getPrototypeOf(root.example.regexp) === RegExp.prototype + ]; }); - expect((await getGCInfo()).trackedValues).to.equal(0); - await callWithBindings(async (root: any) => { - const fn = root.example.getFunction(); - root.example.track(fn); - root.x = [fn]; + expect(result).to.deep.equal([true, true]); + }); + + it('should handle recursive objects', async () => { + await makeBindingWindow(() => { + const o: any = { value: 135 }; + o.o = o; + contextBridge.exposeInMainWorld('example', { + o + }); }); - expect((await getGCInfo()).trackedValues).to.equal(1); - await callWithBindings(async (root: any) => { - root.x = []; - root.GCRunner.run(); + const result = await callWithBindings((root: any) => { + return [ + root.example.o.value, + root.example.o.o.value, + root.example.o.o.o.value + ]; }); - expect((await getGCInfo()).trackedValues).to.equal(0); + expect(result).to.deep.equal([135, 135, 135]); }); - } - if (useSandbox) { - it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => { + it('should handle DOM elements', async () => { await makeBindingWindow(() => { - const trackedValues: WeakRef[] = []; - require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length })); contextBridge.exposeInMainWorld('example', { - getFunction: () => () => 123, - track: (value: object) => { trackedValues.push(new WeakRef(value)); } + getElem: () => document.body }); - require('electron').ipcRenderer.send('window-ready-for-tasking'); - }); - const loadPromise = emittedOnce(ipcMain, 'window-ready-for-tasking'); - expect((await getGCInfo()).trackedValues).to.equal(0); - await callWithBindings((root: any) => { - root.example.track(root.example.getFunction()); }); - expect((await getGCInfo()).trackedValues).to.equal(1); - await callWithBindings((root: any) => { - root.location.reload(); + const result = await callWithBindings((root: any) => { + return [ + root.example.getElem().tagName, + root.example.getElem().constructor.name, + typeof root.example.getElem().querySelector + ]; }); - await loadPromise; - await forceGCOnWindow(); - // If this is ever "2" it means we leaked the exposed function and - // therefore the entire context after a reload - expect((await getGCInfo()).trackedValues).to.equal(0); + expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); }); - } - it('it should not let you overwrite existing exposed things', async () => { - await makeBindingWindow(() => { - let threw = false; - contextBridge.exposeInMainWorld('example', { - attempt: 1, - getThrew: () => threw - }); - try { + it('should handle DOM elements going backwards over the bridge', async () => { + await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', { - attempt: 2, - getThrew: () => threw + getElemInfo: (fn: Function) => { + const elem = fn(); + return [ + elem.tagName, + elem.constructor.name, + typeof elem.querySelector + ]; + } }); - } catch { - threw = true; + }); + const result = await callWithBindings((root: any) => { + return root.example.getElemInfo(() => document.body); + }); + expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); + }); + + it('should handle Blobs', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getBlob: () => new Blob(['ab', 'cd']) + }); + }); + const result = await callWithBindings(async (root: any) => { + return [await root.example.getBlob().text()]; + }); + expect(result).to.deep.equal(['abcd']); + }); + + it('should handle Blobs going backwards over the bridge', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getBlobText: async (fn: Function) => { + const blob = fn(); + return [await blob.text()]; + } + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.getBlobText(() => new Blob(['12', '45'])); + }); + expect(result).to.deep.equal(['1245']); + }); + + // Can only run tests which use the GCRunner in non-sandboxed environments + if (!useSandbox) { + it('should release the global hold on methods sent across contexts', async () => { + await makeBindingWindow(() => { + const trackedValues: WeakRef[] = []; + require('electron').ipcRenderer.on('get-gc-info', (e) => + e.sender.send('gc-info', { + trackedValues: trackedValues.filter((value) => value.deref()) + .length + }) + ); + contextBridge.exposeInMainWorld('example', { + getFunction: () => () => 123, + track: (value: object) => { + trackedValues.push(new WeakRef(value)); + } + }); + }); + await callWithBindings(async (root: any) => { + root.GCRunner.run(); + }); + expect((await getGCInfo()).trackedValues).to.equal(0); + await callWithBindings(async (root: any) => { + const fn = root.example.getFunction(); + root.example.track(fn); + root.x = [fn]; + }); + expect((await getGCInfo()).trackedValues).to.equal(1); + await callWithBindings(async (root: any) => { + root.x = []; + root.GCRunner.run(); + }); + expect((await getGCInfo()).trackedValues).to.equal(0); + }); + } + + if (useSandbox) { + it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => { + await makeBindingWindow(() => { + const trackedValues: WeakRef[] = []; + require('electron').ipcRenderer.on('get-gc-info', (e) => + e.sender.send('gc-info', { + trackedValues: trackedValues.filter((value) => value.deref()) + .length + }) + ); + contextBridge.exposeInMainWorld('example', { + getFunction: () => () => 123, + track: (value: object) => { + trackedValues.push(new WeakRef(value)); + } + }); + require('electron').ipcRenderer.send('window-ready-for-tasking'); + }); + const loadPromise = emittedOnce( + ipcMain, + 'window-ready-for-tasking' + ); + expect((await getGCInfo()).trackedValues).to.equal(0); + await callWithBindings((root: any) => { + root.example.track(root.example.getFunction()); + }); + expect((await getGCInfo()).trackedValues).to.equal(1); + await callWithBindings((root: any) => { + root.location.reload(); + }); + await loadPromise; + await forceGCOnWindow(); + // If this is ever "2" it means we leaked the exposed function and + // therefore the entire context after a reload + expect((await getGCInfo()).trackedValues).to.equal(0); + }); + } + + it('it should not let you overwrite existing exposed things', async () => { + await makeBindingWindow(() => { + let threw = false; + contextBridge.exposeInMainWorld('example', { + attempt: 1, + getThrew: () => threw + }); + try { + contextBridge.exposeInMainWorld('example', { + attempt: 2, + getThrew: () => threw + }); + } catch { + threw = true; + } + }); + const result = await callWithBindings((root: any) => { + return [root.example.attempt, root.example.getThrew()]; + }); + expect(result).to.deep.equal([1, true]); + }); + + it('should work with complex nested methods and promises', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + first: (second: Function) => + second((fourth: Function) => { + return fourth(); + }) + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.first((third: Function) => { + return third(() => Promise.resolve('final value')); + }); + }); + expect(result).to.equal('final value'); + }); + + it('should work with complex nested methods and promises attached directly to the global', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', (second: Function) => + second((fourth: Function) => { + return fourth(); + }) + ); + }); + const result = await callWithBindings((root: any) => { + return root.example((third: Function) => { + return third(() => Promise.resolve('final value')); + }); + }); + expect(result).to.equal('final value'); + }); + + it('should throw an error when recursion depth is exceeded', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + doThing: (a: any) => console.log(a) + }); + }); + let threw = await callWithBindings((root: any) => { + try { + let a: any = []; + for (let i = 0; i < 999; i++) { + a = [a]; + } + root.example.doThing(a); + return false; + } catch { + return true; + } + }); + expect(threw).to.equal(false); + threw = await callWithBindings((root: any) => { + try { + let a: any = []; + for (let i = 0; i < 1000; i++) { + a = [a]; + } + root.example.doThing(a); + return false; + } catch { + return true; + } + }); + expect(threw).to.equal(true); + }); + + it('should copy thrown errors into the other context', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + throwNormal: () => { + throw new Error('whoops'); + }, + throwWeird: () => { + throw 'this is no error...'; // eslint-disable-line no-throw-literal + }, + throwNotClonable: () => { + return Object(Symbol('foo')); + }, + argumentConvert: () => {} + }); + }); + const result = await callWithBindings((root: any) => { + const getError = (fn: Function) => { + try { + fn(); + } catch (e) { + return e; + } + return null; + }; + const normalIsError = + Object.getPrototypeOf(getError(root.example.throwNormal)) === + Error.prototype; + const weirdIsError = + Object.getPrototypeOf(getError(root.example.throwWeird)) === + Error.prototype; + const notClonableIsError = + Object.getPrototypeOf(getError(root.example.throwNotClonable)) === + Error.prototype; + const argumentConvertIsError = + Object.getPrototypeOf( + getError(() => + root.example.argumentConvert(Object(Symbol('test'))) + ) + ) === Error.prototype; + return [ + normalIsError, + weirdIsError, + notClonableIsError, + argumentConvertIsError + ]; + }); + expect(result).to.deep.equal( + [true, true, true, true], + 'should all be errors in the current context' + ); + }); + + it('should not leak prototypes', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + symbol: Symbol('foo'), + bigInt: 10n, + getObject: () => ({ thing: 123 }), + getNumber: () => 123, + getString: () => 'string', + getBoolean: () => true, + getArr: () => [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }), + getFunctionFromFunction: async () => () => null, + object: { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }) + }, + receiveArguments: (fn: any) => fn({ key: 'value' }), + symbolKeyed: { + [Symbol('foo')]: 123 + }, + getBody: () => document.body, + getBlob: () => new Blob(['ab', 'cd']) + }); + }); + const result = await callWithBindings(async (root: any) => { + const { example } = root; + let arg: any; + example.receiveArguments((o: any) => { + arg = o; + }); + const protoChecks = [ + ...Object.keys(example).map((key) => [key, String]), + ...Object.getOwnPropertySymbols(example.symbolKeyed).map( + (key) => [key, Symbol] + ), + [example, Object], + [example.number, Number], + [example.string, String], + [example.boolean, Boolean], + [example.arr, Array], + [example.arr[0], Number], + [example.arr[1], String], + [example.arr[2], Boolean], + [example.arr[3], Array], + [example.arr[3][0], String], + [example.symbol, Symbol], + [example.bigInt, BigInt], + [example.getNumber, Function], + [example.getNumber(), Number], + [example.getObject(), Object], + [example.getString(), String], + [example.getBoolean(), Boolean], + [example.getArr(), Array], + [example.getArr()[0], Number], + [example.getArr()[1], String], + [example.getArr()[2], Boolean], + [example.getArr()[3], Array], + [example.getArr()[3][0], String], + [example.getFunctionFromFunction, Function], + [example.getFunctionFromFunction(), Promise], + [await example.getFunctionFromFunction(), Function], + [example.getPromise(), Promise], + [await example.getPromise(), Object], + [(await example.getPromise()).number, Number], + [(await example.getPromise()).string, String], + [(await example.getPromise()).boolean, Boolean], + [(await example.getPromise()).fn, Function], + [(await example.getPromise()).fn(), String], + [(await example.getPromise()).arr, Array], + [(await example.getPromise()).arr[0], Number], + [(await example.getPromise()).arr[1], String], + [(await example.getPromise()).arr[2], Boolean], + [(await example.getPromise()).arr[3], Array], + [(await example.getPromise()).arr[3][0], String], + [example.object, Object], + [example.object.number, Number], + [example.object.string, String], + [example.object.boolean, Boolean], + [example.object.arr, Array], + [example.object.arr[0], Number], + [example.object.arr[1], String], + [example.object.arr[2], Boolean], + [example.object.arr[3], Array], + [example.object.arr[3][0], String], + [await example.object.getPromise(), Object], + [(await example.object.getPromise()).number, Number], + [(await example.object.getPromise()).string, String], + [(await example.object.getPromise()).boolean, Boolean], + [(await example.object.getPromise()).fn, Function], + [(await example.object.getPromise()).fn(), String], + [(await example.object.getPromise()).arr, Array], + [(await example.object.getPromise()).arr[0], Number], + [(await example.object.getPromise()).arr[1], String], + [(await example.object.getPromise()).arr[2], Boolean], + [(await example.object.getPromise()).arr[3], Array], + [(await example.object.getPromise()).arr[3][0], String], + [arg, Object], + [arg.key, String], + [example.getBody(), HTMLBodyElement], + [example.getBlob(), Blob] + ]; + return { + protoMatches: protoChecks.map( + ([a, Constructor]) => + Object.getPrototypeOf(a) === Constructor.prototype + ) + }; + }); + // Every protomatch should be true + expect(result.protoMatches).to.deep.equal( + result.protoMatches.map(() => true) + ); + }); + + it('should not leak prototypes when attaching directly to the global', async () => { + await makeBindingWindow(() => { + const toExpose = { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + symbol: Symbol('foo'), + bigInt: 10n, + getObject: () => ({ thing: 123 }), + getNumber: () => 123, + getString: () => 'string', + getBoolean: () => true, + getArr: () => [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }), + getFunctionFromFunction: async () => () => null, + getError: () => new Error('foo'), + getWeirdError: () => { + const e = new Error('foo'); + e.message = { garbage: true } as any; + return e; + }, + object: { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }) + }, + receiveArguments: (fn: any) => fn({ key: 'value' }), + symbolKeyed: { + [Symbol('foo')]: 123 + } + }; + const keys: string[] = []; + Object.entries(toExpose).forEach(([key, value]) => { + keys.push(key); + contextBridge.exposeInMainWorld(key, value); + }); + contextBridge.exposeInMainWorld('keys', keys); + }); + const result = await callWithBindings(async (root: any) => { + const { keys } = root; + const cleanedRoot: any = {}; + for (const [key, value] of Object.entries(root)) { + if (keys.includes(key)) { + cleanedRoot[key] = value; + } + } + + let arg: any; + cleanedRoot.receiveArguments((o: any) => { + arg = o; + }); + const protoChecks = [ + ...Object.keys(cleanedRoot).map((key) => [key, String]), + ...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map( + (key) => [key, Symbol] + ), + [cleanedRoot, Object], + [cleanedRoot.number, Number], + [cleanedRoot.string, String], + [cleanedRoot.boolean, Boolean], + [cleanedRoot.arr, Array], + [cleanedRoot.arr[0], Number], + [cleanedRoot.arr[1], String], + [cleanedRoot.arr[2], Boolean], + [cleanedRoot.arr[3], Array], + [cleanedRoot.arr[3][0], String], + [cleanedRoot.symbol, Symbol], + [cleanedRoot.bigInt, BigInt], + [cleanedRoot.getNumber, Function], + [cleanedRoot.getNumber(), Number], + [cleanedRoot.getObject(), Object], + [cleanedRoot.getString(), String], + [cleanedRoot.getBoolean(), Boolean], + [cleanedRoot.getArr(), Array], + [cleanedRoot.getArr()[0], Number], + [cleanedRoot.getArr()[1], String], + [cleanedRoot.getArr()[2], Boolean], + [cleanedRoot.getArr()[3], Array], + [cleanedRoot.getArr()[3][0], String], + [cleanedRoot.getFunctionFromFunction, Function], + [cleanedRoot.getFunctionFromFunction(), Promise], + [await cleanedRoot.getFunctionFromFunction(), Function], + [cleanedRoot.getError(), Error], + [cleanedRoot.getError().message, String], + [cleanedRoot.getWeirdError(), Error], + [cleanedRoot.getWeirdError().message, String], + [cleanedRoot.getPromise(), Promise], + [await cleanedRoot.getPromise(), Object], + [(await cleanedRoot.getPromise()).number, Number], + [(await cleanedRoot.getPromise()).string, String], + [(await cleanedRoot.getPromise()).boolean, Boolean], + [(await cleanedRoot.getPromise()).fn, Function], + [(await cleanedRoot.getPromise()).fn(), String], + [(await cleanedRoot.getPromise()).arr, Array], + [(await cleanedRoot.getPromise()).arr[0], Number], + [(await cleanedRoot.getPromise()).arr[1], String], + [(await cleanedRoot.getPromise()).arr[2], Boolean], + [(await cleanedRoot.getPromise()).arr[3], Array], + [(await cleanedRoot.getPromise()).arr[3][0], String], + [cleanedRoot.object, Object], + [cleanedRoot.object.number, Number], + [cleanedRoot.object.string, String], + [cleanedRoot.object.boolean, Boolean], + [cleanedRoot.object.arr, Array], + [cleanedRoot.object.arr[0], Number], + [cleanedRoot.object.arr[1], String], + [cleanedRoot.object.arr[2], Boolean], + [cleanedRoot.object.arr[3], Array], + [cleanedRoot.object.arr[3][0], String], + [await cleanedRoot.object.getPromise(), Object], + [(await cleanedRoot.object.getPromise()).number, Number], + [(await cleanedRoot.object.getPromise()).string, String], + [(await cleanedRoot.object.getPromise()).boolean, Boolean], + [(await cleanedRoot.object.getPromise()).fn, Function], + [(await cleanedRoot.object.getPromise()).fn(), String], + [(await cleanedRoot.object.getPromise()).arr, Array], + [(await cleanedRoot.object.getPromise()).arr[0], Number], + [(await cleanedRoot.object.getPromise()).arr[1], String], + [(await cleanedRoot.object.getPromise()).arr[2], Boolean], + [(await cleanedRoot.object.getPromise()).arr[3], Array], + [(await cleanedRoot.object.getPromise()).arr[3][0], String], + [arg, Object], + [arg.key, String] + ]; + return { + protoMatches: protoChecks.map( + ([a, Constructor]) => + Object.getPrototypeOf(a) === Constructor.prototype + ) + }; + }); + // Every protomatch should be true + expect(result.protoMatches).to.deep.equal( + result.protoMatches.map(() => true) + ); + }); + }); + }); + describe('exposeInIsolatedWorld', () => { + const makeBindingWindow = async (bindingCreator: Function) => { + const preloadContent = `const renderer_1 = require('electron'); + ${ + useSandbox + ? '' + : `require('v8').setFlagsFromString('--expose_gc'); + const gc=require('vm').runInNewContext('gc'); + renderer_1.webFrame.setIsolatedWorldInfo(1004, { + name: "Isolated World" + }); + renderer_1.contextBridge.exposeInIsolatedWorld(1004,'GCRunner', { + run: () => gc() + });` + } + (${bindingCreator.toString()})();`; + const tmpDir = await fs.mkdtemp( + path.resolve(os.tmpdir(), 'electron-spec-preload-') + ); + dir = tmpDir; + await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent); + w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: true, + sandbox: useSandbox, + preload: path.resolve(tmpDir, 'preload.js'), + additionalArguments: [ + '--unsafely-expose-electron-internals-for-testing' + ] } }); - const result = await callWithBindings((root: any) => { - return [root.example.attempt, root.example.getThrew()]; + await w.loadURL( + `http://127.0.0.1:${(server.address() as AddressInfo).port}` + ); + }; + const callWithBindings = (fn: Function) => + w.webContents.executeJavaScriptInIsolatedWorld(1004, [{ + code: `(${fn.toString()})(window)` + }]); + describe(`with sandbox=${useSandbox}`, () => { + it('should proxy numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', 123); + }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal(123); + }); + + it('should make global properties read-only', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', 123); + }); + const result = await callWithBindings((root: any) => { + root.example = 456; + return root.example; + }); + expect(result).to.equal(123); + }); + + it('should proxy nested numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myNumber: 123 + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myNumber; + }); + expect(result).to.equal(123); + }); + + it('should make properties unwriteable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myNumber: 123 + }); + }); + const result = await callWithBindings((root: any) => { + root.example.myNumber = 456; + return root.example.myNumber; + }); + expect(result).to.equal(123); + }); + + it('should proxy strings', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', 'my-words'); + }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal('my-words'); + }); + + it('should proxy nested strings', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myString: 'my-words' + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myString; + }); + expect(result).to.equal('my-words'); + }); + + it('should proxy arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', [ + 123, + 'my-words' + ]); + }); + const result = await callWithBindings((root: any) => { + return [root.example, Array.isArray(root.example)]; + }); + expect(result).to.deep.equal([[123, 'my-words'], true]); + }); + + it('should proxy nested arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myArr: [123, 'my-words'] + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myArr; + }); + expect(result).to.deep.equal([123, 'my-words']); + }); + + it('should make arrays immutable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', [ + 123, + 'my-words' + ]); + }); + const immutable = await callWithBindings((root: any) => { + try { + root.example.push(456); + return false; + } catch { + return true; + } + }); + expect(immutable).to.equal(true); + }); + + it('should make nested arrays immutable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myArr: [123, 'my-words'] + }); + }); + const immutable = await callWithBindings((root: any) => { + try { + root.example.myArr.push(456); + return false; + } catch { + return true; + } + }); + expect(immutable).to.equal(true); + }); + + it('should proxy booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', true); + }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal(true); + }); + + it('should proxy nested booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myBool: true + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myBool; + }); + expect(result).to.equal(true); + }); + + it('should proxy promises and resolve with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + Promise.resolve('i-resolved') + ); + }); + const result = await callWithBindings((root: any) => { + return root.example; + }); + expect(result).to.equal('i-resolved'); + }); + + it('should proxy nested promises and resolve with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myPromise: Promise.resolve('i-resolved') + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise; + }); + expect(result).to.equal('i-resolved'); + }); + + it('should proxy promises and reject with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + Promise.reject(new Error('i-rejected')) + ); + }); + const result = await callWithBindings(async (root: any) => { + try { + await root.example; + return null; + } catch (err) { + return err; + } + }); + expect(result) + .to.be.an.instanceOf(Error) + .with.property('message', 'i-rejected'); + }); + + it('should proxy nested promises and reject with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myPromise: Promise.reject(new Error('i-rejected')) + }); + }); + const result = await callWithBindings(async (root: any) => { + try { + await root.example.myPromise; + return null; + } catch (err) { + return err; + } + }); + expect(result) + .to.be.an.instanceOf(Error) + .with.property('message', 'i-rejected'); + }); + + it('should proxy promises and resolve with the correct value if it resolves later', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myPromise: () => + new Promise((resolve) => + setTimeout(() => resolve('delayed'), 20) + ) + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise(); + }); + expect(result).to.equal('delayed'); + }); + + it('should proxy nested promises correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myPromise: () => + new Promise((resolve) => + setTimeout(() => resolve(Promise.resolve(123)), 20) + ) + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.myPromise(); + }); + expect(result).to.equal(123); + }); + + it('should proxy methods', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getNumber: () => 123, + getString: () => 'help', + getBoolean: () => false, + getPromise: async () => 'promise' + }); + }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example.getNumber(), + root.example.getString(), + root.example.getBoolean(), + await root.example.getPromise() + ]; + }); + expect(result).to.deep.equal([123, 'help', false, 'promise']); + }); + + it('should proxy functions', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + () => 'return-value' + ); + }); + const result = await callWithBindings(async (root: any) => { + return root.example(); + }); + expect(result).equal('return-value'); + }); + + it('should not double-proxy functions when they are returned to their origin side of the bridge', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + (fn: any) => fn + ); + }); + const result = await callWithBindings(async (root: any) => { + const fn = () => null; + return root.example(fn) === fn; + }); + expect(result).equal(true); + }); + + it('should properly handle errors thrown in proxied functions', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', () => { + throw new Error('oh no'); + }); + }); + const result = await callWithBindings(async (root: any) => { + try { + root.example(); + } catch (e) { + return (e as Error).message; + } + }); + expect(result).equal('oh no'); + }); + + it('should proxy methods that are callable multiple times', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + doThing: () => 123 + }); + }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example.doThing(), + root.example.doThing(), + root.example.doThing() + ]; + }); + expect(result).to.deep.equal([123, 123, 123]); + }); + + it('should proxy methods in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + callWithNumber: (fn: any) => fn(123) + }); + }); + const result = await callWithBindings(async (root: any) => { + return root.example.callWithNumber((n: number) => n + 1); + }); + expect(result).to.equal(124); + }); + + it('should proxy promises in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getPromiseValue: (p: Promise) => p + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.getPromiseValue( + Promise.resolve('my-proxied-value') + ); + }); + expect(result).to.equal('my-proxied-value'); + }); + + it('should proxy objects with number keys', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + 1: 123, + 2: 456, + 3: 789 + }); + }); + const result = await callWithBindings(async (root: any) => { + return [ + root.example[1], + root.example[2], + root.example[3], + Array.isArray(root.example) + ]; + }); + expect(result).to.deep.equal([123, 456, 789, false]); }); - expect(result).to.deep.equal([1, true]); - }); - it('should work with complex nested methods and promises', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - first: (second: Function) => second((fourth: Function) => { - return fourth(); - }) + it('it should proxy null', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', null); }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return `${root.example}`; + }); + expect(result).to.deep.equal('null'); }); - const result = await callWithBindings((root: any) => { - return root.example.first((third: Function) => { - return third(() => Promise.resolve('final value')); + + it('it should proxy undefined', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', undefined); + }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return `${root.example}`; }); + expect(result).to.deep.equal('undefined'); + }); + + it('it should proxy nested null and undefined correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + values: [null, undefined] + }); + }); + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return root.example.values.map((val: any) => `${val}`); + }); + expect(result).to.deep.equal(['null', 'undefined']); }); - expect(result).to.equal('final value'); - }); - it('should work with complex nested methods and promises attached directly to the global', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', - (second: Function) => second((fourth: Function) => { - return fourth(); - }) + it('should proxy symbols', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + const isSymbol = (s: Symbol) => s === mySymbol; + contextBridge.exposeInIsolatedWorld(1004, 'symbol', mySymbol); + contextBridge.exposeInIsolatedWorld(1004, 'isSymbol', isSymbol); + }); + const result = await callWithBindings((root: any) => { + return root.isSymbol(root.symbol); + }); + expect(result).to.equal( + true, + 'symbols should be equal across contexts' ); }); - const result = await callWithBindings((root: any) => { - return root.example((third: Function) => { - return third(() => Promise.resolve('final value')); + + it('should proxy symbols such that symbol equality works', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getSymbol: () => mySymbol, + isSymbol: (s: Symbol) => s === mySymbol + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.isSymbol(root.example.getSymbol()); }); + expect(result).to.equal( + true, + 'symbols should be equal across contexts' + ); + }); + + it('should proxy symbols such that symbol key lookup works', async () => { + await makeBindingWindow(() => { + const mySymbol = Symbol('unique'); + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getSymbol: () => mySymbol, + getObject: () => ({ [mySymbol]: 123 }) + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.getObject()[root.example.getSymbol()]; + }); + expect(result).to.equal( + 123, + 'symbols key lookup should work across contexts' + ); }); - expect(result).to.equal('final value'); - }); - it('should throw an error when recursion depth is exceeded', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - doThing: (a: any) => console.log(a) + it('should proxy typed arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + new Uint8Array(100) + ); }); + const result = await callWithBindings((root: any) => { + return Object.getPrototypeOf(root.example) === Uint8Array.prototype; + }); + expect(result).equal(true); }); - let threw = await callWithBindings((root: any) => { - try { - let a: any = []; - for (let i = 0; i < 999; i++) { - a = [a]; - } - root.example.doThing(a); - return false; - } catch { - return true; - } + + it('should proxy regexps', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', /a/g); + }); + const result = await callWithBindings((root: any) => { + return Object.getPrototypeOf(root.example) === RegExp.prototype; + }); + expect(result).equal(true); + }); + + it('should proxy typed arrays and regexps through the serializer', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + arr: new Uint8Array(100), + regexp: /a/g + }); + }); + const result = await callWithBindings((root: any) => { + return [ + Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype, + Object.getPrototypeOf(root.example.regexp) === RegExp.prototype + ]; + }); + expect(result).to.deep.equal([true, true]); + }); + + it('should handle recursive objects', async () => { + await makeBindingWindow(() => { + const o: any = { value: 135 }; + o.o = o; + contextBridge.exposeInIsolatedWorld(1004, 'example', { + o + }); + }); + const result = await callWithBindings((root: any) => { + return [ + root.example.o.value, + root.example.o.o.value, + root.example.o.o.o.value + ]; + }); + expect(result).to.deep.equal([135, 135, 135]); + }); + + it('should handle DOM elements', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getElem: () => document.body + }); + }); + const result = await callWithBindings((root: any) => { + return [ + root.example.getElem().tagName, + root.example.getElem().constructor.name, + typeof root.example.getElem().querySelector + ]; + }); + expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); + }); + + it('should handle DOM elements going backwards over the bridge', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getElemInfo: (fn: Function) => { + const elem = fn(); + return [ + elem.tagName, + elem.constructor.name, + typeof elem.querySelector + ]; + } + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.getElemInfo(() => document.body); + }); + expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']); + }); + + it('should handle Blobs', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getBlob: () => new Blob(['ab', 'cd']) + }); + }); + const result = await callWithBindings(async (root: any) => { + return [await root.example.getBlob().text()]; + }); + expect(result).to.deep.equal(['abcd']); + }); + + it('should handle Blobs going backwards over the bridge', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getBlobText: async (fn: Function) => { + const blob = fn(); + return [await blob.text()]; + } + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.getBlobText(() => new Blob(['12', '45'])); + }); + expect(result).to.deep.equal(['1245']); }); - expect(threw).to.equal(false); - threw = await callWithBindings((root: any) => { - try { - let a: any = []; - for (let i = 0; i < 1000; i++) { - a = [a]; + + // Can only run tests which use the GCRunner in non-sandboxed environments + if (!useSandbox) { + it('should release the global hold on methods sent across contexts', async () => { + await makeBindingWindow(() => { + const trackedValues: WeakRef[] = []; + require('electron').ipcRenderer.on('get-gc-info', (e) => + e.sender.send('gc-info', { + trackedValues: trackedValues.filter((value) => value.deref()) + .length + }) + ); + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getFunction: () => () => 123, + track: (value: object) => { + trackedValues.push(new WeakRef(value)); + } + }); + }); + await callWithBindings(async (root: any) => { + root.GCRunner.run(); + }); + expect((await getGCInfo()).trackedValues).to.equal(0); + await callWithBindings(async (root: any) => { + const fn = root.example.getFunction(); + root.example.track(fn); + root.x = [fn]; + }); + expect((await getGCInfo()).trackedValues).to.equal(1); + await callWithBindings(async (root: any) => { + root.x = []; + root.GCRunner.run(); + }); + expect((await getGCInfo()).trackedValues).to.equal(0); + }); + } + + if (useSandbox) { + it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => { + await makeBindingWindow(() => { + const trackedValues: WeakRef[] = []; + require('electron').ipcRenderer.on('get-gc-info', (e) => + e.sender.send('gc-info', { + trackedValues: trackedValues.filter((value) => value.deref()) + .length + }) + ); + contextBridge.exposeInIsolatedWorld(1004, 'example', { + getFunction: () => () => 123, + track: (value: object) => { + trackedValues.push(new WeakRef(value)); + } + }); + require('electron').ipcRenderer.send('window-ready-for-tasking'); + }); + const loadPromise = emittedOnce( + ipcMain, + 'window-ready-for-tasking' + ); + expect((await getGCInfo()).trackedValues).to.equal(0); + await callWithBindings((root: any) => { + root.example.track(root.example.getFunction()); + }); + expect((await getGCInfo()).trackedValues).to.equal(1); + await callWithBindings((root: any) => { + root.location.reload(); + }); + await loadPromise; + await forceGCOnWindow(); + // If this is ever "2" it means we leaked the exposed function and + // therefore the entire context after a reload + expect((await getGCInfo()).trackedValues).to.equal(0); + }); + } + + it('it should not let you overwrite existing exposed things', async () => { + await makeBindingWindow(() => { + let threw = false; + contextBridge.exposeInIsolatedWorld(1004, 'example', { + attempt: 1, + getThrew: () => threw + }); + try { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + attempt: 2, + getThrew: () => threw + }); + } catch { + threw = true; } - root.example.doThing(a); - return false; - } catch { - return true; - } + }); + const result = await callWithBindings((root: any) => { + return [root.example.attempt, root.example.getThrew()]; + }); + expect(result).to.deep.equal([1, true]); + }); + + it('should work with complex nested methods and promises', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + first: (second: Function) => + second((fourth: Function) => { + return fourth(); + }) + }); + }); + const result = await callWithBindings((root: any) => { + return root.example.first((third: Function) => { + return third(() => Promise.resolve('final value')); + }); + }); + expect(result).to.equal('final value'); + }); + + it('should work with complex nested methods and promises attached directly to the global', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld( + 1004, + 'example', + (second: Function) => + second((fourth: Function) => { + return fourth(); + }) + ); + }); + const result = await callWithBindings((root: any) => { + return root.example((third: Function) => { + return third(() => Promise.resolve('final value')); + }); + }); + expect(result).to.equal('final value'); }); - expect(threw).to.equal(true); - }); - it('should copy thrown errors into the other context', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - throwNormal: () => { - throw new Error('whoops'); - }, - throwWeird: () => { - throw 'this is no error...'; // eslint-disable-line no-throw-literal - }, - throwNotClonable: () => { - return Object(Symbol('foo')); - }, - argumentConvert: () => {} - }); - }); - const result = await callWithBindings((root: any) => { - const getError = (fn: Function) => { + it('should throw an error when recursion depth is exceeded', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + doThing: (a: any) => console.log(a) + }); + }); + let threw = await callWithBindings((root: any) => { try { - fn(); - } catch (e) { - return e; + let a: any = []; + for (let i = 0; i < 999; i++) { + a = [a]; + } + root.example.doThing(a); + return false; + } catch { + return true; } - return null; - }; - const normalIsError = Object.getPrototypeOf(getError(root.example.throwNormal)) === Error.prototype; - const weirdIsError = Object.getPrototypeOf(getError(root.example.throwWeird)) === Error.prototype; - const notClonableIsError = Object.getPrototypeOf(getError(root.example.throwNotClonable)) === Error.prototype; - const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype; - return [normalIsError, weirdIsError, notClonableIsError, argumentConvertIsError]; - }); - expect(result).to.deep.equal([true, true, true, true], 'should all be errors in the current context'); - }); + }); + expect(threw).to.equal(false); + threw = await callWithBindings((root: any) => { + try { + let a: any = []; + for (let i = 0; i < 1000; i++) { + a = [a]; + } + root.example.doThing(a); + return false; + } catch { + return true; + } + }); + expect(threw).to.equal(true); + }); + + it('should copy thrown errors into the other context', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + throwNormal: () => { + throw new Error('whoops'); + }, + throwWeird: () => { + throw 'this is no error...'; // eslint-disable-line no-throw-literal + }, + throwNotClonable: () => { + return Object(Symbol('foo')); + }, + argumentConvert: () => {} + }); + }); + const result = await callWithBindings((root: any) => { + const getError = (fn: Function) => { + try { + fn(); + } catch (e) { + return e; + } + return null; + }; + const normalIsError = + Object.getPrototypeOf(getError(root.example.throwNormal)) === + Error.prototype; + const weirdIsError = + Object.getPrototypeOf(getError(root.example.throwWeird)) === + Error.prototype; + const notClonableIsError = + Object.getPrototypeOf(getError(root.example.throwNotClonable)) === + Error.prototype; + const argumentConvertIsError = + Object.getPrototypeOf( + getError(() => + root.example.argumentConvert(Object(Symbol('test'))) + ) + ) === Error.prototype; + return [ + normalIsError, + weirdIsError, + notClonableIsError, + argumentConvertIsError + ]; + }); + expect(result).to.deep.equal( + [true, true, true, true], + 'should all be errors in the current context' + ); + }); - it('should not leak prototypes', async () => { - await makeBindingWindow(() => { - contextBridge.exposeInMainWorld('example', { - number: 123, - string: 'string', - boolean: true, - arr: [123, 'string', true, ['foo']], - symbol: Symbol('foo'), - bigInt: 10n, - getObject: () => ({ thing: 123 }), - getNumber: () => 123, - getString: () => 'string', - getBoolean: () => true, - getArr: () => [123, 'string', true, ['foo']], - getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }), - getFunctionFromFunction: async () => () => null, - object: { + it('should not leak prototypes', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { number: 123, string: 'string', boolean: true, arr: [123, 'string', true, ['foo']], - getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }) - }, - receiveArguments: (fn: any) => fn({ key: 'value' }), - symbolKeyed: { - [Symbol('foo')]: 123 - }, - getBody: () => document.body, - getBlob: () => new Blob(['ab', 'cd']) - }); - }); - const result = await callWithBindings(async (root: any) => { - const { example } = root; - let arg: any; - example.receiveArguments((o: any) => { arg = o; }); - const protoChecks = [ - ...Object.keys(example).map(key => [key, String]), - ...Object.getOwnPropertySymbols(example.symbolKeyed).map(key => [key, Symbol]), - [example, Object], - [example.number, Number], - [example.string, String], - [example.boolean, Boolean], - [example.arr, Array], - [example.arr[0], Number], - [example.arr[1], String], - [example.arr[2], Boolean], - [example.arr[3], Array], - [example.arr[3][0], String], - [example.symbol, Symbol], - [example.bigInt, BigInt], - [example.getNumber, Function], - [example.getNumber(), Number], - [example.getObject(), Object], - [example.getString(), String], - [example.getBoolean(), Boolean], - [example.getArr(), Array], - [example.getArr()[0], Number], - [example.getArr()[1], String], - [example.getArr()[2], Boolean], - [example.getArr()[3], Array], - [example.getArr()[3][0], String], - [example.getFunctionFromFunction, Function], - [example.getFunctionFromFunction(), Promise], - [await example.getFunctionFromFunction(), Function], - [example.getPromise(), Promise], - [await example.getPromise(), Object], - [(await example.getPromise()).number, Number], - [(await example.getPromise()).string, String], - [(await example.getPromise()).boolean, Boolean], - [(await example.getPromise()).fn, Function], - [(await example.getPromise()).fn(), String], - [(await example.getPromise()).arr, Array], - [(await example.getPromise()).arr[0], Number], - [(await example.getPromise()).arr[1], String], - [(await example.getPromise()).arr[2], Boolean], - [(await example.getPromise()).arr[3], Array], - [(await example.getPromise()).arr[3][0], String], - [example.object, Object], - [example.object.number, Number], - [example.object.string, String], - [example.object.boolean, Boolean], - [example.object.arr, Array], - [example.object.arr[0], Number], - [example.object.arr[1], String], - [example.object.arr[2], Boolean], - [example.object.arr[3], Array], - [example.object.arr[3][0], String], - [await example.object.getPromise(), Object], - [(await example.object.getPromise()).number, Number], - [(await example.object.getPromise()).string, String], - [(await example.object.getPromise()).boolean, Boolean], - [(await example.object.getPromise()).fn, Function], - [(await example.object.getPromise()).fn(), String], - [(await example.object.getPromise()).arr, Array], - [(await example.object.getPromise()).arr[0], Number], - [(await example.object.getPromise()).arr[1], String], - [(await example.object.getPromise()).arr[2], Boolean], - [(await example.object.getPromise()).arr[3], Array], - [(await example.object.getPromise()).arr[3][0], String], - [arg, Object], - [arg.key, String], - [example.getBody(), HTMLBodyElement], - [example.getBlob(), Blob] - ]; - return { - protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype) - }; - }); - // Every protomatch should be true - expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true)); - }); + symbol: Symbol('foo'), + bigInt: 10n, + getObject: () => ({ thing: 123 }), + getNumber: () => 123, + getString: () => 'string', + getBoolean: () => true, + getArr: () => [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }), + getFunctionFromFunction: async () => () => null, + object: { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }) + }, + receiveArguments: (fn: any) => fn({ key: 'value' }), + symbolKeyed: { + [Symbol('foo')]: 123 + }, + getBody: () => document.body, + getBlob: () => new Blob(['ab', 'cd']) + }); + }); + const result = await callWithBindings(async (root: any) => { + const { example } = root; + let arg: any; + example.receiveArguments((o: any) => { + arg = o; + }); + const protoChecks = [ + ...Object.keys(example).map((key) => [key, String]), + ...Object.getOwnPropertySymbols(example.symbolKeyed).map( + (key) => [key, Symbol] + ), + [example, Object], + [example.number, Number], + [example.string, String], + [example.boolean, Boolean], + [example.arr, Array], + [example.arr[0], Number], + [example.arr[1], String], + [example.arr[2], Boolean], + [example.arr[3], Array], + [example.arr[3][0], String], + [example.symbol, Symbol], + [example.bigInt, BigInt], + [example.getNumber, Function], + [example.getNumber(), Number], + [example.getObject(), Object], + [example.getString(), String], + [example.getBoolean(), Boolean], + [example.getArr(), Array], + [example.getArr()[0], Number], + [example.getArr()[1], String], + [example.getArr()[2], Boolean], + [example.getArr()[3], Array], + [example.getArr()[3][0], String], + [example.getFunctionFromFunction, Function], + [example.getFunctionFromFunction(), Promise], + [await example.getFunctionFromFunction(), Function], + [example.getPromise(), Promise], + [await example.getPromise(), Object], + [(await example.getPromise()).number, Number], + [(await example.getPromise()).string, String], + [(await example.getPromise()).boolean, Boolean], + [(await example.getPromise()).fn, Function], + [(await example.getPromise()).fn(), String], + [(await example.getPromise()).arr, Array], + [(await example.getPromise()).arr[0], Number], + [(await example.getPromise()).arr[1], String], + [(await example.getPromise()).arr[2], Boolean], + [(await example.getPromise()).arr[3], Array], + [(await example.getPromise()).arr[3][0], String], + [example.object, Object], + [example.object.number, Number], + [example.object.string, String], + [example.object.boolean, Boolean], + [example.object.arr, Array], + [example.object.arr[0], Number], + [example.object.arr[1], String], + [example.object.arr[2], Boolean], + [example.object.arr[3], Array], + [example.object.arr[3][0], String], + [await example.object.getPromise(), Object], + [(await example.object.getPromise()).number, Number], + [(await example.object.getPromise()).string, String], + [(await example.object.getPromise()).boolean, Boolean], + [(await example.object.getPromise()).fn, Function], + [(await example.object.getPromise()).fn(), String], + [(await example.object.getPromise()).arr, Array], + [(await example.object.getPromise()).arr[0], Number], + [(await example.object.getPromise()).arr[1], String], + [(await example.object.getPromise()).arr[2], Boolean], + [(await example.object.getPromise()).arr[3], Array], + [(await example.object.getPromise()).arr[3][0], String], + [arg, Object], + [arg.key, String], + [example.getBody(), HTMLBodyElement], + [example.getBlob(), Blob] + ]; + return { + protoMatches: protoChecks.map( + ([a, Constructor]) => + Object.getPrototypeOf(a) === Constructor.prototype + ) + }; + }); + // Every protomatch should be true + expect(result.protoMatches).to.deep.equal( + result.protoMatches.map(() => true) + ); + }); - it('should not leak prototypes when attaching directly to the global', async () => { - await makeBindingWindow(() => { - const toExpose = { - number: 123, - string: 'string', - boolean: true, - arr: [123, 'string', true, ['foo']], - symbol: Symbol('foo'), - bigInt: 10n, - getObject: () => ({ thing: 123 }), - getNumber: () => 123, - getString: () => 'string', - getBoolean: () => true, - getArr: () => [123, 'string', true, ['foo']], - getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }), - getFunctionFromFunction: async () => () => null, - getError: () => new Error('foo'), - getWeirdError: () => { - const e = new Error('foo'); - e.message = { garbage: true } as any; - return e; - }, - object: { + it('should not leak prototypes when attaching directly to the global', async () => { + await makeBindingWindow(() => { + const toExpose = { number: 123, string: 'string', boolean: true, arr: [123, 'string', true, ['foo']], - getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }) - }, - receiveArguments: (fn: any) => fn({ key: 'value' }), - symbolKeyed: { - [Symbol('foo')]: 123 - } - }; - const keys: string[] = []; - Object.entries(toExpose).forEach(([key, value]) => { - keys.push(key); - contextBridge.exposeInMainWorld(key, value); - }); - contextBridge.exposeInMainWorld('keys', keys); - }); - const result = await callWithBindings(async (root: any) => { - const { keys } = root; - const cleanedRoot: any = {}; - for (const [key, value] of Object.entries(root)) { - if (keys.includes(key)) { - cleanedRoot[key] = value; + symbol: Symbol('foo'), + bigInt: 10n, + getObject: () => ({ thing: 123 }), + getNumber: () => 123, + getString: () => 'string', + getBoolean: () => true, + getArr: () => [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }), + getFunctionFromFunction: async () => () => null, + getError: () => new Error('foo'), + getWeirdError: () => { + const e = new Error('foo'); + e.message = { garbage: true } as any; + return e; + }, + object: { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getPromise: async () => ({ + number: 123, + string: 'string', + boolean: true, + fn: () => 'string', + arr: [123, 'string', true, ['foo']] + }) + }, + receiveArguments: (fn: any) => fn({ key: 'value' }), + symbolKeyed: { + [Symbol('foo')]: 123 + } + }; + const keys: string[] = []; + Object.entries(toExpose).forEach(([key, value]) => { + keys.push(key); + contextBridge.exposeInIsolatedWorld(1004, key, value); + }); + contextBridge.exposeInIsolatedWorld(1004, 'keys', keys); + }); + const result = await callWithBindings(async (root: any) => { + const { keys } = root; + const cleanedRoot: any = {}; + for (const [key, value] of Object.entries(root)) { + if (keys.includes(key)) { + cleanedRoot[key] = value; + } } - } - - let arg: any; - cleanedRoot.receiveArguments((o: any) => { arg = o; }); - const protoChecks = [ - ...Object.keys(cleanedRoot).map(key => [key, String]), - ...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map(key => [key, Symbol]), - [cleanedRoot, Object], - [cleanedRoot.number, Number], - [cleanedRoot.string, String], - [cleanedRoot.boolean, Boolean], - [cleanedRoot.arr, Array], - [cleanedRoot.arr[0], Number], - [cleanedRoot.arr[1], String], - [cleanedRoot.arr[2], Boolean], - [cleanedRoot.arr[3], Array], - [cleanedRoot.arr[3][0], String], - [cleanedRoot.symbol, Symbol], - [cleanedRoot.bigInt, BigInt], - [cleanedRoot.getNumber, Function], - [cleanedRoot.getNumber(), Number], - [cleanedRoot.getObject(), Object], - [cleanedRoot.getString(), String], - [cleanedRoot.getBoolean(), Boolean], - [cleanedRoot.getArr(), Array], - [cleanedRoot.getArr()[0], Number], - [cleanedRoot.getArr()[1], String], - [cleanedRoot.getArr()[2], Boolean], - [cleanedRoot.getArr()[3], Array], - [cleanedRoot.getArr()[3][0], String], - [cleanedRoot.getFunctionFromFunction, Function], - [cleanedRoot.getFunctionFromFunction(), Promise], - [await cleanedRoot.getFunctionFromFunction(), Function], - [cleanedRoot.getError(), Error], - [cleanedRoot.getError().message, String], - [cleanedRoot.getWeirdError(), Error], - [cleanedRoot.getWeirdError().message, String], - [cleanedRoot.getPromise(), Promise], - [await cleanedRoot.getPromise(), Object], - [(await cleanedRoot.getPromise()).number, Number], - [(await cleanedRoot.getPromise()).string, String], - [(await cleanedRoot.getPromise()).boolean, Boolean], - [(await cleanedRoot.getPromise()).fn, Function], - [(await cleanedRoot.getPromise()).fn(), String], - [(await cleanedRoot.getPromise()).arr, Array], - [(await cleanedRoot.getPromise()).arr[0], Number], - [(await cleanedRoot.getPromise()).arr[1], String], - [(await cleanedRoot.getPromise()).arr[2], Boolean], - [(await cleanedRoot.getPromise()).arr[3], Array], - [(await cleanedRoot.getPromise()).arr[3][0], String], - [cleanedRoot.object, Object], - [cleanedRoot.object.number, Number], - [cleanedRoot.object.string, String], - [cleanedRoot.object.boolean, Boolean], - [cleanedRoot.object.arr, Array], - [cleanedRoot.object.arr[0], Number], - [cleanedRoot.object.arr[1], String], - [cleanedRoot.object.arr[2], Boolean], - [cleanedRoot.object.arr[3], Array], - [cleanedRoot.object.arr[3][0], String], - [await cleanedRoot.object.getPromise(), Object], - [(await cleanedRoot.object.getPromise()).number, Number], - [(await cleanedRoot.object.getPromise()).string, String], - [(await cleanedRoot.object.getPromise()).boolean, Boolean], - [(await cleanedRoot.object.getPromise()).fn, Function], - [(await cleanedRoot.object.getPromise()).fn(), String], - [(await cleanedRoot.object.getPromise()).arr, Array], - [(await cleanedRoot.object.getPromise()).arr[0], Number], - [(await cleanedRoot.object.getPromise()).arr[1], String], - [(await cleanedRoot.object.getPromise()).arr[2], Boolean], - [(await cleanedRoot.object.getPromise()).arr[3], Array], - [(await cleanedRoot.object.getPromise()).arr[3][0], String], - [arg, Object], - [arg.key, String] - ]; - return { - protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype) - }; - }); - // Every protomatch should be true - expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true)); - }); - describe('internalContextBridge', () => { - describe('overrideGlobalValueFromIsolatedWorld', () => { - it('should override top level properties', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['open'], () => ({ you: 'are a wizard' })); + let arg: any; + cleanedRoot.receiveArguments((o: any) => { + arg = o; }); - const result = await callWithBindings(async (root: any) => { - return root.open(); - }); - expect(result).to.deep.equal({ you: 'are a wizard' }); + const protoChecks = [ + ...Object.keys(cleanedRoot).map((key) => [key, String]), + ...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map( + (key) => [key, Symbol] + ), + [cleanedRoot, Object], + [cleanedRoot.number, Number], + [cleanedRoot.string, String], + [cleanedRoot.boolean, Boolean], + [cleanedRoot.arr, Array], + [cleanedRoot.arr[0], Number], + [cleanedRoot.arr[1], String], + [cleanedRoot.arr[2], Boolean], + [cleanedRoot.arr[3], Array], + [cleanedRoot.arr[3][0], String], + [cleanedRoot.symbol, Symbol], + [cleanedRoot.bigInt, BigInt], + [cleanedRoot.getNumber, Function], + [cleanedRoot.getNumber(), Number], + [cleanedRoot.getObject(), Object], + [cleanedRoot.getString(), String], + [cleanedRoot.getBoolean(), Boolean], + [cleanedRoot.getArr(), Array], + [cleanedRoot.getArr()[0], Number], + [cleanedRoot.getArr()[1], String], + [cleanedRoot.getArr()[2], Boolean], + [cleanedRoot.getArr()[3], Array], + [cleanedRoot.getArr()[3][0], String], + [cleanedRoot.getFunctionFromFunction, Function], + [cleanedRoot.getFunctionFromFunction(), Promise], + [await cleanedRoot.getFunctionFromFunction(), Function], + [cleanedRoot.getError(), Error], + [cleanedRoot.getError().message, String], + [cleanedRoot.getWeirdError(), Error], + [cleanedRoot.getWeirdError().message, String], + [cleanedRoot.getPromise(), Promise], + [await cleanedRoot.getPromise(), Object], + [(await cleanedRoot.getPromise()).number, Number], + [(await cleanedRoot.getPromise()).string, String], + [(await cleanedRoot.getPromise()).boolean, Boolean], + [(await cleanedRoot.getPromise()).fn, Function], + [(await cleanedRoot.getPromise()).fn(), String], + [(await cleanedRoot.getPromise()).arr, Array], + [(await cleanedRoot.getPromise()).arr[0], Number], + [(await cleanedRoot.getPromise()).arr[1], String], + [(await cleanedRoot.getPromise()).arr[2], Boolean], + [(await cleanedRoot.getPromise()).arr[3], Array], + [(await cleanedRoot.getPromise()).arr[3][0], String], + [cleanedRoot.object, Object], + [cleanedRoot.object.number, Number], + [cleanedRoot.object.string, String], + [cleanedRoot.object.boolean, Boolean], + [cleanedRoot.object.arr, Array], + [cleanedRoot.object.arr[0], Number], + [cleanedRoot.object.arr[1], String], + [cleanedRoot.object.arr[2], Boolean], + [cleanedRoot.object.arr[3], Array], + [cleanedRoot.object.arr[3][0], String], + [await cleanedRoot.object.getPromise(), Object], + [(await cleanedRoot.object.getPromise()).number, Number], + [(await cleanedRoot.object.getPromise()).string, String], + [(await cleanedRoot.object.getPromise()).boolean, Boolean], + [(await cleanedRoot.object.getPromise()).fn, Function], + [(await cleanedRoot.object.getPromise()).fn(), String], + [(await cleanedRoot.object.getPromise()).arr, Array], + [(await cleanedRoot.object.getPromise()).arr[0], Number], + [(await cleanedRoot.object.getPromise()).arr[1], String], + [(await cleanedRoot.object.getPromise()).arr[2], Boolean], + [(await cleanedRoot.object.getPromise()).arr[3], Array], + [(await cleanedRoot.object.getPromise()).arr[3][0], String], + [arg, Object], + [arg.key, String] + ]; + return { + protoMatches: protoChecks.map( + ([a, Constructor]) => + Object.getPrototypeOf(a) === Constructor.prototype + ) + }; + }); + // Every protomatch should be true + expect(result.protoMatches).to.deep.equal( + result.protoMatches.map(() => true) + ); + }); + }); + }); + describe('internalContextBridge', () => { + const makeBindingWindow = async (bindingCreator: Function) => { + const preloadContent = `const renderer_1 = require('electron'); + ${ + useSandbox + ? '' + : `require('v8').setFlagsFromString('--expose_gc'); + const gc=require('vm').runInNewContext('gc'); + renderer_1.contextBridge.exposeInMainWorld('GCRunner', { + run: () => gc() + });` + } + (${bindingCreator.toString()})();`; + const tmpDir = await fs.mkdtemp( + path.resolve(os.tmpdir(), 'electron-spec-preload-') + ); + dir = tmpDir; + await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent); + w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: true, + sandbox: useSandbox, + preload: path.resolve(tmpDir, 'preload.js'), + additionalArguments: [ + '--unsafely-expose-electron-internals-for-testing' + ] + } + }); + await w.loadURL( + `http://127.0.0.1:${(server.address() as AddressInfo).port}` + ); + }; + const callWithBindings = (fn: Function) => + w.webContents.executeJavaScript(`(${fn.toString()})(window)`); + describe('overrideGlobalValueFromIsolatedWorld', () => { + it('should override top level properties', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld( + ['open'], + () => ({ you: 'are a wizard' }) + ); + }); + const result = await callWithBindings(async (root: any) => { + return root.open(); }); + expect(result).to.deep.equal({ you: 'are a wizard' }); + }); - it('should override deep properties', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['document', 'foo'], () => 'I am foo'); - }); - const result = await callWithBindings(async (root: any) => { - return root.document.foo(); - }); - expect(result).to.equal('I am foo'); + it('should override deep properties', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld( + ['document', 'foo'], + () => 'I am foo' + ); + }); + const result = await callWithBindings(async (root: any) => { + return root.document.foo(); }); + expect(result).to.equal('I am foo'); }); + }); - describe('overrideGlobalPropertyFromIsolatedWorld', () => { - it('should call the getter correctly', async () => { - await makeBindingWindow(() => { - let callCount = 0; - const getter = () => { - callCount++; - return true; - }; - contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], getter); - contextBridge.exposeInMainWorld('foo', { - callCount: () => callCount - }); - }); - const result = await callWithBindings(async (root: any) => { - return [root.isFun, root.foo.callCount()]; + describe('overrideGlobalPropertyFromIsolatedWorld', () => { + it('should call the getter correctly', async () => { + await makeBindingWindow(() => { + let callCount = 0; + const getter = () => { + callCount++; + return true; + }; + contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld( + ['isFun'], + getter + ); + contextBridge.exposeInMainWorld('foo', { + callCount: () => callCount }); - expect(result[0]).to.equal(true); - expect(result[1]).to.equal(1); }); + const result = await callWithBindings(async (root: any) => { + return [root.isFun, root.foo.callCount()]; + }); + expect(result[0]).to.equal(true); + expect(result[1]).to.equal(1); + }); - it('should not make a setter if none is provided', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true); - }); - const result = await callWithBindings(async (root: any) => { - root.isFun = 123; - return root.isFun; - }); - expect(result).to.equal(true); + it('should not make a setter if none is provided', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld( + ['isFun'], + () => true + ); + }); + const result = await callWithBindings(async (root: any) => { + root.isFun = 123; + return root.isFun; }); + expect(result).to.equal(true); + }); - it('should call the setter correctly', async () => { - await makeBindingWindow(() => { - const callArgs: any[] = []; - const setter = (...args: any[]) => { - callArgs.push(args); - return true; - }; - contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true, setter); - contextBridge.exposeInMainWorld('foo', { - callArgs: () => callArgs - }); - }); - const result = await callWithBindings(async (root: any) => { - root.isFun = 123; - return root.foo.callArgs(); + it('should call the setter correctly', async () => { + await makeBindingWindow(() => { + const callArgs: any[] = []; + const setter = (...args: any[]) => { + callArgs.push(args); + return true; + }; + contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld( + ['isFun'], + () => true, + setter + ); + contextBridge.exposeInMainWorld('foo', { + callArgs: () => callArgs }); - expect(result).to.have.lengthOf(1); - expect(result[0]).to.have.lengthOf(1); - expect(result[0][0]).to.equal(123); }); + const result = await callWithBindings(async (root: any) => { + root.isFun = 123; + return root.foo.callArgs(); + }); + expect(result).to.have.lengthOf(1); + expect(result[0]).to.have.lengthOf(1); + expect(result[0][0]).to.equal(123); }); + }); - describe('overrideGlobalValueWithDynamicPropsFromIsolatedWorld', () => { - it('should not affect normal values', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + describe('overrideGlobalValueWithDynamicPropsFromIsolatedWorld', () => { + it('should not affect normal values', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { a: 123, b: () => 2, c: () => ({ d: 3 }) - }); - }); - const result = await callWithBindings(async (root: any) => { - return [root.thing.a, root.thing.b(), root.thing.c()]; - }); - expect(result).to.deep.equal([123, 2, { d: 3 }]); + } + ); }); + const result = await callWithBindings(async (root: any) => { + return [root.thing.a, root.thing.b(), root.thing.c()]; + }); + expect(result).to.deep.equal([123, 2, { d: 3 }]); + }); - it('should work with getters', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + it('should work with getters', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { get foo () { return 'hi there'; } - }); - }); - const result = await callWithBindings(async (root: any) => { - return root.thing.foo; - }); - expect(result).to.equal('hi there'); + } + ); + }); + const result = await callWithBindings(async (root: any) => { + return root.thing.foo; }); + expect(result).to.equal('hi there'); + }); - it('should work with nested getters', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + it('should work with nested getters', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { get foo () { return { get bar () { @@ -1147,37 +2488,43 @@ describe('contextBridge', () => { } }; } - }); - }); - const result = await callWithBindings(async (root: any) => { - return root.thing.foo.bar; - }); - expect(result).to.equal('hi there'); + } + ); }); + const result = await callWithBindings(async (root: any) => { + return root.thing.foo.bar; + }); + expect(result).to.equal('hi there'); + }); - it('should work with setters', async () => { - await makeBindingWindow(() => { - let a: any = null; - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + it('should work with setters', async () => { + await makeBindingWindow(() => { + let a: any = null; + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { get foo () { return a; }, set foo (arg: any) { a = arg + 1; } - }); - }); - const result = await callWithBindings(async (root: any) => { - root.thing.foo = 123; - return root.thing.foo; - }); - expect(result).to.equal(124); + } + ); + }); + const result = await callWithBindings(async (root: any) => { + root.thing.foo = 123; + return root.thing.foo; }); + expect(result).to.equal(124); + }); - it('should work with nested getter / setter combos', async () => { - await makeBindingWindow(() => { - let a: any = null; - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + it('should work with nested getter / setter combos', async () => { + await makeBindingWindow(() => { + let a: any = null; + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { get thingy () { return { get foo () { @@ -1188,30 +2535,33 @@ describe('contextBridge', () => { } }; } - }); - }); - const result = await callWithBindings(async (root: any) => { - root.thing.thingy.foo = 123; - return root.thing.thingy.foo; - }); - expect(result).to.equal(124); + } + ); }); + const result = await callWithBindings(async (root: any) => { + root.thing.thingy.foo = 123; + return root.thing.thingy.foo; + }); + expect(result).to.equal(124); + }); - it('should work with deep properties', async () => { - await makeBindingWindow(() => { - contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], { + it('should work with deep properties', async () => { + await makeBindingWindow(() => { + contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld( + ['thing'], + { a: () => ({ get foo () { return 'still here'; } }) - }); - }); - const result = await callWithBindings(async (root: any) => { - return root.thing.a().foo; - }); - expect(result).to.equal('still here'); + } + ); + }); + const result = await callWithBindings(async (root: any) => { + return root.thing.a().foo; }); + expect(result).to.equal('still here'); }); }); }); @@ -1224,10 +2574,16 @@ describe('contextBridge', () => { describe('ContextBridgeMutability', () => { it('should not make properties unwriteable and read-only if ContextBridgeMutability is on', async () => { const appPath = path.join(fixturesPath, 'context-bridge-mutability'); - const appProcess = cp.spawn(process.execPath, ['--enable-logging', '--enable-features=ContextBridgeMutability', appPath]); + const appProcess = cp.spawn(process.execPath, [ + '--enable-logging', + '--enable-features=ContextBridgeMutability', + appPath + ]); let output = ''; - appProcess.stdout.on('data', data => { output += data; }); + appProcess.stdout.on('data', (data) => { + output += data; + }); await emittedOnce(appProcess, 'exit'); expect(output).to.include('some-modified-text'); @@ -1237,10 +2593,15 @@ describe('ContextBridgeMutability', () => { it('should make properties unwriteable and read-only if ContextBridgeMutability is off', async () => { const appPath = path.join(fixturesPath, 'context-bridge-mutability'); - const appProcess = cp.spawn(process.execPath, ['--enable-logging', appPath]); + const appProcess = cp.spawn(process.execPath, [ + '--enable-logging', + appPath + ]); let output = ''; - appProcess.stdout.on('data', data => { output += data; }); + appProcess.stdout.on('data', (data) => { + output += data; + }); await emittedOnce(appProcess, 'exit'); expect(output).to.include('some-text');