api-ipc-spec.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. import { EventEmitter, once } from 'node:events';
  2. import { expect } from 'chai';
  3. import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
  4. import { closeAllWindows } from './lib/window-helpers';
  5. import { defer, listen } from './lib/spec-helpers';
  6. import * as path from 'node:path';
  7. import * as http from 'node:http';
  8. const v8Util = process._linkedBinding('electron_common_v8_util');
  9. const fixturesPath = path.resolve(__dirname, 'fixtures');
  10. describe('ipc module', () => {
  11. describe('invoke', () => {
  12. let w: BrowserWindow;
  13. before(async () => {
  14. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  15. await w.loadURL('about:blank');
  16. });
  17. after(async () => {
  18. w.destroy();
  19. });
  20. async function rendererInvoke (...args: any[]) {
  21. const { ipcRenderer } = require('electron');
  22. try {
  23. const result = await ipcRenderer.invoke('test', ...args);
  24. ipcRenderer.send('result', { result });
  25. } catch (e) {
  26. ipcRenderer.send('result', { error: (e as Error).message });
  27. }
  28. }
  29. it('receives a response from a synchronous handler', async () => {
  30. ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => {
  31. expect(arg).to.equal(123);
  32. return 3;
  33. });
  34. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  35. expect(arg).to.deep.equal({ result: 3 });
  36. resolve();
  37. }));
  38. await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`);
  39. await done;
  40. });
  41. it('receives a response from an asynchronous handler', async () => {
  42. ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => {
  43. expect(arg).to.equal(123);
  44. await new Promise(setImmediate);
  45. return 3;
  46. });
  47. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  48. expect(arg).to.deep.equal({ result: 3 });
  49. resolve();
  50. }));
  51. await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`);
  52. await done;
  53. });
  54. it('receives an error from a synchronous handler', async () => {
  55. ipcMain.handleOnce('test', () => {
  56. throw new Error('some error');
  57. });
  58. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  59. expect(arg.error).to.match(/some error/);
  60. resolve();
  61. }));
  62. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  63. await done;
  64. });
  65. it('receives an error from an asynchronous handler', async () => {
  66. ipcMain.handleOnce('test', async () => {
  67. await new Promise(setImmediate);
  68. throw new Error('some error');
  69. });
  70. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  71. expect(arg.error).to.match(/some error/);
  72. resolve();
  73. }));
  74. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  75. await done;
  76. });
  77. it('throws an error if no handler is registered', async () => {
  78. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  79. expect(arg.error).to.match(/No handler registered/);
  80. resolve();
  81. }));
  82. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  83. await done;
  84. });
  85. it('throws an error when invoking a handler that was removed', async () => {
  86. ipcMain.handle('test', () => { });
  87. ipcMain.removeHandler('test');
  88. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  89. expect(arg.error).to.match(/No handler registered/);
  90. resolve();
  91. }));
  92. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  93. await done;
  94. });
  95. it('forbids multiple handlers', async () => {
  96. ipcMain.handle('test', () => { });
  97. try {
  98. expect(() => { ipcMain.handle('test', () => { }); }).to.throw(/second handler/);
  99. } finally {
  100. ipcMain.removeHandler('test');
  101. }
  102. });
  103. it('throws an error in the renderer if the reply callback is dropped', async () => {
  104. ipcMain.handleOnce('test', () => new Promise(() => {
  105. setTimeout(() => v8Util.requestGarbageCollectionForTesting());
  106. /* never resolve */
  107. }));
  108. w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  109. const [, { error }] = await once(ipcMain, 'result');
  110. expect(error).to.match(/reply was never sent/);
  111. });
  112. });
  113. describe('ordering', () => {
  114. let w: BrowserWindow;
  115. before(async () => {
  116. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  117. await w.loadURL('about:blank');
  118. });
  119. after(async () => {
  120. w.destroy();
  121. });
  122. it('between send and sendSync is consistent', async () => {
  123. const received: number[] = [];
  124. ipcMain.on('test-async', (e, i) => { received.push(i); });
  125. ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null; });
  126. const done = new Promise<void>(resolve => ipcMain.once('done', () => { resolve(); }));
  127. function rendererStressTest () {
  128. const { ipcRenderer } = require('electron');
  129. for (let i = 0; i < 1000; i++) {
  130. switch ((Math.random() * 2) | 0) {
  131. case 0:
  132. ipcRenderer.send('test-async', i);
  133. break;
  134. case 1:
  135. ipcRenderer.sendSync('test-sync', i);
  136. break;
  137. }
  138. }
  139. ipcRenderer.send('done');
  140. }
  141. try {
  142. w.webContents.executeJavaScript(`(${rendererStressTest})()`);
  143. await done;
  144. } finally {
  145. ipcMain.removeAllListeners('test-async');
  146. ipcMain.removeAllListeners('test-sync');
  147. }
  148. expect(received).to.have.lengthOf(1000);
  149. expect(received).to.deep.equal([...received].sort((a, b) => a - b));
  150. });
  151. it('between send, sendSync, and invoke is consistent', async () => {
  152. const received: number[] = [];
  153. ipcMain.handle('test-invoke', (e, i) => { received.push(i); });
  154. ipcMain.on('test-async', (e, i) => { received.push(i); });
  155. ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null; });
  156. const done = new Promise<void>(resolve => ipcMain.once('done', () => { resolve(); }));
  157. function rendererStressTest () {
  158. const { ipcRenderer } = require('electron');
  159. for (let i = 0; i < 1000; i++) {
  160. switch ((Math.random() * 3) | 0) {
  161. case 0:
  162. ipcRenderer.send('test-async', i);
  163. break;
  164. case 1:
  165. ipcRenderer.sendSync('test-sync', i);
  166. break;
  167. case 2:
  168. ipcRenderer.invoke('test-invoke', i);
  169. break;
  170. }
  171. }
  172. ipcRenderer.send('done');
  173. }
  174. try {
  175. w.webContents.executeJavaScript(`(${rendererStressTest})()`);
  176. await done;
  177. } finally {
  178. ipcMain.removeHandler('test-invoke');
  179. ipcMain.removeAllListeners('test-async');
  180. ipcMain.removeAllListeners('test-sync');
  181. }
  182. expect(received).to.have.lengthOf(1000);
  183. expect(received).to.deep.equal([...received].sort((a, b) => a - b));
  184. });
  185. });
  186. describe('MessagePort', () => {
  187. afterEach(closeAllWindows);
  188. it('can send a port to the main process', async () => {
  189. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  190. w.loadURL('about:blank');
  191. const p = once(ipcMain, 'port');
  192. await w.webContents.executeJavaScript(`(${function () {
  193. const channel = new MessageChannel();
  194. require('electron').ipcRenderer.postMessage('port', 'hi', [channel.port1]);
  195. }})()`);
  196. const [ev, msg] = await p;
  197. expect(msg).to.equal('hi');
  198. expect(ev.ports).to.have.length(1);
  199. expect(ev.senderFrame.parent).to.be.null();
  200. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  201. const [port] = ev.ports;
  202. expect(port).to.be.an.instanceOf(EventEmitter);
  203. });
  204. it('can sent a message without a transfer', async () => {
  205. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  206. w.loadURL('about:blank');
  207. const p = once(ipcMain, 'port');
  208. await w.webContents.executeJavaScript(`(${function () {
  209. require('electron').ipcRenderer.postMessage('port', 'hi');
  210. }})()`);
  211. const [ev, msg] = await p;
  212. expect(msg).to.equal('hi');
  213. expect(ev.ports).to.deep.equal([]);
  214. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  215. });
  216. it('can communicate between main and renderer', async () => {
  217. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  218. w.loadURL('about:blank');
  219. const p = once(ipcMain, 'port');
  220. await w.webContents.executeJavaScript(`(${function () {
  221. const channel = new MessageChannel();
  222. channel.port2.onmessage = (ev: any) => {
  223. channel.port2.postMessage(ev.data * 2);
  224. };
  225. require('electron').ipcRenderer.postMessage('port', '', [channel.port1]);
  226. }})()`);
  227. const [ev] = await p;
  228. expect(ev.ports).to.have.length(1);
  229. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  230. const [port] = ev.ports;
  231. port.start();
  232. port.postMessage(42);
  233. const [ev2] = await once(port, 'message');
  234. expect(ev2.data).to.equal(84);
  235. });
  236. it('can receive a port from a renderer over a MessagePort connection', async () => {
  237. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  238. w.loadURL('about:blank');
  239. function fn () {
  240. const channel1 = new MessageChannel();
  241. const channel2 = new MessageChannel();
  242. channel1.port2.postMessage('', [channel2.port1]);
  243. channel2.port2.postMessage('matryoshka');
  244. require('electron').ipcRenderer.postMessage('port', '', [channel1.port1]);
  245. }
  246. w.webContents.executeJavaScript(`(${fn})()`);
  247. const [{ ports: [port1] }] = await once(ipcMain, 'port');
  248. port1.start();
  249. const [{ ports: [port2] }] = await once(port1, 'message');
  250. port2.start();
  251. const [{ data }] = await once(port2, 'message');
  252. expect(data).to.equal('matryoshka');
  253. });
  254. it('can forward a port from one renderer to another renderer', async () => {
  255. const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  256. const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  257. w1.loadURL('about:blank');
  258. w2.loadURL('about:blank');
  259. w1.webContents.executeJavaScript(`(${function () {
  260. const channel = new MessageChannel();
  261. channel.port2.onmessage = (ev: any) => {
  262. require('electron').ipcRenderer.send('message received', ev.data);
  263. };
  264. require('electron').ipcRenderer.postMessage('port', '', [channel.port1]);
  265. }})()`);
  266. const [{ ports: [port] }] = await once(ipcMain, 'port');
  267. await w2.webContents.executeJavaScript(`(${function () {
  268. require('electron').ipcRenderer.on('port', ({ ports: [port] }: any) => {
  269. port.postMessage('a message');
  270. });
  271. }})()`);
  272. w2.webContents.postMessage('port', '', [port]);
  273. const [, data] = await once(ipcMain, 'message received');
  274. expect(data).to.equal('a message');
  275. });
  276. describe('close event', () => {
  277. describe('in renderer', () => {
  278. it('is emitted when the main process closes its end of the port', async () => {
  279. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  280. w.loadURL('about:blank');
  281. await w.webContents.executeJavaScript(`(${function () {
  282. const { ipcRenderer } = require('electron');
  283. ipcRenderer.on('port', e => {
  284. const [port] = e.ports;
  285. port.start();
  286. port.onclose = () => {
  287. ipcRenderer.send('closed');
  288. };
  289. });
  290. }})()`);
  291. const { port1, port2 } = new MessageChannelMain();
  292. w.webContents.postMessage('port', null, [port2]);
  293. port1.close();
  294. await once(ipcMain, 'closed');
  295. });
  296. it('is emitted when the other end of a port is garbage-collected', async () => {
  297. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  298. w.loadURL('about:blank');
  299. await w.webContents.executeJavaScript(`(${async function () {
  300. const { port2 } = new MessageChannel();
  301. await new Promise<void>(resolve => {
  302. port2.start();
  303. port2.onclose = resolve;
  304. // @ts-ignore --expose-gc is enabled.
  305. gc({ type: 'major', execution: 'async' });
  306. });
  307. }})()`);
  308. });
  309. it('is emitted when the other end of a port is sent to nowhere', async () => {
  310. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  311. w.loadURL('about:blank');
  312. ipcMain.once('do-a-gc', () => v8Util.requestGarbageCollectionForTesting());
  313. await w.webContents.executeJavaScript(`(${async function () {
  314. const { port1, port2 } = new MessageChannel();
  315. await new Promise<void>(resolve => {
  316. port2.start();
  317. port2.onclose = resolve;
  318. require('electron').ipcRenderer.postMessage('nobody-listening', null, [port1]);
  319. require('electron').ipcRenderer.send('do-a-gc');
  320. });
  321. }})()`);
  322. });
  323. });
  324. describe('when context destroyed', () => {
  325. it('does not crash', async () => {
  326. let count = 0;
  327. const server = http.createServer((req, res) => {
  328. switch (req.url) {
  329. case '/index.html':
  330. res.setHeader('content-type', 'text/html');
  331. res.statusCode = 200;
  332. count = count + 1;
  333. res.end(`
  334. <title>Hello${count}</title>
  335. <script>
  336. var sharedWorker = new SharedWorker('worker.js');
  337. sharedWorker.port.addEventListener('close', function(event) {
  338. console.log('close event', event.data);
  339. });
  340. </script>`);
  341. break;
  342. case '/worker.js':
  343. res.setHeader('content-type', 'application/javascript; charset=UTF-8');
  344. res.statusCode = 200;
  345. res.end(`
  346. self.addEventListener('connect', function(event) {
  347. var port = event.ports[0];
  348. port.addEventListener('message', function(event) {
  349. console.log('Message from main:', event.data);
  350. port.postMessage('Hello from SharedWorker!');
  351. });
  352. });`);
  353. break;
  354. default:
  355. throw new Error(`unsupported endpoint: ${req.url}`);
  356. }
  357. });
  358. const { port } = await listen(server);
  359. defer(() => {
  360. server.close();
  361. });
  362. const w = new BrowserWindow({ show: false });
  363. await w.loadURL(`http://localhost:${port}/index.html`);
  364. expect(w.webContents.getTitle()).to.equal('Hello1');
  365. // Before the fix, it would crash if reloaded, but now it doesn't
  366. await w.loadURL(`http://localhost:${port}/index.html`);
  367. expect(w.webContents.getTitle()).to.equal('Hello2');
  368. // const crashEvent = emittedOnce(w.webContents, 'render-process-gone');
  369. // await crashEvent;
  370. });
  371. });
  372. });
  373. describe('MessageChannelMain', () => {
  374. it('can be created', () => {
  375. const { port1, port2 } = new MessageChannelMain();
  376. expect(port1).not.to.be.null();
  377. expect(port2).not.to.be.null();
  378. });
  379. it('throws an error when an invalid parameter is sent to postMessage', () => {
  380. const { port1 } = new MessageChannelMain();
  381. expect(() => {
  382. const buffer = new ArrayBuffer(10) as any;
  383. port1.postMessage(null, [buffer]);
  384. }).to.throw(/Port at index 0 is not a valid port/);
  385. expect(() => {
  386. port1.postMessage(null, ['1' as any]);
  387. }).to.throw(/Port at index 0 is not a valid port/);
  388. expect(() => {
  389. port1.postMessage(null, [new Date() as any]);
  390. }).to.throw(/Port at index 0 is not a valid port/);
  391. });
  392. it('throws when postMessage transferables contains the source port', () => {
  393. const { port1 } = new MessageChannelMain();
  394. expect(() => {
  395. port1.postMessage(null, [port1]);
  396. }).to.throw(/Port at index 0 contains the source port./);
  397. });
  398. it('can send messages within the process', async () => {
  399. const { port1, port2 } = new MessageChannelMain();
  400. port2.postMessage('hello');
  401. port1.start();
  402. const [ev] = await once(port1, 'message');
  403. expect(ev.data).to.equal('hello');
  404. });
  405. it('can pass one end to a WebContents', async () => {
  406. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  407. w.loadURL('about:blank');
  408. await w.webContents.executeJavaScript(`(${function () {
  409. const { ipcRenderer } = require('electron');
  410. ipcRenderer.on('port', ev => {
  411. const [port] = ev.ports;
  412. port.onmessage = () => {
  413. ipcRenderer.send('done');
  414. };
  415. });
  416. }})()`);
  417. const { port1, port2 } = new MessageChannelMain();
  418. port1.postMessage('hello');
  419. w.webContents.postMessage('port', null, [port2]);
  420. await once(ipcMain, 'done');
  421. });
  422. it('can be passed over another channel', async () => {
  423. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  424. w.loadURL('about:blank');
  425. await w.webContents.executeJavaScript(`(${function () {
  426. const { ipcRenderer } = require('electron');
  427. ipcRenderer.on('port', e1 => {
  428. e1.ports[0].onmessage = e2 => {
  429. e2.ports[0].onmessage = e3 => {
  430. ipcRenderer.send('done', e3.data);
  431. };
  432. };
  433. });
  434. }})()`);
  435. const { port1, port2 } = new MessageChannelMain();
  436. const { port1: port3, port2: port4 } = new MessageChannelMain();
  437. port1.postMessage(null, [port4]);
  438. port3.postMessage('hello');
  439. w.webContents.postMessage('port', null, [port2]);
  440. const [, message] = await once(ipcMain, 'done');
  441. expect(message).to.equal('hello');
  442. });
  443. it('can send messages to a closed port', () => {
  444. const { port1, port2 } = new MessageChannelMain();
  445. port2.start();
  446. port2.on('message', () => { throw new Error('unexpected message received'); });
  447. port1.close();
  448. port1.postMessage('hello');
  449. });
  450. it('can send messages to a port whose remote end is closed', () => {
  451. const { port1, port2 } = new MessageChannelMain();
  452. port2.start();
  453. port2.on('message', () => { throw new Error('unexpected message received'); });
  454. port2.close();
  455. port1.postMessage('hello');
  456. });
  457. it('throws when passing null ports', () => {
  458. const { port1 } = new MessageChannelMain();
  459. expect(() => {
  460. port1.postMessage(null, [null] as any);
  461. }).to.throw(/Port at index 0 is not a valid port/);
  462. });
  463. it('throws when passing duplicate ports', () => {
  464. const { port1 } = new MessageChannelMain();
  465. const { port1: port3 } = new MessageChannelMain();
  466. expect(() => {
  467. port1.postMessage(null, [port3, port3]);
  468. }).to.throw(/duplicate/);
  469. });
  470. it('throws when passing ports that have already been neutered', () => {
  471. const { port1 } = new MessageChannelMain();
  472. const { port1: port3 } = new MessageChannelMain();
  473. port1.postMessage(null, [port3]);
  474. expect(() => {
  475. port1.postMessage(null, [port3]);
  476. }).to.throw(/already neutered/);
  477. });
  478. it('throws when passing itself', () => {
  479. const { port1 } = new MessageChannelMain();
  480. expect(() => {
  481. port1.postMessage(null, [port1]);
  482. }).to.throw(/contains the source port/);
  483. });
  484. describe('GC behavior', () => {
  485. it('is not collected while it could still receive messages', async () => {
  486. let trigger: Function;
  487. const promise = new Promise(resolve => { trigger = resolve; });
  488. const port1 = (() => {
  489. const { port1, port2 } = new MessageChannelMain();
  490. port2.on('message', (e) => { trigger(e.data); });
  491. port2.start();
  492. return port1;
  493. })();
  494. v8Util.requestGarbageCollectionForTesting();
  495. port1.postMessage('hello');
  496. expect(await promise).to.equal('hello');
  497. });
  498. });
  499. });
  500. const generateTests = (title: string, postMessage: (contents: WebContents) => WebContents['postMessage']) => {
  501. describe(title, () => {
  502. it('sends a message', async () => {
  503. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  504. w.loadURL('about:blank');
  505. await w.webContents.executeJavaScript(`(${function () {
  506. const { ipcRenderer } = require('electron');
  507. ipcRenderer.on('foo', (_e, msg) => {
  508. ipcRenderer.send('bar', msg);
  509. });
  510. }})()`);
  511. postMessage(w.webContents)('foo', { some: 'message' });
  512. const [, msg] = await once(ipcMain, 'bar');
  513. expect(msg).to.deep.equal({ some: 'message' });
  514. });
  515. describe('error handling', () => {
  516. it('throws on missing channel', async () => {
  517. const w = new BrowserWindow({ show: false });
  518. await w.loadURL('about:blank');
  519. expect(() => {
  520. (postMessage(w.webContents) as any)();
  521. }).to.throw(/Insufficient number of arguments/);
  522. });
  523. it('throws on invalid channel', async () => {
  524. const w = new BrowserWindow({ show: false });
  525. await w.loadURL('about:blank');
  526. expect(() => {
  527. postMessage(w.webContents)(null as any, '', []);
  528. }).to.throw(/Error processing argument at index 0/);
  529. });
  530. it('throws on missing message', async () => {
  531. const w = new BrowserWindow({ show: false });
  532. await w.loadURL('about:blank');
  533. expect(() => {
  534. (postMessage(w.webContents) as any)('channel');
  535. }).to.throw(/Insufficient number of arguments/);
  536. });
  537. it('throws on non-serializable message', async () => {
  538. const w = new BrowserWindow({ show: false });
  539. await w.loadURL('about:blank');
  540. expect(() => {
  541. postMessage(w.webContents)('channel', w);
  542. }).to.throw(/An object could not be cloned/);
  543. });
  544. it('throws on invalid transferable list', async () => {
  545. const w = new BrowserWindow({ show: false });
  546. await w.loadURL('about:blank');
  547. expect(() => {
  548. postMessage(w.webContents)('', '', null as any);
  549. }).to.throw(/Invalid value for transfer/);
  550. });
  551. it('throws on transferring non-transferable', async () => {
  552. const w = new BrowserWindow({ show: false });
  553. await w.loadURL('about:blank');
  554. expect(() => {
  555. (postMessage(w.webContents) as any)('channel', '', [123]);
  556. }).to.throw(/Invalid value for transfer/);
  557. });
  558. it('throws when passing null ports', async () => {
  559. const w = new BrowserWindow({ show: false });
  560. await w.loadURL('about:blank');
  561. expect(() => {
  562. postMessage(w.webContents)('foo', null, [null] as any);
  563. }).to.throw(/Invalid value for transfer/);
  564. });
  565. it('throws when passing duplicate ports', async () => {
  566. const w = new BrowserWindow({ show: false });
  567. await w.loadURL('about:blank');
  568. const { port1 } = new MessageChannelMain();
  569. expect(() => {
  570. postMessage(w.webContents)('foo', null, [port1, port1]);
  571. }).to.throw(/duplicate/);
  572. });
  573. it('throws when passing ports that have already been neutered', async () => {
  574. const w = new BrowserWindow({ show: false });
  575. await w.loadURL('about:blank');
  576. const { port1 } = new MessageChannelMain();
  577. postMessage(w.webContents)('foo', null, [port1]);
  578. expect(() => {
  579. postMessage(w.webContents)('foo', null, [port1]);
  580. }).to.throw(/already neutered/);
  581. });
  582. });
  583. });
  584. };
  585. generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents));
  586. generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame));
  587. });
  588. describe('WebContents.ipc', () => {
  589. afterEach(closeAllWindows);
  590. it('receives ipc messages sent from the WebContents', async () => {
  591. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  592. w.loadURL('about:blank');
  593. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
  594. const [, num] = await once(w.webContents.ipc, 'test');
  595. expect(num).to.equal(42);
  596. });
  597. it('receives sync-ipc messages sent from the WebContents', async () => {
  598. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  599. w.loadURL('about:blank');
  600. w.webContents.ipc.on('test', (event, arg) => {
  601. event.returnValue = arg * 2;
  602. });
  603. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)');
  604. expect(result).to.equal(42 * 2);
  605. });
  606. it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
  607. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  608. w.loadURL('about:blank');
  609. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])');
  610. const [event] = await once(w.webContents.ipc, 'test');
  611. expect(event.ports.length).to.equal(1);
  612. });
  613. it('handles invoke messages sent from the WebContents', async () => {
  614. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  615. w.loadURL('about:blank');
  616. w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
  617. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  618. expect(result).to.equal(42 * 2);
  619. });
  620. it('cascades to ipcMain', async () => {
  621. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  622. w.loadURL('about:blank');
  623. let gotFromIpcMain = false;
  624. const ipcMainReceived = new Promise<void>(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); }));
  625. const ipcReceived = new Promise<boolean>(resolve => w.webContents.ipc.on('test', () => { resolve(gotFromIpcMain); }));
  626. defer(() => ipcMain.removeAllListeners('test'));
  627. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
  628. // assert that they are delivered in the correct order
  629. expect(await ipcReceived).to.be.false();
  630. await ipcMainReceived;
  631. });
  632. it('overrides ipcMain handlers', async () => {
  633. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  634. w.loadURL('about:blank');
  635. w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
  636. ipcMain.handle('test', () => { throw new Error('should not be called'); });
  637. defer(() => ipcMain.removeHandler('test'));
  638. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  639. expect(result).to.equal(42 * 2);
  640. });
  641. it('falls back to ipcMain handlers', async () => {
  642. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  643. w.loadURL('about:blank');
  644. ipcMain.handle('test', (_event, arg) => { return arg * 2; });
  645. defer(() => ipcMain.removeHandler('test'));
  646. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  647. expect(result).to.equal(42 * 2);
  648. });
  649. it('receives ipcs from child frames', async () => {
  650. const server = http.createServer((req, res) => {
  651. res.setHeader('content-type', 'text/html');
  652. res.end('');
  653. });
  654. const { port } = await listen(server);
  655. defer(() => {
  656. server.close();
  657. });
  658. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } });
  659. // Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page.
  660. await w.loadURL(`data:text/html,<iframe src="http://localhost:${port}"></iframe>`);
  661. w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)');
  662. const [, arg] = await once(w.webContents.ipc, 'test');
  663. expect(arg).to.equal(42);
  664. });
  665. });
  666. describe('WebFrameMain.ipc', () => {
  667. afterEach(closeAllWindows);
  668. it('responds to ipc messages in the main frame', async () => {
  669. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  670. w.loadURL('about:blank');
  671. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
  672. const [, arg] = await once(w.webContents.mainFrame.ipc, 'test');
  673. expect(arg).to.equal(42);
  674. });
  675. it('responds to sync ipc messages in the main frame', async () => {
  676. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  677. w.loadURL('about:blank');
  678. w.webContents.mainFrame.ipc.on('test', (event, arg) => {
  679. event.returnValue = arg * 2;
  680. });
  681. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)');
  682. expect(result).to.equal(42 * 2);
  683. });
  684. it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
  685. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  686. w.loadURL('about:blank');
  687. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])');
  688. const [event] = await once(w.webContents.mainFrame.ipc, 'test');
  689. expect(event.ports.length).to.equal(1);
  690. });
  691. it('handles invoke messages sent from the WebContents', async () => {
  692. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  693. w.loadURL('about:blank');
  694. w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
  695. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  696. expect(result).to.equal(42 * 2);
  697. });
  698. it('cascades to WebContents and ipcMain', async () => {
  699. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  700. w.loadURL('about:blank');
  701. let gotFromIpcMain = false;
  702. let gotFromWebContents = false;
  703. const ipcMainReceived = new Promise<void>(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); }));
  704. const ipcWebContentsReceived = new Promise<boolean>(resolve => w.webContents.ipc.on('test', () => { gotFromWebContents = true; resolve(gotFromIpcMain); }));
  705. const ipcReceived = new Promise<boolean>(resolve => w.webContents.mainFrame.ipc.on('test', () => { resolve(gotFromWebContents); }));
  706. defer(() => ipcMain.removeAllListeners('test'));
  707. w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
  708. // assert that they are delivered in the correct order
  709. expect(await ipcReceived).to.be.false();
  710. expect(await ipcWebContentsReceived).to.be.false();
  711. await ipcMainReceived;
  712. });
  713. it('overrides ipcMain handlers', async () => {
  714. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  715. w.loadURL('about:blank');
  716. w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
  717. ipcMain.handle('test', () => { throw new Error('should not be called'); });
  718. defer(() => ipcMain.removeHandler('test'));
  719. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  720. expect(result).to.equal(42 * 2);
  721. });
  722. it('overrides WebContents handlers', async () => {
  723. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  724. w.loadURL('about:blank');
  725. w.webContents.ipc.handle('test', () => { throw new Error('should not be called'); });
  726. w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
  727. ipcMain.handle('test', () => { throw new Error('should not be called'); });
  728. defer(() => ipcMain.removeHandler('test'));
  729. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  730. expect(result).to.equal(42 * 2);
  731. });
  732. it('falls back to WebContents handlers', async () => {
  733. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  734. w.loadURL('about:blank');
  735. w.webContents.ipc.handle('test', (_event, arg) => { return arg * 2; });
  736. const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
  737. expect(result).to.equal(42 * 2);
  738. });
  739. it('receives ipcs from child frames', async () => {
  740. const server = http.createServer((req, res) => {
  741. res.setHeader('content-type', 'text/html');
  742. res.end('');
  743. });
  744. const { port } = await listen(server);
  745. defer(() => {
  746. server.close();
  747. });
  748. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } });
  749. // Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page.
  750. await w.loadURL(`data:text/html,<iframe src="http://localhost:${port}"></iframe>`);
  751. w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)');
  752. w.webContents.mainFrame.ipc.on('test', () => { throw new Error('should not be called'); });
  753. const [, arg] = await once(w.webContents.mainFrame.frames[0].ipc, 'test');
  754. expect(arg).to.equal(42);
  755. });
  756. });
  757. });