api-context-bridge-spec.ts 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462
  1. import { BrowserWindow, ipcMain } from 'electron/main';
  2. import { contextBridge } from 'electron/renderer';
  3. import { expect } from 'chai';
  4. import * as cp from 'node:child_process';
  5. import { once } from 'node:events';
  6. import * as fs from 'node:fs';
  7. import * as http from 'node:http';
  8. import * as os from 'node:os';
  9. import * as path from 'node:path';
  10. import { listen } from './lib/spec-helpers';
  11. import { closeWindow } from './lib/window-helpers';
  12. const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
  13. describe('contextBridge', () => {
  14. let w: BrowserWindow;
  15. let dir: string;
  16. let server: http.Server;
  17. let serverUrl: string;
  18. before(async () => {
  19. server = http.createServer((req, res) => {
  20. res.setHeader('Content-Type', 'text/html');
  21. res.end('');
  22. });
  23. serverUrl = (await listen(server)).url;
  24. });
  25. after(async () => {
  26. if (server) await new Promise(resolve => server.close(resolve));
  27. server = null as any;
  28. });
  29. afterEach(async () => {
  30. await closeWindow(w);
  31. if (dir) await fs.promises.rm(dir, { force: true, recursive: true });
  32. });
  33. it('should not be accessible when contextIsolation is disabled', async () => {
  34. w = new BrowserWindow({
  35. show: false,
  36. webPreferences: {
  37. contextIsolation: false,
  38. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  39. }
  40. });
  41. w.loadFile(path.resolve(fixturesPath, 'empty.html'));
  42. const [, bound] = await once(ipcMain, 'context-bridge-bound');
  43. expect(bound).to.equal(false);
  44. });
  45. it('should be accessible when contextIsolation is enabled', async () => {
  46. w = new BrowserWindow({
  47. show: false,
  48. webPreferences: {
  49. contextIsolation: true,
  50. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  51. }
  52. });
  53. w.loadFile(path.resolve(fixturesPath, 'empty.html'));
  54. const [, bound] = await once(ipcMain, 'context-bridge-bound');
  55. expect(bound).to.equal(true);
  56. });
  57. const generateTests = (useSandbox: boolean) => {
  58. describe(`with sandbox=${useSandbox}`, () => {
  59. const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
  60. const preloadContentForMainWorld = `const renderer_1 = require('electron');
  61. ${useSandbox
  62. ? ''
  63. : `require('node:v8').setFlagsFromString('--expose_gc');
  64. const gc=require('node:vm').runInNewContext('gc');
  65. renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
  66. run: () => gc()
  67. });`}
  68. (${bindingCreator.toString()})();`;
  69. const preloadContentForIsolatedWorld = `const renderer_1 = require('electron');
  70. ${useSandbox
  71. ? ''
  72. : `require('node:v8').setFlagsFromString('--expose_gc');
  73. const gc=require('node:vm').runInNewContext('gc');
  74. renderer_1.webFrame.setIsolatedWorldInfo(${worldId}, {
  75. name: "Isolated World"
  76. });
  77. renderer_1.contextBridge.exposeInIsolatedWorld(${worldId}, 'GCRunner', {
  78. run: () => gc()
  79. });`}
  80. (${bindingCreator.toString()})();`;
  81. const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
  82. dir = tmpDir;
  83. await fs.promises.writeFile(path.resolve(tmpDir, 'preload.js'), worldId === 0 ? preloadContentForMainWorld : preloadContentForIsolatedWorld);
  84. w = new BrowserWindow({
  85. show: false,
  86. webPreferences: {
  87. contextIsolation: true,
  88. nodeIntegration: true,
  89. sandbox: useSandbox,
  90. preload: path.resolve(tmpDir, 'preload.js'),
  91. additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
  92. }
  93. });
  94. await w.loadURL(serverUrl);
  95. };
  96. const callWithBindings = (fn: Function, worldId: number = 0) =>
  97. worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]);
  98. const getGCInfo = async (): Promise<{
  99. trackedValues: number;
  100. }> => {
  101. w.webContents.send('get-gc-info');
  102. const [, info] = await once(ipcMain, 'gc-info');
  103. return info;
  104. };
  105. const forceGCOnWindow = async () => {
  106. w.webContents.debugger.attach();
  107. await w.webContents.debugger.sendCommand('HeapProfiler.enable');
  108. await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage');
  109. await w.webContents.debugger.sendCommand('HeapProfiler.disable');
  110. w.webContents.debugger.detach();
  111. };
  112. it('should proxy numbers', async () => {
  113. await makeBindingWindow(() => {
  114. contextBridge.exposeInMainWorld('example', 123);
  115. });
  116. const result = await callWithBindings((root: any) => {
  117. return root.example;
  118. });
  119. expect(result).to.equal(123);
  120. });
  121. it('should proxy numbers when exposed in isolated world', async () => {
  122. await makeBindingWindow(() => {
  123. contextBridge.exposeInIsolatedWorld(1004, 'example', 123);
  124. }, 1004);
  125. const result = await callWithBindings((root: any) => {
  126. return root.example;
  127. }, 1004);
  128. expect(result).to.equal(123);
  129. });
  130. it('should make global properties read-only', async () => {
  131. await makeBindingWindow(() => {
  132. contextBridge.exposeInMainWorld('example', 123);
  133. });
  134. const result = await callWithBindings((root: any) => {
  135. root.example = 456;
  136. return root.example;
  137. });
  138. expect(result).to.equal(123);
  139. });
  140. it('should proxy nested numbers', async () => {
  141. await makeBindingWindow(() => {
  142. contextBridge.exposeInMainWorld('example', {
  143. myNumber: 123
  144. });
  145. });
  146. const result = await callWithBindings((root: any) => {
  147. return root.example.myNumber;
  148. });
  149. expect(result).to.equal(123);
  150. });
  151. it('should make properties unwriteable', async () => {
  152. await makeBindingWindow(() => {
  153. contextBridge.exposeInMainWorld('example', {
  154. myNumber: 123
  155. });
  156. });
  157. const result = await callWithBindings((root: any) => {
  158. root.example.myNumber = 456;
  159. return root.example.myNumber;
  160. });
  161. expect(result).to.equal(123);
  162. });
  163. it('should proxy strings', async () => {
  164. await makeBindingWindow(() => {
  165. contextBridge.exposeInMainWorld('example', 'my-words');
  166. });
  167. const result = await callWithBindings((root: any) => {
  168. return root.example;
  169. });
  170. expect(result).to.equal('my-words');
  171. });
  172. it('should proxy nested strings', async () => {
  173. await makeBindingWindow(() => {
  174. contextBridge.exposeInMainWorld('example', {
  175. myString: 'my-words'
  176. });
  177. });
  178. const result = await callWithBindings((root: any) => {
  179. return root.example.myString;
  180. });
  181. expect(result).to.equal('my-words');
  182. });
  183. it('should proxy nested strings when exposed in isolated world', async () => {
  184. await makeBindingWindow(() => {
  185. contextBridge.exposeInIsolatedWorld(1004, 'example', {
  186. myString: 'my-words'
  187. });
  188. }, 1004);
  189. const result = await callWithBindings((root: any) => {
  190. return root.example.myString;
  191. }, 1004);
  192. expect(result).to.equal('my-words');
  193. });
  194. it('should proxy arrays', async () => {
  195. await makeBindingWindow(() => {
  196. contextBridge.exposeInMainWorld('example', [123, 'my-words']);
  197. });
  198. const result = await callWithBindings((root: any) => {
  199. return [root.example, Array.isArray(root.example)];
  200. });
  201. expect(result).to.deep.equal([[123, 'my-words'], true]);
  202. });
  203. it('should proxy nested arrays', async () => {
  204. await makeBindingWindow(() => {
  205. contextBridge.exposeInMainWorld('example', {
  206. myArr: [123, 'my-words']
  207. });
  208. });
  209. const result = await callWithBindings((root: any) => {
  210. return root.example.myArr;
  211. });
  212. expect(result).to.deep.equal([123, 'my-words']);
  213. });
  214. it('should make arrays immutable', async () => {
  215. await makeBindingWindow(() => {
  216. contextBridge.exposeInMainWorld('example', [123, 'my-words']);
  217. });
  218. const immutable = await callWithBindings((root: any) => {
  219. try {
  220. root.example.push(456);
  221. return false;
  222. } catch {
  223. return true;
  224. }
  225. });
  226. expect(immutable).to.equal(true);
  227. });
  228. it('should make nested arrays immutable', async () => {
  229. await makeBindingWindow(() => {
  230. contextBridge.exposeInMainWorld('example', {
  231. myArr: [123, 'my-words']
  232. });
  233. });
  234. const immutable = await callWithBindings((root: any) => {
  235. try {
  236. root.example.myArr.push(456);
  237. return false;
  238. } catch {
  239. return true;
  240. }
  241. });
  242. expect(immutable).to.equal(true);
  243. });
  244. it('should proxy booleans', async () => {
  245. await makeBindingWindow(() => {
  246. contextBridge.exposeInMainWorld('example', true);
  247. });
  248. const result = await callWithBindings((root: any) => {
  249. return root.example;
  250. });
  251. expect(result).to.equal(true);
  252. });
  253. it('should proxy nested booleans', async () => {
  254. await makeBindingWindow(() => {
  255. contextBridge.exposeInMainWorld('example', {
  256. myBool: true
  257. });
  258. });
  259. const result = await callWithBindings((root: any) => {
  260. return root.example.myBool;
  261. });
  262. expect(result).to.equal(true);
  263. });
  264. it('should proxy promises and resolve with the correct value', async () => {
  265. await makeBindingWindow(() => {
  266. contextBridge.exposeInMainWorld('example',
  267. Promise.resolve('i-resolved')
  268. );
  269. });
  270. const result = await callWithBindings((root: any) => {
  271. return root.example;
  272. });
  273. expect(result).to.equal('i-resolved');
  274. });
  275. it('should proxy nested promises and resolve with the correct value', async () => {
  276. await makeBindingWindow(() => {
  277. contextBridge.exposeInMainWorld('example', {
  278. myPromise: Promise.resolve('i-resolved')
  279. });
  280. });
  281. const result = await callWithBindings((root: any) => {
  282. return root.example.myPromise;
  283. });
  284. expect(result).to.equal('i-resolved');
  285. });
  286. it('should proxy promises and reject with the correct value', async () => {
  287. await makeBindingWindow(() => {
  288. contextBridge.exposeInMainWorld('example', Promise.reject(new Error('i-rejected')));
  289. });
  290. const result = await callWithBindings(async (root: any) => {
  291. try {
  292. await root.example;
  293. return null;
  294. } catch (err) {
  295. return err;
  296. }
  297. });
  298. expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected');
  299. });
  300. it('should proxy nested promises and reject with the correct value', async () => {
  301. await makeBindingWindow(() => {
  302. contextBridge.exposeInMainWorld('example', {
  303. myPromise: Promise.reject(new Error('i-rejected'))
  304. });
  305. });
  306. const result = await callWithBindings(async (root: any) => {
  307. try {
  308. await root.example.myPromise;
  309. return null;
  310. } catch (err) {
  311. return err;
  312. }
  313. });
  314. expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected');
  315. });
  316. it('should proxy promises and resolve with the correct value if it resolves later', async () => {
  317. await makeBindingWindow(() => {
  318. contextBridge.exposeInMainWorld('example', {
  319. myPromise: () => new Promise(resolve => setTimeout(() => resolve('delayed'), 20))
  320. });
  321. });
  322. const result = await callWithBindings((root: any) => {
  323. return root.example.myPromise();
  324. });
  325. expect(result).to.equal('delayed');
  326. });
  327. it('should proxy nested promises correctly', async () => {
  328. await makeBindingWindow(() => {
  329. contextBridge.exposeInMainWorld('example', {
  330. myPromise: () => new Promise(resolve => setTimeout(() => resolve(Promise.resolve(123)), 20))
  331. });
  332. });
  333. const result = await callWithBindings((root: any) => {
  334. return root.example.myPromise();
  335. });
  336. expect(result).to.equal(123);
  337. });
  338. it('should proxy methods', async () => {
  339. await makeBindingWindow(() => {
  340. contextBridge.exposeInMainWorld('example', {
  341. getNumber: () => 123,
  342. getString: () => 'help',
  343. getBoolean: () => false,
  344. getPromise: async () => 'promise'
  345. });
  346. });
  347. const result = await callWithBindings(async (root: any) => {
  348. return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()];
  349. });
  350. expect(result).to.deep.equal([123, 'help', false, 'promise']);
  351. });
  352. it('should proxy functions', async () => {
  353. await makeBindingWindow(() => {
  354. contextBridge.exposeInMainWorld('example', () => 'return-value');
  355. });
  356. const result = await callWithBindings(async (root: any) => {
  357. return root.example();
  358. });
  359. expect(result).equal('return-value');
  360. });
  361. it('should not double-proxy functions when they are returned to their origin side of the bridge', async () => {
  362. await makeBindingWindow(() => {
  363. contextBridge.exposeInMainWorld('example', (fn: any) => fn);
  364. });
  365. const result = await callWithBindings(async (root: any) => {
  366. const fn = () => null;
  367. return root.example(fn) === fn;
  368. });
  369. expect(result).equal(true);
  370. });
  371. it('should proxy function arguments only once', async () => {
  372. await makeBindingWindow(() => {
  373. contextBridge.exposeInMainWorld('example', (a: any, b: any) => a === b);
  374. });
  375. const result = await callWithBindings(async (root: any) => {
  376. const obj = { foo: 1 };
  377. return root.example(obj, obj);
  378. });
  379. expect(result).to.be.true();
  380. });
  381. it('should properly handle errors thrown in proxied functions', async () => {
  382. await makeBindingWindow(() => {
  383. contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); });
  384. });
  385. const result = await callWithBindings(async (root: any) => {
  386. try {
  387. root.example();
  388. } catch (e) {
  389. return (e as Error).message;
  390. }
  391. });
  392. expect(result).equal('oh no');
  393. });
  394. it('should proxy methods that are callable multiple times', async () => {
  395. await makeBindingWindow(() => {
  396. contextBridge.exposeInMainWorld('example', {
  397. doThing: () => 123
  398. });
  399. });
  400. const result = await callWithBindings(async (root: any) => {
  401. return [root.example.doThing(), root.example.doThing(), root.example.doThing()];
  402. });
  403. expect(result).to.deep.equal([123, 123, 123]);
  404. });
  405. it('should proxy methods in the reverse direction', async () => {
  406. await makeBindingWindow(() => {
  407. contextBridge.exposeInMainWorld('example', {
  408. callWithNumber: (fn: any) => fn(123)
  409. });
  410. });
  411. const result = await callWithBindings(async (root: any) => {
  412. return root.example.callWithNumber((n: number) => n + 1);
  413. });
  414. expect(result).to.equal(124);
  415. });
  416. it('should proxy promises in the reverse direction', async () => {
  417. await makeBindingWindow(() => {
  418. contextBridge.exposeInMainWorld('example', {
  419. getPromiseValue: (p: Promise<any>) => p
  420. });
  421. });
  422. const result = await callWithBindings((root: any) => {
  423. return root.example.getPromiseValue(Promise.resolve('my-proxied-value'));
  424. });
  425. expect(result).to.equal('my-proxied-value');
  426. });
  427. it('should proxy objects with number keys', async () => {
  428. await makeBindingWindow(() => {
  429. contextBridge.exposeInMainWorld('example', {
  430. 1: 123,
  431. 2: 456,
  432. 3: 789
  433. });
  434. });
  435. const result = await callWithBindings(async (root: any) => {
  436. return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)];
  437. });
  438. expect(result).to.deep.equal([123, 456, 789, false]);
  439. });
  440. it('it should proxy null', async () => {
  441. await makeBindingWindow(() => {
  442. contextBridge.exposeInMainWorld('example', null);
  443. });
  444. const result = await callWithBindings((root: any) => {
  445. // Convert to strings as although the context bridge keeps the right value
  446. // IPC does not
  447. return `${root.example}`;
  448. });
  449. expect(result).to.deep.equal('null');
  450. });
  451. it('it should proxy undefined', async () => {
  452. await makeBindingWindow(() => {
  453. contextBridge.exposeInMainWorld('example', undefined);
  454. });
  455. const result = await callWithBindings((root: any) => {
  456. // Convert to strings as although the context bridge keeps the right value
  457. // IPC does not
  458. return `${root.example}`;
  459. });
  460. expect(result).to.deep.equal('undefined');
  461. });
  462. it('it should proxy nested null and undefined correctly', async () => {
  463. await makeBindingWindow(() => {
  464. contextBridge.exposeInMainWorld('example', {
  465. values: [null, undefined]
  466. });
  467. });
  468. const result = await callWithBindings((root: any) => {
  469. // Convert to strings as although the context bridge keeps the right value
  470. // IPC does not
  471. return root.example.values.map((val: any) => `${val}`);
  472. });
  473. expect(result).to.deep.equal(['null', 'undefined']);
  474. });
  475. it('should proxy symbols', async () => {
  476. await makeBindingWindow(() => {
  477. const mySymbol = Symbol('unique');
  478. const isSymbol = (s: Symbol) => s === mySymbol;
  479. contextBridge.exposeInMainWorld('symbol', mySymbol);
  480. contextBridge.exposeInMainWorld('isSymbol', isSymbol);
  481. });
  482. const result = await callWithBindings((root: any) => {
  483. return root.isSymbol(root.symbol);
  484. });
  485. expect(result).to.equal(true, 'symbols should be equal across contexts');
  486. });
  487. it('should proxy symbols such that symbol equality works', async () => {
  488. await makeBindingWindow(() => {
  489. const mySymbol = Symbol('unique');
  490. contextBridge.exposeInMainWorld('example', {
  491. getSymbol: () => mySymbol,
  492. isSymbol: (s: Symbol) => s === mySymbol
  493. });
  494. });
  495. const result = await callWithBindings((root: any) => {
  496. return root.example.isSymbol(root.example.getSymbol());
  497. });
  498. expect(result).to.equal(true, 'symbols should be equal across contexts');
  499. });
  500. it('should proxy symbols such that symbol key lookup works', async () => {
  501. await makeBindingWindow(() => {
  502. const mySymbol = Symbol('unique');
  503. contextBridge.exposeInMainWorld('example', {
  504. getSymbol: () => mySymbol,
  505. getObject: () => ({ [mySymbol]: 123 })
  506. });
  507. });
  508. const result = await callWithBindings((root: any) => {
  509. return root.example.getObject()[root.example.getSymbol()];
  510. });
  511. expect(result).to.equal(123, 'symbols key lookup should work across contexts');
  512. });
  513. it('should proxy typed arrays', async () => {
  514. await makeBindingWindow(() => {
  515. contextBridge.exposeInMainWorld('example', new Uint8Array(100));
  516. });
  517. const result = await callWithBindings((root: any) => {
  518. return Object.getPrototypeOf(root.example) === Uint8Array.prototype;
  519. });
  520. expect(result).equal(true);
  521. });
  522. it('should proxy regexps', async () => {
  523. await makeBindingWindow(() => {
  524. contextBridge.exposeInMainWorld('example', /a/g);
  525. });
  526. const result = await callWithBindings((root: any) => {
  527. return Object.getPrototypeOf(root.example) === RegExp.prototype;
  528. });
  529. expect(result).equal(true);
  530. });
  531. it('should proxy typed arrays and regexps through the serializer', async () => {
  532. await makeBindingWindow(() => {
  533. contextBridge.exposeInMainWorld('example', {
  534. arr: new Uint8Array(100),
  535. regexp: /a/g
  536. });
  537. });
  538. const result = await callWithBindings((root: any) => {
  539. return [
  540. Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype,
  541. Object.getPrototypeOf(root.example.regexp) === RegExp.prototype
  542. ];
  543. });
  544. expect(result).to.deep.equal([true, true]);
  545. });
  546. it('should handle recursive objects', async () => {
  547. await makeBindingWindow(() => {
  548. const o: any = { value: 135 };
  549. o.o = o;
  550. contextBridge.exposeInMainWorld('example', {
  551. o
  552. });
  553. });
  554. const result = await callWithBindings((root: any) => {
  555. return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value];
  556. });
  557. expect(result).to.deep.equal([135, 135, 135]);
  558. });
  559. it('should handle DOM elements', async () => {
  560. await makeBindingWindow(() => {
  561. contextBridge.exposeInMainWorld('example', {
  562. getElem: () => document.body
  563. });
  564. });
  565. const result = await callWithBindings((root: any) => {
  566. return [root.example.getElem().tagName, root.example.getElem().constructor.name, typeof root.example.getElem().querySelector];
  567. });
  568. expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']);
  569. });
  570. it('should handle DOM elements going backwards over the bridge', async () => {
  571. await makeBindingWindow(() => {
  572. contextBridge.exposeInMainWorld('example', {
  573. getElemInfo: (fn: Function) => {
  574. const elem = fn();
  575. return [elem.tagName, elem.constructor.name, typeof elem.querySelector];
  576. }
  577. });
  578. });
  579. const result = await callWithBindings((root: any) => {
  580. return root.example.getElemInfo(() => document.body);
  581. });
  582. expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']);
  583. });
  584. it('should handle Blobs', async () => {
  585. await makeBindingWindow(() => {
  586. contextBridge.exposeInMainWorld('example', {
  587. getBlob: () => new Blob(['ab', 'cd'])
  588. });
  589. });
  590. const result = await callWithBindings(async (root: any) => {
  591. return [await root.example.getBlob().text()];
  592. });
  593. expect(result).to.deep.equal(['abcd']);
  594. });
  595. it('should handle Blobs going backwards over the bridge', async () => {
  596. await makeBindingWindow(() => {
  597. contextBridge.exposeInMainWorld('example', {
  598. getBlobText: async (fn: Function) => {
  599. const blob = fn();
  600. return [await blob.text()];
  601. }
  602. });
  603. });
  604. const result = await callWithBindings((root: any) => {
  605. return root.example.getBlobText(() => new Blob(['12', '45']));
  606. });
  607. expect(result).to.deep.equal(['1245']);
  608. });
  609. // Can only run tests which use the GCRunner in non-sandboxed environments
  610. if (!useSandbox) {
  611. it('should release the global hold on methods sent across contexts', async () => {
  612. await makeBindingWindow(() => {
  613. const trackedValues: WeakRef<object>[] = [];
  614. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length }));
  615. contextBridge.exposeInMainWorld('example', {
  616. getFunction: () => () => 123,
  617. track: (value: object) => { trackedValues.push(new WeakRef(value)); }
  618. });
  619. });
  620. await callWithBindings(async (root: any) => {
  621. root.GCRunner.run();
  622. });
  623. expect((await getGCInfo()).trackedValues).to.equal(0);
  624. await callWithBindings(async (root: any) => {
  625. const fn = root.example.getFunction();
  626. root.example.track(fn);
  627. root.x = [fn];
  628. });
  629. expect((await getGCInfo()).trackedValues).to.equal(1);
  630. await callWithBindings(async (root: any) => {
  631. root.x = [];
  632. root.GCRunner.run();
  633. });
  634. expect((await getGCInfo()).trackedValues).to.equal(0);
  635. });
  636. }
  637. if (useSandbox) {
  638. it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => {
  639. await makeBindingWindow(() => {
  640. const trackedValues: WeakRef<object>[] = [];
  641. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length }));
  642. contextBridge.exposeInMainWorld('example', {
  643. getFunction: () => () => 123,
  644. track: (value: object) => { trackedValues.push(new WeakRef(value)); }
  645. });
  646. require('electron').ipcRenderer.send('window-ready-for-tasking');
  647. });
  648. const loadPromise = once(ipcMain, 'window-ready-for-tasking');
  649. expect((await getGCInfo()).trackedValues).to.equal(0);
  650. await callWithBindings((root: any) => {
  651. root.example.track(root.example.getFunction());
  652. });
  653. expect((await getGCInfo()).trackedValues).to.equal(1);
  654. await callWithBindings((root: any) => {
  655. root.location.reload();
  656. });
  657. await loadPromise;
  658. await forceGCOnWindow();
  659. // If this is ever "2" it means we leaked the exposed function and
  660. // therefore the entire context after a reload
  661. expect((await getGCInfo()).trackedValues).to.equal(0);
  662. });
  663. }
  664. it('it should not let you overwrite existing exposed things', async () => {
  665. await makeBindingWindow(() => {
  666. let threw = false;
  667. contextBridge.exposeInMainWorld('example', {
  668. attempt: 1,
  669. getThrew: () => threw
  670. });
  671. try {
  672. contextBridge.exposeInMainWorld('example', {
  673. attempt: 2,
  674. getThrew: () => threw
  675. });
  676. } catch {
  677. threw = true;
  678. }
  679. });
  680. const result = await callWithBindings((root: any) => {
  681. return [root.example.attempt, root.example.getThrew()];
  682. });
  683. expect(result).to.deep.equal([1, true]);
  684. });
  685. it('should work with complex nested methods and promises', async () => {
  686. await makeBindingWindow(() => {
  687. contextBridge.exposeInMainWorld('example', {
  688. first: (second: Function) => second((fourth: Function) => {
  689. return fourth();
  690. })
  691. });
  692. });
  693. const result = await callWithBindings((root: any) => {
  694. return root.example.first((third: Function) => {
  695. return third(() => Promise.resolve('final value'));
  696. });
  697. });
  698. expect(result).to.equal('final value');
  699. });
  700. it('should work with complex nested methods and promises attached directly to the global', async () => {
  701. await makeBindingWindow(() => {
  702. contextBridge.exposeInMainWorld('example',
  703. (second: Function) => second((fourth: Function) => {
  704. return fourth();
  705. })
  706. );
  707. });
  708. const result = await callWithBindings((root: any) => {
  709. return root.example((third: Function) => {
  710. return third(() => Promise.resolve('final value'));
  711. });
  712. });
  713. expect(result).to.equal('final value');
  714. });
  715. it('should throw an error when recursion depth is exceeded', async () => {
  716. await makeBindingWindow(() => {
  717. contextBridge.exposeInMainWorld('example', {
  718. doThing: (a: any) => console.log(a)
  719. });
  720. });
  721. let threw = await callWithBindings((root: any) => {
  722. try {
  723. let a: any = [];
  724. for (let i = 0; i < 999; i++) {
  725. a = [a];
  726. }
  727. root.example.doThing(a);
  728. return false;
  729. } catch {
  730. return true;
  731. }
  732. });
  733. expect(threw).to.equal(false);
  734. threw = await callWithBindings((root: any) => {
  735. try {
  736. let a: any = [];
  737. for (let i = 0; i < 1000; i++) {
  738. a = [a];
  739. }
  740. root.example.doThing(a);
  741. return false;
  742. } catch {
  743. return true;
  744. }
  745. });
  746. expect(threw).to.equal(true);
  747. });
  748. it('should copy thrown errors into the other context', async () => {
  749. await makeBindingWindow(() => {
  750. contextBridge.exposeInMainWorld('example', {
  751. throwNormal: () => {
  752. throw new Error('whoops');
  753. },
  754. throwWeird: () => {
  755. throw 'this is no error...'; // eslint-disable-line no-throw-literal
  756. },
  757. throwNotClonable: () => {
  758. return Object(Symbol('foo'));
  759. },
  760. throwNotClonableNestedArray: () => {
  761. return [Object(Symbol('foo'))];
  762. },
  763. throwNotClonableNestedObject: () => {
  764. return {
  765. bad: Object(Symbol('foo'))
  766. };
  767. },
  768. throwDynamic: () => {
  769. return {
  770. get bad () {
  771. throw new Error('damm');
  772. }
  773. };
  774. },
  775. argumentConvert: () => {},
  776. rejectNotClonable: async () => {
  777. throw Object(Symbol('foo'));
  778. },
  779. resolveNotClonable: async () => Object(Symbol('foo'))
  780. });
  781. });
  782. const result = await callWithBindings(async (root: any) => {
  783. const getError = (fn: Function) => {
  784. try {
  785. fn();
  786. } catch (e) {
  787. return e;
  788. }
  789. return null;
  790. };
  791. const getAsyncError = async (fn: Function) => {
  792. try {
  793. await fn();
  794. } catch (e) {
  795. return e;
  796. }
  797. return null;
  798. };
  799. const normalIsError = Object.getPrototypeOf(getError(root.example.throwNormal)) === Error.prototype;
  800. const weirdIsError = Object.getPrototypeOf(getError(root.example.throwWeird)) === Error.prototype;
  801. const notClonableIsError = Object.getPrototypeOf(getError(root.example.throwNotClonable)) === Error.prototype;
  802. const notClonableNestedArrayIsError = Object.getPrototypeOf(getError(root.example.throwNotClonableNestedArray)) === Error.prototype;
  803. const notClonableNestedObjectIsError = Object.getPrototypeOf(getError(root.example.throwNotClonableNestedObject)) === Error.prototype;
  804. const dynamicIsError = Object.getPrototypeOf(getError(root.example.throwDynamic)) === Error.prototype;
  805. const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype;
  806. const rejectNotClonableIsError = Object.getPrototypeOf(await getAsyncError(root.example.rejectNotClonable)) === Error.prototype;
  807. const resolveNotClonableIsError = Object.getPrototypeOf(await getAsyncError(root.example.resolveNotClonable)) === Error.prototype;
  808. return [normalIsError, weirdIsError, notClonableIsError, notClonableNestedArrayIsError, notClonableNestedObjectIsError, dynamicIsError, argumentConvertIsError, rejectNotClonableIsError, resolveNotClonableIsError];
  809. });
  810. expect(result).to.deep.equal([true, true, true, true, true, true, true, true, true], 'should all be errors in the current context');
  811. });
  812. it('should not leak prototypes', async () => {
  813. await makeBindingWindow(() => {
  814. contextBridge.exposeInMainWorld('example', {
  815. number: 123,
  816. string: 'string',
  817. boolean: true,
  818. arr: [123, 'string', true, ['foo']],
  819. symbol: Symbol('foo'),
  820. bigInt: 10n,
  821. getObject: () => ({ thing: 123 }),
  822. getNumber: () => 123,
  823. getString: () => 'string',
  824. getBoolean: () => true,
  825. getArr: () => [123, 'string', true, ['foo']],
  826. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
  827. getFunctionFromFunction: async () => () => null,
  828. object: {
  829. number: 123,
  830. string: 'string',
  831. boolean: true,
  832. arr: [123, 'string', true, ['foo']],
  833. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
  834. },
  835. receiveArguments: (fn: any) => fn({ key: 'value' }),
  836. symbolKeyed: {
  837. [Symbol('foo')]: 123
  838. },
  839. getBody: () => document.body,
  840. getBlob: () => new Blob(['ab', 'cd'])
  841. });
  842. });
  843. const result = await callWithBindings(async (root: any) => {
  844. const { example } = root;
  845. let arg: any;
  846. example.receiveArguments((o: any) => { arg = o; });
  847. const protoChecks = [
  848. ...Object.keys(example).map(key => [key, String]),
  849. ...Object.getOwnPropertySymbols(example.symbolKeyed).map(key => [key, Symbol]),
  850. [example, Object],
  851. [example.number, Number],
  852. [example.string, String],
  853. [example.boolean, Boolean],
  854. [example.arr, Array],
  855. [example.arr[0], Number],
  856. [example.arr[1], String],
  857. [example.arr[2], Boolean],
  858. [example.arr[3], Array],
  859. [example.arr[3][0], String],
  860. [example.symbol, Symbol],
  861. [example.bigInt, BigInt],
  862. [example.getNumber, Function],
  863. [example.getNumber(), Number],
  864. [example.getObject(), Object],
  865. [example.getString(), String],
  866. [example.getBoolean(), Boolean],
  867. [example.getArr(), Array],
  868. [example.getArr()[0], Number],
  869. [example.getArr()[1], String],
  870. [example.getArr()[2], Boolean],
  871. [example.getArr()[3], Array],
  872. [example.getArr()[3][0], String],
  873. [example.getFunctionFromFunction, Function],
  874. [example.getFunctionFromFunction(), Promise],
  875. [await example.getFunctionFromFunction(), Function],
  876. [example.getPromise(), Promise],
  877. [await example.getPromise(), Object],
  878. [(await example.getPromise()).number, Number],
  879. [(await example.getPromise()).string, String],
  880. [(await example.getPromise()).boolean, Boolean],
  881. [(await example.getPromise()).fn, Function],
  882. [(await example.getPromise()).fn(), String],
  883. [(await example.getPromise()).arr, Array],
  884. [(await example.getPromise()).arr[0], Number],
  885. [(await example.getPromise()).arr[1], String],
  886. [(await example.getPromise()).arr[2], Boolean],
  887. [(await example.getPromise()).arr[3], Array],
  888. [(await example.getPromise()).arr[3][0], String],
  889. [example.object, Object],
  890. [example.object.number, Number],
  891. [example.object.string, String],
  892. [example.object.boolean, Boolean],
  893. [example.object.arr, Array],
  894. [example.object.arr[0], Number],
  895. [example.object.arr[1], String],
  896. [example.object.arr[2], Boolean],
  897. [example.object.arr[3], Array],
  898. [example.object.arr[3][0], String],
  899. [await example.object.getPromise(), Object],
  900. [(await example.object.getPromise()).number, Number],
  901. [(await example.object.getPromise()).string, String],
  902. [(await example.object.getPromise()).boolean, Boolean],
  903. [(await example.object.getPromise()).fn, Function],
  904. [(await example.object.getPromise()).fn(), String],
  905. [(await example.object.getPromise()).arr, Array],
  906. [(await example.object.getPromise()).arr[0], Number],
  907. [(await example.object.getPromise()).arr[1], String],
  908. [(await example.object.getPromise()).arr[2], Boolean],
  909. [(await example.object.getPromise()).arr[3], Array],
  910. [(await example.object.getPromise()).arr[3][0], String],
  911. [arg, Object],
  912. [arg.key, String],
  913. [example.getBody(), HTMLBodyElement],
  914. [example.getBlob(), Blob]
  915. ];
  916. return {
  917. protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
  918. };
  919. });
  920. // Every protomatch should be true
  921. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
  922. });
  923. it('should not leak prototypes when attaching directly to the global', async () => {
  924. await makeBindingWindow(() => {
  925. const toExpose = {
  926. number: 123,
  927. string: 'string',
  928. boolean: true,
  929. arr: [123, 'string', true, ['foo']],
  930. symbol: Symbol('foo'),
  931. bigInt: 10n,
  932. getObject: () => ({ thing: 123 }),
  933. getNumber: () => 123,
  934. getString: () => 'string',
  935. getBoolean: () => true,
  936. getArr: () => [123, 'string', true, ['foo']],
  937. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
  938. getFunctionFromFunction: async () => () => null,
  939. getError: () => new Error('foo'),
  940. getWeirdError: () => {
  941. const e = new Error('foo');
  942. e.message = { garbage: true } as any;
  943. return e;
  944. },
  945. object: {
  946. number: 123,
  947. string: 'string',
  948. boolean: true,
  949. arr: [123, 'string', true, ['foo']],
  950. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
  951. },
  952. receiveArguments: (fn: any) => fn({ key: 'value' }),
  953. symbolKeyed: {
  954. [Symbol('foo')]: 123
  955. }
  956. };
  957. const keys: string[] = [];
  958. for (const [key, value] of Object.entries(toExpose)) {
  959. keys.push(key);
  960. contextBridge.exposeInMainWorld(key, value);
  961. }
  962. contextBridge.exposeInMainWorld('keys', keys);
  963. });
  964. const result = await callWithBindings(async (root: any) => {
  965. const { keys } = root;
  966. const cleanedRoot: any = {};
  967. for (const [key, value] of Object.entries(root)) {
  968. if (keys.includes(key)) {
  969. cleanedRoot[key] = value;
  970. }
  971. }
  972. let arg: any;
  973. cleanedRoot.receiveArguments((o: any) => { arg = o; });
  974. const protoChecks = [
  975. ...Object.keys(cleanedRoot).map(key => [key, String]),
  976. ...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map(key => [key, Symbol]),
  977. [cleanedRoot, Object],
  978. [cleanedRoot.number, Number],
  979. [cleanedRoot.string, String],
  980. [cleanedRoot.boolean, Boolean],
  981. [cleanedRoot.arr, Array],
  982. [cleanedRoot.arr[0], Number],
  983. [cleanedRoot.arr[1], String],
  984. [cleanedRoot.arr[2], Boolean],
  985. [cleanedRoot.arr[3], Array],
  986. [cleanedRoot.arr[3][0], String],
  987. [cleanedRoot.symbol, Symbol],
  988. [cleanedRoot.bigInt, BigInt],
  989. [cleanedRoot.getNumber, Function],
  990. [cleanedRoot.getNumber(), Number],
  991. [cleanedRoot.getObject(), Object],
  992. [cleanedRoot.getString(), String],
  993. [cleanedRoot.getBoolean(), Boolean],
  994. [cleanedRoot.getArr(), Array],
  995. [cleanedRoot.getArr()[0], Number],
  996. [cleanedRoot.getArr()[1], String],
  997. [cleanedRoot.getArr()[2], Boolean],
  998. [cleanedRoot.getArr()[3], Array],
  999. [cleanedRoot.getArr()[3][0], String],
  1000. [cleanedRoot.getFunctionFromFunction, Function],
  1001. [cleanedRoot.getFunctionFromFunction(), Promise],
  1002. [await cleanedRoot.getFunctionFromFunction(), Function],
  1003. [cleanedRoot.getError(), Error],
  1004. [cleanedRoot.getError().message, String],
  1005. [cleanedRoot.getWeirdError(), Error],
  1006. [cleanedRoot.getWeirdError().message, String],
  1007. [cleanedRoot.getPromise(), Promise],
  1008. [await cleanedRoot.getPromise(), Object],
  1009. [(await cleanedRoot.getPromise()).number, Number],
  1010. [(await cleanedRoot.getPromise()).string, String],
  1011. [(await cleanedRoot.getPromise()).boolean, Boolean],
  1012. [(await cleanedRoot.getPromise()).fn, Function],
  1013. [(await cleanedRoot.getPromise()).fn(), String],
  1014. [(await cleanedRoot.getPromise()).arr, Array],
  1015. [(await cleanedRoot.getPromise()).arr[0], Number],
  1016. [(await cleanedRoot.getPromise()).arr[1], String],
  1017. [(await cleanedRoot.getPromise()).arr[2], Boolean],
  1018. [(await cleanedRoot.getPromise()).arr[3], Array],
  1019. [(await cleanedRoot.getPromise()).arr[3][0], String],
  1020. [cleanedRoot.object, Object],
  1021. [cleanedRoot.object.number, Number],
  1022. [cleanedRoot.object.string, String],
  1023. [cleanedRoot.object.boolean, Boolean],
  1024. [cleanedRoot.object.arr, Array],
  1025. [cleanedRoot.object.arr[0], Number],
  1026. [cleanedRoot.object.arr[1], String],
  1027. [cleanedRoot.object.arr[2], Boolean],
  1028. [cleanedRoot.object.arr[3], Array],
  1029. [cleanedRoot.object.arr[3][0], String],
  1030. [await cleanedRoot.object.getPromise(), Object],
  1031. [(await cleanedRoot.object.getPromise()).number, Number],
  1032. [(await cleanedRoot.object.getPromise()).string, String],
  1033. [(await cleanedRoot.object.getPromise()).boolean, Boolean],
  1034. [(await cleanedRoot.object.getPromise()).fn, Function],
  1035. [(await cleanedRoot.object.getPromise()).fn(), String],
  1036. [(await cleanedRoot.object.getPromise()).arr, Array],
  1037. [(await cleanedRoot.object.getPromise()).arr[0], Number],
  1038. [(await cleanedRoot.object.getPromise()).arr[1], String],
  1039. [(await cleanedRoot.object.getPromise()).arr[2], Boolean],
  1040. [(await cleanedRoot.object.getPromise()).arr[3], Array],
  1041. [(await cleanedRoot.object.getPromise()).arr[3][0], String],
  1042. [arg, Object],
  1043. [arg.key, String]
  1044. ];
  1045. return {
  1046. protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
  1047. };
  1048. });
  1049. // Every protomatch should be true
  1050. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
  1051. });
  1052. describe('internalContextBridge', () => {
  1053. describe('overrideGlobalValueFromIsolatedWorld', () => {
  1054. it('should override top level properties', async () => {
  1055. await makeBindingWindow(() => {
  1056. contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['open'], () => ({ you: 'are a wizard' }));
  1057. });
  1058. const result = await callWithBindings(async (root: any) => {
  1059. return root.open();
  1060. });
  1061. expect(result).to.deep.equal({ you: 'are a wizard' });
  1062. });
  1063. it('should override deep properties', async () => {
  1064. await makeBindingWindow(() => {
  1065. contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['document', 'foo'], () => 'I am foo');
  1066. });
  1067. const result = await callWithBindings(async (root: any) => {
  1068. return root.document.foo();
  1069. });
  1070. expect(result).to.equal('I am foo');
  1071. });
  1072. });
  1073. describe('overrideGlobalPropertyFromIsolatedWorld', () => {
  1074. it('should call the getter correctly', async () => {
  1075. await makeBindingWindow(() => {
  1076. let callCount = 0;
  1077. const getter = () => {
  1078. callCount++;
  1079. return true;
  1080. };
  1081. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], getter);
  1082. contextBridge.exposeInMainWorld('foo', {
  1083. callCount: () => callCount
  1084. });
  1085. });
  1086. const result = await callWithBindings(async (root: any) => {
  1087. return [root.isFun, root.foo.callCount()];
  1088. });
  1089. expect(result[0]).to.equal(true);
  1090. expect(result[1]).to.equal(1);
  1091. });
  1092. it('should not make a setter if none is provided', async () => {
  1093. await makeBindingWindow(() => {
  1094. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true);
  1095. });
  1096. const result = await callWithBindings(async (root: any) => {
  1097. root.isFun = 123;
  1098. return root.isFun;
  1099. });
  1100. expect(result).to.equal(true);
  1101. });
  1102. it('should call the setter correctly', async () => {
  1103. await makeBindingWindow(() => {
  1104. const callArgs: any[] = [];
  1105. const setter = (...args: any[]) => {
  1106. callArgs.push(args);
  1107. return true;
  1108. };
  1109. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true, setter);
  1110. contextBridge.exposeInMainWorld('foo', {
  1111. callArgs: () => callArgs
  1112. });
  1113. });
  1114. const result = await callWithBindings(async (root: any) => {
  1115. root.isFun = 123;
  1116. return root.foo.callArgs();
  1117. });
  1118. expect(result).to.have.lengthOf(1);
  1119. expect(result[0]).to.have.lengthOf(1);
  1120. expect(result[0][0]).to.equal(123);
  1121. });
  1122. });
  1123. describe('overrideGlobalValueWithDynamicPropsFromIsolatedWorld', () => {
  1124. it('should not affect normal values', async () => {
  1125. await makeBindingWindow(() => {
  1126. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1127. a: 123,
  1128. b: () => 2,
  1129. c: () => ({ d: 3 })
  1130. });
  1131. });
  1132. const result = await callWithBindings(async (root: any) => {
  1133. return [root.thing.a, root.thing.b(), root.thing.c()];
  1134. });
  1135. expect(result).to.deep.equal([123, 2, { d: 3 }]);
  1136. });
  1137. it('should work with getters', async () => {
  1138. await makeBindingWindow(() => {
  1139. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1140. get foo () {
  1141. return 'hi there';
  1142. }
  1143. });
  1144. });
  1145. const result = await callWithBindings(async (root: any) => {
  1146. return root.thing.foo;
  1147. });
  1148. expect(result).to.equal('hi there');
  1149. });
  1150. it('should work with nested getters', async () => {
  1151. await makeBindingWindow(() => {
  1152. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1153. get foo () {
  1154. return {
  1155. get bar () {
  1156. return 'hi there';
  1157. }
  1158. };
  1159. }
  1160. });
  1161. });
  1162. const result = await callWithBindings(async (root: any) => {
  1163. return root.thing.foo.bar;
  1164. });
  1165. expect(result).to.equal('hi there');
  1166. });
  1167. it('should work with setters', async () => {
  1168. await makeBindingWindow(() => {
  1169. let a: any = null;
  1170. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1171. get foo () {
  1172. return a;
  1173. },
  1174. set foo (arg: any) {
  1175. a = arg + 1;
  1176. }
  1177. });
  1178. });
  1179. const result = await callWithBindings(async (root: any) => {
  1180. root.thing.foo = 123;
  1181. return root.thing.foo;
  1182. });
  1183. expect(result).to.equal(124);
  1184. });
  1185. it('should work with nested getter / setter combos', async () => {
  1186. await makeBindingWindow(() => {
  1187. let a: any = null;
  1188. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1189. get thingy () {
  1190. return {
  1191. get foo () {
  1192. return a;
  1193. },
  1194. set foo (arg: any) {
  1195. a = arg + 1;
  1196. }
  1197. };
  1198. }
  1199. });
  1200. });
  1201. const result = await callWithBindings(async (root: any) => {
  1202. root.thing.thingy.foo = 123;
  1203. return root.thing.thingy.foo;
  1204. });
  1205. expect(result).to.equal(124);
  1206. });
  1207. it('should work with deep properties', async () => {
  1208. await makeBindingWindow(() => {
  1209. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1210. a: () => ({
  1211. get foo () {
  1212. return 'still here';
  1213. }
  1214. })
  1215. });
  1216. });
  1217. const result = await callWithBindings(async (root: any) => {
  1218. return root.thing.a().foo;
  1219. });
  1220. expect(result).to.equal('still here');
  1221. });
  1222. });
  1223. });
  1224. describe('executeInMainWorld', () => {
  1225. it('serializes function and proxies args', async () => {
  1226. await makeBindingWindow(async () => {
  1227. const values = [
  1228. undefined,
  1229. null,
  1230. 123,
  1231. 'string',
  1232. true,
  1233. [123, 'string', true, ['foo']],
  1234. () => 'string',
  1235. Symbol('foo')
  1236. ];
  1237. function appendArg (arg: any) {
  1238. // @ts-ignore
  1239. globalThis.args = globalThis.args || [];
  1240. // @ts-ignore
  1241. globalThis.args.push(arg);
  1242. }
  1243. for (const value of values) {
  1244. try {
  1245. await contextBridge.executeInMainWorld({
  1246. func: appendArg,
  1247. args: [value]
  1248. });
  1249. } catch {
  1250. contextBridge.executeInMainWorld({
  1251. func: appendArg,
  1252. args: ['FAIL']
  1253. });
  1254. }
  1255. }
  1256. });
  1257. const result = await callWithBindings(() => {
  1258. // @ts-ignore
  1259. return globalThis.args.map(arg => {
  1260. // Map unserializable IPC types to their type string
  1261. if (['function', 'symbol'].includes(typeof arg)) {
  1262. return typeof arg;
  1263. } else {
  1264. return arg;
  1265. }
  1266. });
  1267. });
  1268. expect(result).to.deep.equal([
  1269. undefined,
  1270. null,
  1271. 123,
  1272. 'string',
  1273. true,
  1274. [123, 'string', true, ['foo']],
  1275. 'function',
  1276. 'symbol'
  1277. ]);
  1278. });
  1279. it('allows function args to be invoked', async () => {
  1280. const donePromise = once(ipcMain, 'done');
  1281. makeBindingWindow(() => {
  1282. const uuid = crypto.randomUUID();
  1283. const done = (receivedUuid: string) => {
  1284. if (receivedUuid === uuid) {
  1285. require('electron').ipcRenderer.send('done');
  1286. }
  1287. };
  1288. contextBridge.executeInMainWorld({
  1289. func: (callback, innerUuid) => {
  1290. callback(innerUuid);
  1291. },
  1292. args: [done, uuid]
  1293. });
  1294. });
  1295. await donePromise;
  1296. });
  1297. it('proxies arguments only once', async () => {
  1298. await makeBindingWindow(() => {
  1299. const obj = {};
  1300. // @ts-ignore
  1301. globalThis.result = contextBridge.executeInMainWorld({
  1302. func: (a, b) => a === b,
  1303. args: [obj, obj]
  1304. });
  1305. });
  1306. const result = await callWithBindings(() => {
  1307. // @ts-ignore
  1308. return globalThis.result;
  1309. }, 999);
  1310. expect(result).to.be.true();
  1311. });
  1312. it('safely clones returned objects', async () => {
  1313. await makeBindingWindow(() => {
  1314. const obj = contextBridge.executeInMainWorld({
  1315. func: () => ({})
  1316. });
  1317. // @ts-ignore
  1318. globalThis.safe = obj.constructor === Object;
  1319. });
  1320. const result = await callWithBindings(() => {
  1321. // @ts-ignore
  1322. return globalThis.safe;
  1323. }, 999);
  1324. expect(result).to.be.true();
  1325. });
  1326. it('uses internal Function.prototype.toString', async () => {
  1327. await makeBindingWindow(() => {
  1328. const funcHack = () => {
  1329. // @ts-ignore
  1330. globalThis.hacked = 'nope';
  1331. };
  1332. funcHack.toString = () => '() => { globalThis.hacked = \'gotem\'; }';
  1333. contextBridge.executeInMainWorld({
  1334. func: funcHack
  1335. });
  1336. });
  1337. const result = await callWithBindings(() => {
  1338. // @ts-ignore
  1339. return globalThis.hacked;
  1340. });
  1341. expect(result).to.equal('nope');
  1342. });
  1343. });
  1344. });
  1345. };
  1346. generateTests(true);
  1347. generateTests(false);
  1348. });
  1349. describe('ContextBridgeMutability', () => {
  1350. it('should not make properties unwriteable and read-only if ContextBridgeMutability is on', async () => {
  1351. const appPath = path.join(fixturesPath, 'context-bridge-mutability');
  1352. const appProcess = cp.spawn(process.execPath, ['--enable-logging', '--enable-features=ContextBridgeMutability', appPath]);
  1353. let output = '';
  1354. appProcess.stdout.on('data', data => { output += data; });
  1355. await once(appProcess, 'exit');
  1356. expect(output).to.include('some-modified-text');
  1357. expect(output).to.include('obj-modified-prop');
  1358. expect(output).to.include('1,2,5,3,4');
  1359. });
  1360. it('should make properties unwriteable and read-only if ContextBridgeMutability is off', async () => {
  1361. const appPath = path.join(fixturesPath, 'context-bridge-mutability');
  1362. const appProcess = cp.spawn(process.execPath, ['--enable-logging', appPath]);
  1363. let output = '';
  1364. appProcess.stdout.on('data', data => { output += data; });
  1365. await once(appProcess, 'exit');
  1366. expect(output).to.include('some-text');
  1367. expect(output).to.include('obj-prop');
  1368. expect(output).to.include('1,2,3,4');
  1369. });
  1370. });