123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326 |
- import { BrowserWindow, ipcMain } from 'electron/main';
- import { contextBridge } from 'electron/renderer';
- import { expect } from 'chai';
- import * as cp from 'node:child_process';
- import { once } from 'node:events';
- import * as fs from 'node:fs';
- import * as http from 'node:http';
- import * as os from 'node:os';
- import * as path from 'node:path';
- import { listen } from './lib/spec-helpers';
- import { closeWindow } from './lib/window-helpers';
- const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
- describe('contextBridge', () => {
- let w: BrowserWindow;
- let dir: string;
- let server: http.Server;
- let serverUrl: string;
- before(async () => {
- server = http.createServer((req, res) => {
- res.setHeader('Content-Type', 'text/html');
- res.end('');
- });
- serverUrl = (await listen(server)).url;
- });
- after(async () => {
- if (server) await new Promise(resolve => server.close(resolve));
- server = null as any;
- });
- afterEach(async () => {
- await closeWindow(w);
- if (dir) await fs.promises.rm(dir, { force: true, recursive: true });
- });
- it('should not be accessible when contextIsolation is disabled', async () => {
- w = new BrowserWindow({
- show: false,
- webPreferences: {
- contextIsolation: false,
- preload: path.resolve(fixturesPath, 'can-bind-preload.js')
- }
- });
- w.loadFile(path.resolve(fixturesPath, 'empty.html'));
- const [, bound] = await once(ipcMain, 'context-bridge-bound');
- expect(bound).to.equal(false);
- });
- it('should be accessible when contextIsolation is enabled', async () => {
- w = new BrowserWindow({
- show: false,
- webPreferences: {
- contextIsolation: true,
- preload: path.resolve(fixturesPath, 'can-bind-preload.js')
- }
- });
- w.loadFile(path.resolve(fixturesPath, 'empty.html'));
- const [, bound] = await once(ipcMain, 'context-bridge-bound');
- expect(bound).to.equal(true);
- });
- const generateTests = (useSandbox: boolean) => {
- describe(`with sandbox=${useSandbox}`, () => {
- const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
- const preloadContentForMainWorld = `const renderer_1 = require('electron');
- ${useSandbox
- ? ''
- : `require('node:v8').setFlagsFromString('--expose_gc');
- const gc=require('node:vm').runInNewContext('gc');
- renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
- run: () => gc()
- });`}
- (${bindingCreator.toString()})();`;
- const preloadContentForIsolatedWorld = `const renderer_1 = require('electron');
- ${useSandbox
- ? ''
- : `require('node:v8').setFlagsFromString('--expose_gc');
- const gc=require('node:vm').runInNewContext('gc');
- renderer_1.webFrame.setIsolatedWorldInfo(${worldId}, {
- name: "Isolated World"
- });
- renderer_1.contextBridge.exposeInIsolatedWorld(${worldId}, 'GCRunner', {
- run: () => gc()
- });`}
- (${bindingCreator.toString()})();`;
- const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
- dir = tmpDir;
- await fs.promises.writeFile(path.resolve(tmpDir, 'preload.js'), worldId === 0 ? preloadContentForMainWorld : preloadContentForIsolatedWorld);
- 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(serverUrl);
- };
- const callWithBindings = (fn: Function, worldId: number = 0) =>
- worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ;
- const getGCInfo = async (): Promise<{
- trackedValues: number;
- }> => {
- w.webContents.send('get-gc-info');
- const [, info] = await once(ipcMain, '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 proxy numbers when exposed in isolated world', async () => {
- await makeBindingWindow(() => {
- contextBridge.exposeInIsolatedWorld(1004, 'example', 123);
- }, 1004);
- const result = await callWithBindings((root: any) => {
- return root.example;
- }, 1004);
- 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
- });
- });
- 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) => {
- 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'
- });
- });
- const result = await callWithBindings((root: any) => {
- return root.example.myString;
- });
- expect(result).to.equal('my-words');
- });
- it('should proxy nested strings when exposed in isolated world', async () => {
- await makeBindingWindow(() => {
- contextBridge.exposeInIsolatedWorld(1004, 'example', {
- myString: 'my-words'
- });
- }, 1004);
- const result = await callWithBindings((root: any) => {
- return root.example.myString;
- }, 1004);
- 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) => {
- 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;
- }
- });
- expect(immutable).to.equal(true);
- });
- 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);
- });
- 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);
- });
- 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);
- });
- 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');
- });
- 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');
- });
- 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');
- });
- 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');
- });
- 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');
- });
- 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);
- });
- 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']);
- });
- 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');
- });
- 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);
- });
- 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');
- });
- 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]);
- });
- 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);
- });
- it('should proxy promises in the reverse direction', async () => {
- await makeBindingWindow(() => {
- contextBridge.exposeInMainWorld('example', {
- getPromiseValue: (p: Promise<any>) => 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.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]);
- });
- 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');
- });
- 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');
- });
- 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']);
- });
- 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');
- });
- 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');
- });
- 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');
- });
- 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);
- });
- 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);
- });
- it('should proxy typed arrays and regexps through the serializer', async () => {
- await makeBindingWindow(() => {
- contextBridge.exposeInMainWorld('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.exposeInMainWorld('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.exposeInMainWorld('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.exposeInMainWorld('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.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<object>[] = [];
- 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<object>[] = [];
- 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 = once(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'));
- },
- throwNotClonableNestedArray: () => {
- return [Object(Symbol('foo'))];
- },
- throwNotClonableNestedObject: () => {
- return {
- bad: Object(Symbol('foo'))
- };
- },
- throwDynamic: () => {
- return {
- get bad () {
- throw new Error('damm');
- }
- };
- },
- argumentConvert: () => {},
- rejectNotClonable: async () => {
- throw Object(Symbol('foo'));
- },
- resolveNotClonable: async () => Object(Symbol('foo'))
- });
- });
- const result = await callWithBindings(async (root: any) => {
- const getError = (fn: Function) => {
- try {
- fn();
- } catch (e) {
- return e;
- }
- return null;
- };
- const getAsyncError = async (fn: Function) => {
- try {
- await 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 notClonableNestedArrayIsError = Object.getPrototypeOf(getError(root.example.throwNotClonableNestedArray)) === Error.prototype;
- const notClonableNestedObjectIsError = Object.getPrototypeOf(getError(root.example.throwNotClonableNestedObject)) === Error.prototype;
- const dynamicIsError = Object.getPrototypeOf(getError(root.example.throwDynamic)) === Error.prototype;
- const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype;
- const rejectNotClonableIsError = Object.getPrototypeOf(await getAsyncError(root.example.rejectNotClonable)) === Error.prototype;
- const resolveNotClonableIsError = Object.getPrototypeOf(await getAsyncError(root.example.resolveNotClonable)) === Error.prototype;
- return [normalIsError, weirdIsError, notClonableIsError, notClonableNestedArrayIsError, notClonableNestedObjectIsError, dynamicIsError, argumentConvertIsError, rejectNotClonableIsError, resolveNotClonableIsError];
- });
- expect(result).to.deep.equal([true, true, true, true, true, 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[] = [];
- for (const [key, value] of Object.entries(toExpose)) {
- 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('internalContextBridge', () => {
- 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');
- });
- });
- 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()];
- });
- 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 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();
- });
- 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'], {
- 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 }]);
- });
- 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');
- });
- it('should work with nested getters', async () => {
- await makeBindingWindow(() => {
- contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
- get foo () {
- return {
- get bar () {
- return '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'], {
- 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);
- });
- it('should work with nested getter / setter combos', async () => {
- await makeBindingWindow(() => {
- let a: any = null;
- contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
- get thingy () {
- return {
- get foo () {
- return a;
- },
- set foo (arg: any) {
- a = arg + 1;
- }
- };
- }
- });
- });
- 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'], {
- a: () => ({
- get foo () {
- return 'still here';
- }
- })
- });
- });
- const result = await callWithBindings(async (root: any) => {
- return root.thing.a().foo;
- });
- expect(result).to.equal('still here');
- });
- });
- });
- });
- };
- generateTests(true);
- generateTests(false);
- });
- 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]);
- let output = '';
- appProcess.stdout.on('data', data => { output += data; });
- await once(appProcess, 'exit');
- expect(output).to.include('some-modified-text');
- expect(output).to.include('obj-modified-prop');
- expect(output).to.include('1,2,5,3,4');
- });
- 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]);
- let output = '';
- appProcess.stdout.on('data', data => { output += data; });
- await once(appProcess, 'exit');
- expect(output).to.include('some-text');
- expect(output).to.include('obj-prop');
- expect(output).to.include('1,2,3,4');
- });
- });
|