api-context-bridge-spec.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. import { BrowserWindow, ipcMain } from 'electron/main';
  2. import { contextBridge } from 'electron/renderer';
  3. import { expect } from 'chai';
  4. import * as fs from 'fs-extra';
  5. import * as http from 'http';
  6. import * as os from 'os';
  7. import * as path from 'path';
  8. import { closeWindow } from './window-helpers';
  9. import { emittedOnce } from './events-helpers';
  10. import { AddressInfo } from 'net';
  11. const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
  12. describe('contextBridge', () => {
  13. let w: BrowserWindow;
  14. let dir: string;
  15. let server: http.Server;
  16. before(async () => {
  17. server = http.createServer((req, res) => {
  18. res.setHeader('Content-Type', 'text/html');
  19. res.end('');
  20. });
  21. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  22. });
  23. after(async () => {
  24. if (server) await new Promise(resolve => server.close(resolve));
  25. server = null as any;
  26. });
  27. afterEach(async () => {
  28. await closeWindow(w);
  29. if (dir) await fs.remove(dir);
  30. });
  31. it('should not be accessible when contextIsolation is disabled', async () => {
  32. w = new BrowserWindow({
  33. show: false,
  34. webPreferences: {
  35. contextIsolation: false,
  36. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  37. }
  38. });
  39. const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')));
  40. expect(bound).to.equal(false);
  41. });
  42. it('should be accessible when contextIsolation is enabled', async () => {
  43. w = new BrowserWindow({
  44. show: false,
  45. webPreferences: {
  46. contextIsolation: true,
  47. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  48. }
  49. });
  50. const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')));
  51. expect(bound).to.equal(true);
  52. });
  53. const generateTests = (useSandbox: boolean) => {
  54. describe(`with sandbox=${useSandbox}`, () => {
  55. const makeBindingWindow = async (bindingCreator: Function) => {
  56. const preloadContent = `const renderer_1 = require('electron');
  57. ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
  58. const gc=require('vm').runInNewContext('gc');
  59. renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
  60. run: () => gc()
  61. });`}
  62. (${bindingCreator.toString()})();`;
  63. const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
  64. dir = tmpDir;
  65. await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent);
  66. w = new BrowserWindow({
  67. show: false,
  68. webPreferences: {
  69. contextIsolation: true,
  70. nodeIntegration: true,
  71. sandbox: useSandbox,
  72. preload: path.resolve(tmpDir, 'preload.js'),
  73. additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
  74. }
  75. });
  76. await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
  77. };
  78. const callWithBindings = (fn: Function) =>
  79. w.webContents.executeJavaScript(`(${fn.toString()})(window)`);
  80. const getGCInfo = async (): Promise<{
  81. trackedValues: number;
  82. }> => {
  83. const [, info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info'));
  84. return info;
  85. };
  86. const forceGCOnWindow = async () => {
  87. w.webContents.debugger.attach();
  88. await w.webContents.debugger.sendCommand('HeapProfiler.enable');
  89. await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage');
  90. await w.webContents.debugger.sendCommand('HeapProfiler.disable');
  91. w.webContents.debugger.detach();
  92. };
  93. it('should proxy numbers', async () => {
  94. await makeBindingWindow(() => {
  95. contextBridge.exposeInMainWorld('example', {
  96. myNumber: 123
  97. });
  98. });
  99. const result = await callWithBindings((root: any) => {
  100. return root.example.myNumber;
  101. });
  102. expect(result).to.equal(123);
  103. });
  104. it('should make properties unwriteable', async () => {
  105. await makeBindingWindow(() => {
  106. contextBridge.exposeInMainWorld('example', {
  107. myNumber: 123
  108. });
  109. });
  110. const result = await callWithBindings((root: any) => {
  111. root.example.myNumber = 456;
  112. return root.example.myNumber;
  113. });
  114. expect(result).to.equal(123);
  115. });
  116. it('should proxy strings', async () => {
  117. await makeBindingWindow(() => {
  118. contextBridge.exposeInMainWorld('example', {
  119. myString: 'my-words'
  120. });
  121. });
  122. const result = await callWithBindings((root: any) => {
  123. return root.example.myString;
  124. });
  125. expect(result).to.equal('my-words');
  126. });
  127. it('should proxy arrays', async () => {
  128. await makeBindingWindow(() => {
  129. contextBridge.exposeInMainWorld('example', {
  130. myArr: [123, 'my-words']
  131. });
  132. });
  133. const result = await callWithBindings((root: any) => {
  134. return root.example.myArr;
  135. });
  136. expect(result).to.deep.equal([123, 'my-words']);
  137. });
  138. it('should make arrays immutable', async () => {
  139. await makeBindingWindow(() => {
  140. contextBridge.exposeInMainWorld('example', {
  141. myArr: [123, 'my-words']
  142. });
  143. });
  144. const immutable = await callWithBindings((root: any) => {
  145. try {
  146. root.example.myArr.push(456);
  147. return false;
  148. } catch {
  149. return true;
  150. }
  151. });
  152. expect(immutable).to.equal(true);
  153. });
  154. it('should proxy booleans', async () => {
  155. await makeBindingWindow(() => {
  156. contextBridge.exposeInMainWorld('example', {
  157. myBool: true
  158. });
  159. });
  160. const result = await callWithBindings((root: any) => {
  161. return root.example.myBool;
  162. });
  163. expect(result).to.equal(true);
  164. });
  165. it('should proxy promises and resolve with the correct value', async () => {
  166. await makeBindingWindow(() => {
  167. contextBridge.exposeInMainWorld('example', {
  168. myPromise: Promise.resolve('i-resolved')
  169. });
  170. });
  171. const result = await callWithBindings((root: any) => {
  172. return root.example.myPromise;
  173. });
  174. expect(result).to.equal('i-resolved');
  175. });
  176. it('should proxy promises and reject with the correct value', async () => {
  177. await makeBindingWindow(() => {
  178. contextBridge.exposeInMainWorld('example', {
  179. myPromise: Promise.reject(new Error('i-rejected'))
  180. });
  181. });
  182. const result = await callWithBindings(async (root: any) => {
  183. try {
  184. await root.example.myPromise;
  185. return null;
  186. } catch (err) {
  187. return err;
  188. }
  189. });
  190. expect(result).to.be.an.instanceOf(Error).with.property('message', 'Uncaught Error: i-rejected');
  191. });
  192. it('should proxy promises and resolve with the correct value if it resolves later', async () => {
  193. await makeBindingWindow(() => {
  194. contextBridge.exposeInMainWorld('example', {
  195. myPromise: () => new Promise(resolve => setTimeout(() => resolve('delayed'), 20))
  196. });
  197. });
  198. const result = await callWithBindings((root: any) => {
  199. return root.example.myPromise();
  200. });
  201. expect(result).to.equal('delayed');
  202. });
  203. it('should proxy nested promises correctly', async () => {
  204. await makeBindingWindow(() => {
  205. contextBridge.exposeInMainWorld('example', {
  206. myPromise: () => new Promise(resolve => setTimeout(() => resolve(Promise.resolve(123)), 20))
  207. });
  208. });
  209. const result = await callWithBindings((root: any) => {
  210. return root.example.myPromise();
  211. });
  212. expect(result).to.equal(123);
  213. });
  214. it('should proxy methods', async () => {
  215. await makeBindingWindow(() => {
  216. contextBridge.exposeInMainWorld('example', {
  217. getNumber: () => 123,
  218. getString: () => 'help',
  219. getBoolean: () => false,
  220. getPromise: async () => 'promise'
  221. });
  222. });
  223. const result = await callWithBindings(async (root: any) => {
  224. return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()];
  225. });
  226. expect(result).to.deep.equal([123, 'help', false, 'promise']);
  227. });
  228. it('should proxy methods that are callable multiple times', async () => {
  229. await makeBindingWindow(() => {
  230. contextBridge.exposeInMainWorld('example', {
  231. doThing: () => 123
  232. });
  233. });
  234. const result = await callWithBindings(async (root: any) => {
  235. return [root.example.doThing(), root.example.doThing(), root.example.doThing()];
  236. });
  237. expect(result).to.deep.equal([123, 123, 123]);
  238. });
  239. it('should proxy methods in the reverse direction', async () => {
  240. await makeBindingWindow(() => {
  241. contextBridge.exposeInMainWorld('example', {
  242. callWithNumber: (fn: any) => fn(123)
  243. });
  244. });
  245. const result = await callWithBindings(async (root: any) => {
  246. return root.example.callWithNumber((n: number) => n + 1);
  247. });
  248. expect(result).to.equal(124);
  249. });
  250. it('should proxy promises in the reverse direction', async () => {
  251. await makeBindingWindow(() => {
  252. contextBridge.exposeInMainWorld('example', {
  253. getPromiseValue: (p: Promise<any>) => p
  254. });
  255. });
  256. const result = await callWithBindings((root: any) => {
  257. return root.example.getPromiseValue(Promise.resolve('my-proxied-value'));
  258. });
  259. expect(result).to.equal('my-proxied-value');
  260. });
  261. it('should proxy objects with number keys', async () => {
  262. await makeBindingWindow(() => {
  263. contextBridge.exposeInMainWorld('example', {
  264. 1: 123,
  265. 2: 456,
  266. 3: 789
  267. });
  268. });
  269. const result = await callWithBindings(async (root: any) => {
  270. return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)];
  271. });
  272. expect(result).to.deep.equal([123, 456, 789, false]);
  273. });
  274. it('it should proxy null and undefined correctly', async () => {
  275. await makeBindingWindow(() => {
  276. contextBridge.exposeInMainWorld('example', {
  277. values: [null, undefined]
  278. });
  279. });
  280. const result = await callWithBindings((root: any) => {
  281. // Convert to strings as although the context bridge keeps the right value
  282. // IPC does not
  283. return root.example.values.map((val: any) => `${val}`);
  284. });
  285. expect(result).to.deep.equal(['null', 'undefined']);
  286. });
  287. it('should proxy symbols such that symbol equality works', async () => {
  288. await makeBindingWindow(() => {
  289. const mySymbol = Symbol('unique');
  290. contextBridge.exposeInMainWorld('example', {
  291. getSymbol: () => mySymbol,
  292. isSymbol: (s: Symbol) => s === mySymbol
  293. });
  294. });
  295. const result = await callWithBindings((root: any) => {
  296. return root.example.isSymbol(root.example.getSymbol());
  297. });
  298. expect(result).to.equal(true, 'symbols should be equal across contexts');
  299. });
  300. it('should proxy symbols such that symbol key lookup works', async () => {
  301. await makeBindingWindow(() => {
  302. const mySymbol = Symbol('unique');
  303. contextBridge.exposeInMainWorld('example', {
  304. getSymbol: () => mySymbol,
  305. getObject: () => ({ [mySymbol]: 123 })
  306. });
  307. });
  308. const result = await callWithBindings((root: any) => {
  309. return root.example.getObject()[root.example.getSymbol()];
  310. });
  311. expect(result).to.equal(123, 'symbols key lookup should work across contexts');
  312. });
  313. it('should proxy typed arrays and regexps through the serializer', async () => {
  314. await makeBindingWindow(() => {
  315. contextBridge.exposeInMainWorld('example', {
  316. arr: new Uint8Array(100),
  317. regexp: /a/g
  318. });
  319. });
  320. const result = await callWithBindings((root: any) => {
  321. return [
  322. Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype,
  323. Object.getPrototypeOf(root.example.regexp) === RegExp.prototype
  324. ];
  325. });
  326. expect(result).to.deep.equal([true, true]);
  327. });
  328. it('it should handle recursive objects', async () => {
  329. await makeBindingWindow(() => {
  330. const o: any = { value: 135 };
  331. o.o = o;
  332. contextBridge.exposeInMainWorld('example', {
  333. o
  334. });
  335. });
  336. const result = await callWithBindings((root: any) => {
  337. return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value];
  338. });
  339. expect(result).to.deep.equal([135, 135, 135]);
  340. });
  341. // Can only run tests which use the GCRunner in non-sandboxed environments
  342. if (!useSandbox) {
  343. it('should release the global hold on methods sent across contexts', async () => {
  344. await makeBindingWindow(() => {
  345. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: process._linkedBinding('electron_common_v8_util').getWeaklyTrackedValues().length }));
  346. const { weaklyTrackValue } = process._linkedBinding('electron_common_v8_util');
  347. contextBridge.exposeInMainWorld('example', {
  348. getFunction: () => () => 123,
  349. track: weaklyTrackValue
  350. });
  351. });
  352. await callWithBindings(async (root: any) => {
  353. root.GCRunner.run();
  354. });
  355. expect((await getGCInfo()).trackedValues).to.equal(0);
  356. await callWithBindings(async (root: any) => {
  357. const fn = root.example.getFunction();
  358. root.example.track(fn);
  359. root.x = [fn];
  360. });
  361. expect((await getGCInfo()).trackedValues).to.equal(1);
  362. await callWithBindings(async (root: any) => {
  363. root.x = [];
  364. root.GCRunner.run();
  365. });
  366. expect((await getGCInfo()).trackedValues).to.equal(0);
  367. });
  368. }
  369. if (useSandbox) {
  370. it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => {
  371. await makeBindingWindow(() => {
  372. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: process._linkedBinding('electron_common_v8_util').getWeaklyTrackedValues().length }));
  373. const { weaklyTrackValue } = process._linkedBinding('electron_common_v8_util');
  374. contextBridge.exposeInMainWorld('example', {
  375. getFunction: () => () => 123,
  376. track: weaklyTrackValue
  377. });
  378. require('electron').ipcRenderer.send('window-ready-for-tasking');
  379. });
  380. const loadPromise = emittedOnce(ipcMain, 'window-ready-for-tasking');
  381. expect((await getGCInfo()).trackedValues).to.equal(0);
  382. await callWithBindings((root: any) => {
  383. root.example.track(root.example.getFunction());
  384. });
  385. expect((await getGCInfo()).trackedValues).to.equal(1);
  386. await callWithBindings((root: any) => {
  387. root.location.reload();
  388. });
  389. await loadPromise;
  390. await forceGCOnWindow();
  391. // If this is ever "2" it means we leaked the exposed function and
  392. // therefore the entire context after a reload
  393. expect((await getGCInfo()).trackedValues).to.equal(0);
  394. });
  395. }
  396. it('it should not let you overwrite existing exposed things', async () => {
  397. await makeBindingWindow(() => {
  398. let threw = false;
  399. contextBridge.exposeInMainWorld('example', {
  400. attempt: 1,
  401. getThrew: () => threw
  402. });
  403. try {
  404. contextBridge.exposeInMainWorld('example', {
  405. attempt: 2,
  406. getThrew: () => threw
  407. });
  408. } catch {
  409. threw = true;
  410. }
  411. });
  412. const result = await callWithBindings((root: any) => {
  413. return [root.example.attempt, root.example.getThrew()];
  414. });
  415. expect(result).to.deep.equal([1, true]);
  416. });
  417. it('should work with complex nested methods and promises', async () => {
  418. await makeBindingWindow(() => {
  419. contextBridge.exposeInMainWorld('example', {
  420. first: (second: Function) => second((fourth: Function) => {
  421. return fourth();
  422. })
  423. });
  424. });
  425. const result = await callWithBindings((root: any) => {
  426. return root.example.first((third: Function) => {
  427. return third(() => Promise.resolve('final value'));
  428. });
  429. });
  430. expect(result).to.equal('final value');
  431. });
  432. it('should throw an error when recursion depth is exceeded', async () => {
  433. await makeBindingWindow(() => {
  434. contextBridge.exposeInMainWorld('example', {
  435. doThing: (a: any) => console.log(a)
  436. });
  437. });
  438. let threw = await callWithBindings((root: any) => {
  439. try {
  440. let a: any = [];
  441. for (let i = 0; i < 999; i++) {
  442. a = [a];
  443. }
  444. root.example.doThing(a);
  445. return false;
  446. } catch {
  447. return true;
  448. }
  449. });
  450. expect(threw).to.equal(false);
  451. threw = await callWithBindings((root: any) => {
  452. try {
  453. let a: any = [];
  454. for (let i = 0; i < 1000; i++) {
  455. a = [a];
  456. }
  457. root.example.doThing(a);
  458. return false;
  459. } catch {
  460. return true;
  461. }
  462. });
  463. expect(threw).to.equal(true);
  464. });
  465. it('should copy thrown errors into the other context', async () => {
  466. await makeBindingWindow(() => {
  467. contextBridge.exposeInMainWorld('example', {
  468. throwNormal: () => {
  469. throw new Error('whoops');
  470. },
  471. throwWeird: () => {
  472. throw 'this is no error...'; // eslint-disable-line no-throw-literal
  473. },
  474. throwNotClonable: () => {
  475. return Object(Symbol('foo'));
  476. },
  477. argumentConvert: () => {}
  478. });
  479. });
  480. const result = await callWithBindings((root: any) => {
  481. const getError = (fn: Function) => {
  482. try {
  483. fn();
  484. } catch (e) {
  485. return e;
  486. }
  487. return null;
  488. };
  489. const normalIsError = Object.getPrototypeOf(getError(root.example.throwNormal)) === Error.prototype;
  490. const weirdIsError = Object.getPrototypeOf(getError(root.example.throwWeird)) === Error.prototype;
  491. const notClonableIsError = Object.getPrototypeOf(getError(root.example.throwNotClonable)) === Error.prototype;
  492. const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype;
  493. return [normalIsError, weirdIsError, notClonableIsError, argumentConvertIsError];
  494. });
  495. expect(result).to.deep.equal([true, true, true, true], 'should all be errors in the current context');
  496. });
  497. it('should not leak prototypes', async () => {
  498. await makeBindingWindow(() => {
  499. contextBridge.exposeInMainWorld('example', {
  500. number: 123,
  501. string: 'string',
  502. boolean: true,
  503. arr: [123, 'string', true, ['foo']],
  504. symbol: Symbol('foo'),
  505. bigInt: 10n,
  506. getObject: () => ({ thing: 123 }),
  507. getNumber: () => 123,
  508. getString: () => 'string',
  509. getBoolean: () => true,
  510. getArr: () => [123, 'string', true, ['foo']],
  511. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
  512. getFunctionFromFunction: async () => () => null,
  513. object: {
  514. number: 123,
  515. string: 'string',
  516. boolean: true,
  517. arr: [123, 'string', true, ['foo']],
  518. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
  519. },
  520. receiveArguments: (fn: any) => fn({ key: 'value' }),
  521. symbolKeyed: {
  522. [Symbol('foo')]: 123
  523. }
  524. });
  525. });
  526. const result = await callWithBindings(async (root: any) => {
  527. const { example } = root;
  528. let arg: any;
  529. example.receiveArguments((o: any) => { arg = o; });
  530. const protoChecks = [
  531. ...Object.keys(example).map(key => [key, String]),
  532. ...Object.getOwnPropertySymbols(example.symbolKeyed).map(key => [key, Symbol]),
  533. [example, Object],
  534. [example.number, Number],
  535. [example.string, String],
  536. [example.boolean, Boolean],
  537. [example.arr, Array],
  538. [example.arr[0], Number],
  539. [example.arr[1], String],
  540. [example.arr[2], Boolean],
  541. [example.arr[3], Array],
  542. [example.arr[3][0], String],
  543. [example.symbol, Symbol],
  544. [example.bigInt, BigInt],
  545. [example.getNumber, Function],
  546. [example.getNumber(), Number],
  547. [example.getObject(), Object],
  548. [example.getString(), String],
  549. [example.getBoolean(), Boolean],
  550. [example.getArr(), Array],
  551. [example.getArr()[0], Number],
  552. [example.getArr()[1], String],
  553. [example.getArr()[2], Boolean],
  554. [example.getArr()[3], Array],
  555. [example.getArr()[3][0], String],
  556. [example.getFunctionFromFunction, Function],
  557. [example.getFunctionFromFunction(), Promise],
  558. [await example.getFunctionFromFunction(), Function],
  559. [example.getPromise(), Promise],
  560. [await example.getPromise(), Object],
  561. [(await example.getPromise()).number, Number],
  562. [(await example.getPromise()).string, String],
  563. [(await example.getPromise()).boolean, Boolean],
  564. [(await example.getPromise()).fn, Function],
  565. [(await example.getPromise()).fn(), String],
  566. [(await example.getPromise()).arr, Array],
  567. [(await example.getPromise()).arr[0], Number],
  568. [(await example.getPromise()).arr[1], String],
  569. [(await example.getPromise()).arr[2], Boolean],
  570. [(await example.getPromise()).arr[3], Array],
  571. [(await example.getPromise()).arr[3][0], String],
  572. [example.object, Object],
  573. [example.object.number, Number],
  574. [example.object.string, String],
  575. [example.object.boolean, Boolean],
  576. [example.object.arr, Array],
  577. [example.object.arr[0], Number],
  578. [example.object.arr[1], String],
  579. [example.object.arr[2], Boolean],
  580. [example.object.arr[3], Array],
  581. [example.object.arr[3][0], String],
  582. [await example.object.getPromise(), Object],
  583. [(await example.object.getPromise()).number, Number],
  584. [(await example.object.getPromise()).string, String],
  585. [(await example.object.getPromise()).boolean, Boolean],
  586. [(await example.object.getPromise()).fn, Function],
  587. [(await example.object.getPromise()).fn(), String],
  588. [(await example.object.getPromise()).arr, Array],
  589. [(await example.object.getPromise()).arr[0], Number],
  590. [(await example.object.getPromise()).arr[1], String],
  591. [(await example.object.getPromise()).arr[2], Boolean],
  592. [(await example.object.getPromise()).arr[3], Array],
  593. [(await example.object.getPromise()).arr[3][0], String],
  594. [arg, Object],
  595. [arg.key, String]
  596. ];
  597. return {
  598. protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
  599. };
  600. });
  601. // Every protomatch should be true
  602. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
  603. });
  604. describe('internalContextBridge', () => {
  605. describe('overrideGlobalValueFromIsolatedWorld', () => {
  606. it('should override top level properties', async () => {
  607. await makeBindingWindow(() => {
  608. contextBridge.internalContextBridge.overrideGlobalValueFromIsolatedWorld(['open'], () => ({ you: 'are a wizard' }));
  609. });
  610. const result = await callWithBindings(async (root: any) => {
  611. return root.open();
  612. });
  613. expect(result).to.deep.equal({ you: 'are a wizard' });
  614. });
  615. it('should override deep properties', async () => {
  616. await makeBindingWindow(() => {
  617. contextBridge.internalContextBridge.overrideGlobalValueFromIsolatedWorld(['document', 'foo'], () => 'I am foo');
  618. });
  619. const result = await callWithBindings(async (root: any) => {
  620. return root.document.foo();
  621. });
  622. expect(result).to.equal('I am foo');
  623. });
  624. });
  625. describe('overrideGlobalPropertyFromIsolatedWorld', () => {
  626. it('should call the getter correctly', async () => {
  627. await makeBindingWindow(() => {
  628. let callCount = 0;
  629. const getter = () => {
  630. callCount++;
  631. return true;
  632. };
  633. contextBridge.internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['isFun'], getter);
  634. contextBridge.exposeInMainWorld('foo', {
  635. callCount: () => callCount
  636. });
  637. });
  638. const result = await callWithBindings(async (root: any) => {
  639. return [root.isFun, root.foo.callCount()];
  640. });
  641. expect(result[0]).to.equal(true);
  642. expect(result[1]).to.equal(1);
  643. });
  644. it('should not make a setter if none is provided', async () => {
  645. await makeBindingWindow(() => {
  646. contextBridge.internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true);
  647. });
  648. const result = await callWithBindings(async (root: any) => {
  649. root.isFun = 123;
  650. return root.isFun;
  651. });
  652. expect(result).to.equal(true);
  653. });
  654. it('should call the setter correctly', async () => {
  655. await makeBindingWindow(() => {
  656. const callArgs: any[] = [];
  657. const setter = (...args: any[]) => {
  658. callArgs.push(args);
  659. return true;
  660. };
  661. contextBridge.internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true, setter);
  662. contextBridge.exposeInMainWorld('foo', {
  663. callArgs: () => callArgs
  664. });
  665. });
  666. const result = await callWithBindings(async (root: any) => {
  667. root.isFun = 123;
  668. return root.foo.callArgs();
  669. });
  670. expect(result).to.have.lengthOf(1);
  671. expect(result[0]).to.have.lengthOf(1);
  672. expect(result[0][0]).to.equal(123);
  673. });
  674. });
  675. describe('overrideGlobalValueWithDynamicPropsFromIsolatedWorld', () => {
  676. it('should not affect normal values', async () => {
  677. await makeBindingWindow(() => {
  678. contextBridge.internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  679. a: 123,
  680. b: () => 2,
  681. c: () => ({ d: 3 })
  682. });
  683. });
  684. const result = await callWithBindings(async (root: any) => {
  685. return [root.thing.a, root.thing.b(), root.thing.c()];
  686. });
  687. expect(result).to.deep.equal([123, 2, { d: 3 }]);
  688. });
  689. it('should work with getters', async () => {
  690. await makeBindingWindow(() => {
  691. contextBridge.internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  692. get foo () {
  693. return 'hi there';
  694. }
  695. });
  696. });
  697. const result = await callWithBindings(async (root: any) => {
  698. return root.thing.foo;
  699. });
  700. expect(result).to.equal('hi there');
  701. });
  702. it('should work with nested getters', async () => {
  703. await makeBindingWindow(() => {
  704. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  705. get foo () {
  706. return {
  707. get bar () {
  708. return 'hi there';
  709. }
  710. };
  711. }
  712. });
  713. });
  714. const result = await callWithBindings(async (root: any) => {
  715. return root.thing.foo.bar;
  716. });
  717. expect(result).to.equal('hi there');
  718. });
  719. it('should work with setters', async () => {
  720. await makeBindingWindow(() => {
  721. let a: any = null;
  722. contextBridge.internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  723. get foo () {
  724. return a;
  725. },
  726. set foo (arg: any) {
  727. a = arg + 1;
  728. }
  729. });
  730. });
  731. const result = await callWithBindings(async (root: any) => {
  732. root.thing.foo = 123;
  733. return root.thing.foo;
  734. });
  735. expect(result).to.equal(124);
  736. });
  737. it('should work with nested getter / setter combos', async () => {
  738. await makeBindingWindow(() => {
  739. let a: any = null;
  740. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  741. get thingy () {
  742. return {
  743. get foo () {
  744. return a;
  745. },
  746. set foo (arg: any) {
  747. a = arg + 1;
  748. }
  749. };
  750. }
  751. });
  752. });
  753. const result = await callWithBindings(async (root: any) => {
  754. root.thing.thingy.foo = 123;
  755. return root.thing.thingy.foo;
  756. });
  757. expect(result).to.equal(124);
  758. });
  759. it('should work with deep properties', async () => {
  760. await makeBindingWindow(() => {
  761. contextBridge.internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  762. a: () => ({
  763. get foo () {
  764. return 'still here';
  765. }
  766. })
  767. });
  768. });
  769. const result = await callWithBindings(async (root: any) => {
  770. return root.thing.a().foo;
  771. });
  772. expect(result).to.equal('still here');
  773. });
  774. });
  775. });
  776. });
  777. };
  778. generateTests(true);
  779. generateTests(false);
  780. });