import { expect } from 'chai' import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron' 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)) }) }) })