api-ipc-spec.ts 34 KB

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