12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250 |
- import { BrowserWindow, ipcMain } from 'electron/main';
- import { contextBridge } from 'electron/renderer';
- import { expect } from 'chai';
- 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 { closeWindow } from './window-helpers';
- import { emittedOnce } from './events-helpers';
- import { AddressInfo } from 'net';
- const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
- describe('contextBridge', () => {
- let w: BrowserWindow;
- let dir: string;
- let server: http.Server;
- before(async () => {
- server = http.createServer((req, res) => {
- res.setHeader('Content-Type', 'text/html');
- res.end('');
- });
- await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
- });
- after(async () => {
- if (server) await new Promise(resolve => server.close(resolve));
- server = null as any;
- });
- afterEach(async () => {
- await closeWindow(w);
- if (dir) await fs.remove(dir);
- });
- 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')
- }
- });
- const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')));
- 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')
- }
- });
- 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 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)`);
- 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
- });
- });
- 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 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.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(() => {
- require('electron').ipcRenderer.on('get-gc-info', (e: Electron.IpcRendererEvent) => e.sender.send('gc-info', { trackedValues: process._linkedBinding('electron_common_v8_util').getWeaklyTrackedValues().length }));
- const { weaklyTrackValue } = process._linkedBinding('electron_common_v8_util');
- contextBridge.exposeInMainWorld('example', {
- getFunction: () => () => 123,
- track: weaklyTrackValue
- });
- });
- 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(() => {
- require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: process._linkedBinding('electron_common_v8_util').getWeaklyTrackedValues().length }));
- const { weaklyTrackValue } = process._linkedBinding('electron_common_v8_util');
- contextBridge.exposeInMainWorld('example', {
- getFunction: () => () => 123,
- track: weaklyTrackValue
- });
- 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('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 emittedOnce(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 emittedOnce(appProcess, 'exit');
- expect(output).to.include('some-text');
- expect(output).to.include('obj-prop');
- expect(output).to.include('1,2,3,4');
- });
- });
|