import { EventEmitter } from 'events' import { expect } from 'chai' import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain } from 'electron' import { closeAllWindows } from './window-helpers' import { emittedOnce } from './events-helpers' const v8Util = process.electronBinding('v8_util') describe('ipc module', () => { describe('invoke', () => { let w = (null as unknown as BrowserWindow) before(async () => { w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) await w.loadURL('about:blank') }) after(async () => { w.destroy() }) async function rendererInvoke (...args: any[]) { const { ipcRenderer } = require('electron') try { const result = await ipcRenderer.invoke('test', ...args) ipcRenderer.send('result', { result }) } catch (e) { ipcRenderer.send('result', { error: e.message }) } } it('receives a response from a synchronous handler', async () => { ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => { expect(arg).to.equal(123) return 3 }) const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg).to.deep.equal({ result: 3 }) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`) await done }) it('receives a response from an asynchronous handler', async () => { ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => { expect(arg).to.equal(123) await new Promise(resolve => setImmediate(resolve)) return 3 }) const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg).to.deep.equal({ result: 3 }) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`) await done }) it('receives an error from a synchronous handler', async () => { ipcMain.handleOnce('test', () => { throw new Error('some error') }) const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg.error).to.match(/some error/) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})()`) await done }) it('receives an error from an asynchronous handler', async () => { ipcMain.handleOnce('test', async () => { await new Promise(resolve => setImmediate(resolve)) throw new Error('some error') }) const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg.error).to.match(/some error/) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})()`) await done }) it('throws an error if no handler is registered', async () => { const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg.error).to.match(/No handler registered/) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})()`) await done }) it('throws an error when invoking a handler that was removed', async () => { ipcMain.handle('test', () => {}) ipcMain.removeHandler('test') const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg.error).to.match(/No handler registered/) resolve() })) await w.webContents.executeJavaScript(`(${rendererInvoke})()`) await done }) it('forbids multiple handlers', async () => { ipcMain.handle('test', () => {}) try { expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/) } finally { ipcMain.removeHandler('test') } }) it('throws an error in the renderer if the reply callback is dropped', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars ipcMain.handleOnce('test', () => new Promise(resolve => { setTimeout(() => v8Util.requestGarbageCollectionForTesting()) /* never resolve */ })) w.webContents.executeJavaScript(`(${rendererInvoke})()`) const [, { error }] = await emittedOnce(ipcMain, 'result') expect(error).to.match(/reply was never sent/) }) }) describe('ordering', () => { let w = (null as unknown as BrowserWindow) before(async () => { w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) await w.loadURL('about:blank') }) after(async () => { w.destroy() }) it('between send and sendSync is consistent', async () => { const received: number[] = [] ipcMain.on('test-async', (e, i) => { received.push(i) }) ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null }) const done = new Promise(resolve => ipcMain.once('done', () => { resolve() })) function rendererStressTest () { const { ipcRenderer } = require('electron') for (let i = 0; i < 1000; i++) { switch ((Math.random() * 2) | 0) { case 0: ipcRenderer.send('test-async', i) break case 1: ipcRenderer.sendSync('test-sync', i) break } } ipcRenderer.send('done') } try { w.webContents.executeJavaScript(`(${rendererStressTest})()`) await done } finally { ipcMain.removeAllListeners('test-async') ipcMain.removeAllListeners('test-sync') } expect(received).to.have.lengthOf(1000) expect(received).to.deep.equal([...received].sort((a, b) => a - b)) }) it('between send, sendSync, and invoke is consistent', async () => { const received: number[] = [] ipcMain.handle('test-invoke', (e, i) => { received.push(i) }) ipcMain.on('test-async', (e, i) => { received.push(i) }) ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null }) const done = new Promise(resolve => ipcMain.once('done', () => { resolve() })) function rendererStressTest () { const { ipcRenderer } = require('electron') for (let i = 0; i < 1000; i++) { switch ((Math.random() * 3) | 0) { case 0: ipcRenderer.send('test-async', i) break case 1: ipcRenderer.sendSync('test-sync', i) break case 2: ipcRenderer.invoke('test-invoke', i) break } } ipcRenderer.send('done') } try { w.webContents.executeJavaScript(`(${rendererStressTest})()`) await done } finally { ipcMain.removeHandler('test-invoke') ipcMain.removeAllListeners('test-async') ipcMain.removeAllListeners('test-sync') } expect(received).to.have.lengthOf(1000) expect(received).to.deep.equal([...received].sort((a, b) => a - b)) }) }) describe('MessagePort', () => { afterEach(closeAllWindows) it('can send a port to the main process', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') const p = emittedOnce(ipcMain, 'port') await w.webContents.executeJavaScript(`(${function () { const channel = new MessageChannel() require('electron').ipcRenderer.postMessage('port', 'hi', [channel.port1]) }})()`) const [ev, msg] = await p expect(msg).to.equal('hi') expect(ev.ports).to.have.length(1) const [port] = ev.ports expect(port).to.be.an.instanceOf(EventEmitter) }) it('can communicate between main and renderer', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') const p = emittedOnce(ipcMain, 'port') await w.webContents.executeJavaScript(`(${function () { const channel = new MessageChannel(); (channel.port2 as any).onmessage = (ev: any) => { channel.port2.postMessage(ev.data * 2) } require('electron').ipcRenderer.postMessage('port', '', [channel.port1]) }})()`) const [ev] = await p expect(ev.ports).to.have.length(1) const [port] = ev.ports port.start() port.postMessage(42) const [ev2] = await emittedOnce(port, 'message') expect(ev2.data).to.equal(84) }) it('can receive a port from a renderer over a MessagePort connection', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') function fn () { const channel1 = new MessageChannel() const channel2 = new MessageChannel() channel1.port2.postMessage('', [channel2.port1]) channel2.port2.postMessage('matryoshka') require('electron').ipcRenderer.postMessage('port', '', [channel1.port1]) } w.webContents.executeJavaScript(`(${fn})()`) const [{ ports: [port1] }] = await emittedOnce(ipcMain, 'port') port1.start() const [{ ports: [port2] }] = await emittedOnce(port1, 'message') port2.start() const [{ data }] = await emittedOnce(port2, 'message') expect(data).to.equal('matryoshka') }) it('can forward a port from one renderer to another renderer', async () => { const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w1.loadURL('about:blank') w2.loadURL('about:blank') w1.webContents.executeJavaScript(`(${function () { const channel = new MessageChannel(); (channel.port2 as any).onmessage = (ev: any) => { require('electron').ipcRenderer.send('message received', ev.data) } require('electron').ipcRenderer.postMessage('port', '', [channel.port1]) }})()`) const [{ ports: [port] }] = await emittedOnce(ipcMain, 'port') await w2.webContents.executeJavaScript(`(${function () { require('electron').ipcRenderer.on('port', ({ ports: [port] }: any) => { port.postMessage('a message') }) }})()`) w2.webContents.postMessage('port', '', [port]) const [, data] = await emittedOnce(ipcMain, 'message received') expect(data).to.equal('a message') }) describe('close event', () => { describe('in renderer', () => { it('is emitted when the main process closes its end of the port', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') await w.webContents.executeJavaScript(`(${function () { const { ipcRenderer } = require('electron') ipcRenderer.on('port', (e) => { const [port] = e.ports port.start(); (port as any).onclose = () => { ipcRenderer.send('closed') } }) }})()`) const { port1, port2 } = new MessageChannelMain() w.webContents.postMessage('port', null, [port2]) port1.close() await emittedOnce(ipcMain, 'closed') }) it('is emitted when the other end of a port is garbage-collected', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') await w.webContents.executeJavaScript(`(${async function () { const { port2 } = new MessageChannel() await new Promise(resolve => { port2.start(); (port2 as any).onclose = resolve process.electronBinding('v8_util').requestGarbageCollectionForTesting() }) }})()`) }) it('is emitted when the other end of a port is sent to nowhere', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') ipcMain.once('do-a-gc', () => v8Util.requestGarbageCollectionForTesting()) await w.webContents.executeJavaScript(`(${async function () { const { port1, port2 } = new MessageChannel() await new Promise(resolve => { port2.start(); (port2 as any).onclose = resolve require('electron').ipcRenderer.postMessage('nobody-listening', null, [port1]) require('electron').ipcRenderer.send('do-a-gc') }) }})()`) }) }) }) describe('MessageChannelMain', () => { it('can be created', () => { const { port1, port2 } = new MessageChannelMain() expect(port1).not.to.be.null() expect(port2).not.to.be.null() }) it('can send messages within the process', async () => { const { port1, port2 } = new MessageChannelMain() port2.postMessage('hello') port1.start() const [ev] = await emittedOnce(port1, 'message') expect(ev.data).to.equal('hello') }) it('can pass one end to a WebContents', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') await w.webContents.executeJavaScript(`(${function () { const { ipcRenderer } = require('electron') ipcRenderer.on('port', (ev) => { const [port] = ev.ports port.onmessage = () => { ipcRenderer.send('done') } }) }})()`) const { port1, port2 } = new MessageChannelMain() port1.postMessage('hello') w.webContents.postMessage('port', null, [port2]) await emittedOnce(ipcMain, 'done') }) it('can be passed over another channel', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') await w.webContents.executeJavaScript(`(${function () { const { ipcRenderer } = require('electron') ipcRenderer.on('port', (e1) => { e1.ports[0].onmessage = (e2) => { e2.ports[0].onmessage = (e3) => { ipcRenderer.send('done', e3.data) } } }) }})()`) const { port1, port2 } = new MessageChannelMain() const { port1: port3, port2: port4 } = new MessageChannelMain() port1.postMessage(null, [port4]) port3.postMessage('hello') w.webContents.postMessage('port', null, [port2]) const [, message] = await emittedOnce(ipcMain, 'done') expect(message).to.equal('hello') }) it('can send messages to a closed port', () => { const { port1, port2 } = new MessageChannelMain() port2.start() port2.on('message', () => { throw new Error('unexpected message received') }) port1.close() port1.postMessage('hello') }) it('can send messages to a port whose remote end is closed', () => { const { port1, port2 } = new MessageChannelMain() port2.start() port2.on('message', () => { throw new Error('unexpected message received') }) port2.close() port1.postMessage('hello') }) it('throws when passing null ports', () => { const { port1 } = new MessageChannelMain() expect(() => { port1.postMessage(null, [null] as any) }).to.throw(/conversion failure/) }) it('throws when passing duplicate ports', () => { const { port1 } = new MessageChannelMain() const { port1: port3 } = new MessageChannelMain() expect(() => { port1.postMessage(null, [port3, port3]) }).to.throw(/duplicate/) }) it('throws when passing ports that have already been neutered', () => { const { port1 } = new MessageChannelMain() const { port1: port3 } = new MessageChannelMain() port1.postMessage(null, [port3]) expect(() => { port1.postMessage(null, [port3]) }).to.throw(/already neutered/) }) it('throws when passing itself', () => { const { port1 } = new MessageChannelMain() expect(() => { port1.postMessage(null, [port1]) }).to.throw(/contains the source port/) }) describe('GC behavior', () => { it('is not collected while it could still receive messages', async () => { let trigger: Function const promise = new Promise(resolve => { trigger = resolve }) const port1 = (() => { const { port1, port2 } = new MessageChannelMain() port2.on('message', (e) => { trigger(e.data) }) port2.start() return port1 })() v8Util.requestGarbageCollectionForTesting() port1.postMessage('hello') expect(await promise).to.equal('hello') }) }) }) describe('WebContents.postMessage', () => { it('sends a message', async () => { const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) w.loadURL('about:blank') await w.webContents.executeJavaScript(`(${function () { const { ipcRenderer } = require('electron') ipcRenderer.on('foo', (e, msg) => { ipcRenderer.send('bar', msg) }) }})()`) w.webContents.postMessage('foo', { some: 'message' }) const [, msg] = await emittedOnce(ipcMain, 'bar') expect(msg).to.deep.equal({ some: 'message' }) }) describe('error handling', () => { it('throws on missing channel', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { (w.webContents.postMessage as any)() }).to.throw(/Insufficient number of arguments/) }) it('throws on invalid channel', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { w.webContents.postMessage(null as any, '', []) }).to.throw(/Error processing argument at index 0/) }) it('throws on missing message', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { (w.webContents.postMessage as any)('channel') }).to.throw(/Insufficient number of arguments/) }) it('throws on non-serializable message', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { w.webContents.postMessage('channel', w) }).to.throw(/An object could not be cloned/) }) it('throws on invalid transferable list', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { w.webContents.postMessage('', '', null as any) }).to.throw(/Invalid value for transfer/) }) it('throws on transferring non-transferable', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { (w.webContents.postMessage as any)('channel', '', [123]) }).to.throw(/Invalid value for transfer/) }) it('throws when passing null ports', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') expect(() => { w.webContents.postMessage('foo', null, [null] as any) }).to.throw(/Invalid value for transfer/) }) it('throws when passing duplicate ports', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') const { port1 } = new MessageChannelMain() expect(() => { w.webContents.postMessage('foo', null, [port1, port1]) }).to.throw(/duplicate/) }) it('throws when passing ports that have already been neutered', async () => { const w = new BrowserWindow({ show: false }) await w.loadURL('about:blank') const { port1 } = new MessageChannelMain() w.webContents.postMessage('foo', null, [port1]) expect(() => { w.webContents.postMessage('foo', null, [port1]) }).to.throw(/already neutered/) }) }) }) }) })