├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── preload.js ├── src ├── base.ts ├── index.ts ├── mainProcess.ts └── renderer.ts ├── test ├── .eslintrc ├── import-tests.ts ├── index-tests.ts ├── mainProcess-tests.ts ├── renderer-tests.ts └── require-tests.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ], 6 | "plugins": [ 7 | "@babel/proposal-class-properties", 8 | "@babel/proposal-object-rest-spread" 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "@babel/proposal-class-properties", 14 | "@babel/proposal-object-rest-spread", 15 | "istanbul" 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | preload.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended", "airbnb", "prettier"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "node": true, 10 | "es6": true 11 | }, 12 | "rules": { 13 | "global-require": 0, 14 | "comma-dangle": 0, 15 | "wrap-iife": ["error", "inside"], 16 | "implicit-arrow-linebreak": "warn", 17 | "@typescript-eslint/no-explicit-any": 2, 18 | "import/no-unresolved": 0, 19 | "import/extensions": 0, 20 | "@typescript-eslint/no-empty-function": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | .coveralls.yml 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # App packaged 35 | dist 36 | release 37 | build 38 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register", "source-map-support/register"], 3 | "full-trace": true, 4 | "bail": true, 5 | "package": "./package.json", 6 | "extension": ["js", "ts"] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | .coveralls.yml 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # IDE 41 | .vscode 42 | 43 | # yarn.lock file 44 | yarn.lock 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | node_js: 4 | - 10 5 | - 12 6 | - 14 7 | - node 8 | os: 9 | - linux 10 | - osx 11 | after_success: 'npm run coveralls' 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ian Sibner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-promise-ipc 2 | 3 | [![Build Status](https://travis-ci.org/sibnerian/electron-promise-ipc.svg?branch=master)](https://travis-ci.org/sibnerian/electron-promise-ipc) [![Coverage Status](https://coveralls.io/repos/github/sibnerian/electron-promise-ipc/badge.svg?branch=master)](https://coveralls.io/github/sibnerian/electron-promise-ipc?branch=master) [![npm version](https://badge.fury.io/js/electron-promise-ipc.svg)](https://badge.fury.io/js/electron-promise-ipc) 4 | 5 | #### Promise-y IPC calls in Electron. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install --save electron-promise-ipc 11 | ``` 12 | 13 | ## Usage 14 | 15 | The most common use case: from the renderer, get data from the main process as a promise. 16 | 17 | ```js 18 | // in main process 19 | import promiseIpc from 'electron-promise-ipc'; 20 | import fsp from 'fs-promise'; 21 | 22 | promiseIpc.on('writeSettingsFile', (newSettings, event) => { 23 | return fsp.writeFile('~/.settings', newSettings); 24 | }); 25 | 26 | // in renderer 27 | import promiseIpc from 'electron-promise-ipc'; 28 | 29 | promiseIpc 30 | .send('writeSettingsFile', '{ "name": "Jeff" }') 31 | .then(() => console.log('You wrote the settings!')) 32 | .catch((e) => console.error(e)); 33 | ``` 34 | 35 | You can also send data from the main process to a renderer, if you pass in its [WebContents](http://electron.atom.io/docs/api/web-contents) object. 36 | 37 | ```js 38 | // in main process 39 | import promiseIpc from 'electron-promise-ipc'; 40 | 41 | promiseIpc 42 | .send('getRendererData', webContentsForRenderer) 43 | .then((rendererData) => console.log(rendererData)) 44 | .catch((e) => console.error(e)); 45 | 46 | // in renderer 47 | import promiseIpc from 'electron-promise-ipc'; 48 | 49 | promiseIpc.on('getRendererData', (event) => { 50 | return getSomeSuperAwesomeRendererData(); 51 | }); 52 | ``` 53 | 54 | Any arguments to `send()` will be passed directly to the event listener from `on()`, followed by the IPC [event](https://electronjs.org/docs/api/ipc-main#event-object) object. If there is an error thrown in the main process's listener, or if the listener returns a rejected promise (e.g., lack of permissions for a file read), then the `send()` promise is rejected with the same error. 55 | 56 | Note that because this is IPC, only JSON-serializable values can be passed as arguments or data. Classes and functions will generally not survive a round of serialization/deserialization. 57 | 58 | ## Preload 59 | 60 | As of Electron 5.0, `nodeIntegration` is _disabled by default._ This means that you cannot import `promiseIpc` directly. Instead, you will need to use a [preload](https://www.electronjs.org/docs/api/browser-window) script when opening a `BrowserWindow`. Preload scripts can access builtins such as `require` even if `nodeIntegration` is disabled. 61 | 62 | For convenience, this library provides a preload script which you can require that sets `window.promiseIpc`. 63 | 64 | ```js 65 | // preload.js 66 | require('electron-promise-ipc/preload'); 67 | ``` 68 | 69 | ## Advanced usage 70 | 71 | #### Timeouts 72 | 73 | By default, the promise will wait forever for the other process to return it some data. If you want to set a timeout (after which the promise will be rejected automatically), you can create another instance of `PromiseIpc` like so: 74 | 75 | ```js 76 | // main process code remains the same 77 | import promiseIpc from 'electron-promise-ipc'; 78 | 79 | promiseIpc.on('someRoute', () => { 80 | return someOperationThatNeverCompletesUhOh(); 81 | }); 82 | 83 | // in renderer - timeout is specified on the side that requests the data 84 | import { PromiseIpc } from 'electron-promise-ipc'; 85 | 86 | const promiseIpc = new PromiseIpc({ maxTimeoutMs: 2000 }); 87 | 88 | promiseIpc 89 | .send('someRoute', '{ "name": "Jeff" }') 90 | .then(() => console.log('You wrote the settings!')) 91 | .catch((e) => console.error(e)); // will error out after 2 seconds 92 | ``` 93 | 94 | #### Removing Listeners 95 | 96 | You can remove a listener with the `off()` method. It's aliased to `removeListener()` as well. 97 | 98 | ```js 99 | import promiseIpc from 'electron-promise-ipc'; 100 | 101 | promiseIpc.on('someRoute', () => { 102 | return something(); 103 | }); 104 | 105 | promiseIpc.off('someRoute'); // never mind 106 | ``` 107 | 108 | ## License 109 | 110 | MIT 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-promise-ipc", 3 | "version": "2.2.4", 4 | "description": "Run IPC calls with a promise API.", 5 | "scripts": { 6 | "pretest": "if [ 'v6' = $(node --version | cut -c -2) ] ; then echo 'Skipping lint for node v6' ; else npm run --silent lint; fi && npm run build", 7 | "coverage": "nyc mocha", 8 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 9 | "test": "cross-env NODE_ENV=test npm run coverage", 10 | "lint": "eslint . --ext=.ts,.js", 11 | "build": "mkdirp build && tsc && babel src --out-dir build --source-maps", 12 | "prepublishOnly": "npm run build", 13 | "clean": "rimraf build" 14 | }, 15 | "main": "build/index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/sibnerian/electron-promise-ipc.git" 19 | }, 20 | "author": "Ian Sibner ", 21 | "license": "MIT", 22 | "peerDependencies": { 23 | "electron": ">= 9.1.2" 24 | }, 25 | "dependencies": { 26 | "is-electron-renderer": "^2.0.1", 27 | "object.entries": "^1.1.3", 28 | "serialize-error": "^5.0.0", 29 | "uuid": "^3.0.1" 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.12.8", 33 | "@babel/core": "^7.12.9", 34 | "@babel/plugin-proposal-class-properties": "^7.12.1", 35 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 36 | "@babel/polyfill": "^7.12.1", 37 | "@babel/preset-env": "^7.12.7", 38 | "@babel/preset-typescript": "^7.12.7", 39 | "@babel/register": "^7.12.1", 40 | "@types/chai": "^4.2.14", 41 | "@types/chai-as-promised": "^7.1.3", 42 | "@types/lolex": "^3.1.1", 43 | "@types/mocha": "^5.2.7", 44 | "@types/serialize-error": "^4.0.1", 45 | "@types/uuid": "^3.4.9", 46 | "@typescript-eslint/eslint-plugin": "^2.34.0", 47 | "@typescript-eslint/parser": "^2.34.0", 48 | "babel-plugin-istanbul": "^5.1.1", 49 | "chai": "^4.2.0", 50 | "chai-as-promised": "^7.1.1", 51 | "coveralls": "^3.1.0", 52 | "cross-env": "^5.2.0", 53 | "electron": "^11.0.3", 54 | "electron-ipc-mock": "^0.0.3", 55 | "eslint": "^5.14.1", 56 | "eslint-config-airbnb": "^17.1.0", 57 | "eslint-config-prettier": "^6.15.0", 58 | "eslint-plugin-import": "^2.22.1", 59 | "eslint-plugin-jsx-a11y": "^6.4.1", 60 | "eslint-plugin-prettier": "^3.2.0", 61 | "eslint-plugin-react": "^7.21.5", 62 | "lolex": "^1.6.0", 63 | "mkdirp": "^0.5.1", 64 | "mocha": "^8.2.1", 65 | "nyc": "^14.1.1", 66 | "prettier": "^1.18.2", 67 | "proxyquire": "2.1.0", 68 | "rimraf": "^2.6.3", 69 | "sinon": "^7.2.5", 70 | "sinon-chai": "^3.3.0", 71 | "source-map-support": "^0.5.19", 72 | "ts-node": "^8.10.2", 73 | "typescript": "^3.9.7" 74 | }, 75 | "nyc": { 76 | "include": [ 77 | "src/**/*.ts" 78 | ], 79 | "extension": [ 80 | ".ts", 81 | ".tsx" 82 | ], 83 | "require": [ 84 | "ts-node/register" 85 | ], 86 | "sourceMap": true, 87 | "instrument": true 88 | }, 89 | "directories": { 90 | "test": "test" 91 | }, 92 | "keywords": [ 93 | "electron", 94 | "promise", 95 | "ipc" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const promiseIpc = require('./build/index.js'); 2 | 3 | window.promiseIpc = promiseIpc; 4 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | import { serializeError } from 'serialize-error'; 3 | import type { IpcMain, IpcRenderer, WebContents, IpcMainEvent, IpcRendererEvent } from 'electron'; 4 | import 'object.entries/auto'; // Shim Object.entries. Required to use serializeError. 5 | 6 | type IpcEvent = IpcRendererEvent & IpcMainEvent; 7 | 8 | /** 9 | * For backwards compatibility, event is the (optional) LAST argument to a listener function. 10 | * This leads to the following verbose overload type for a listener function. 11 | */ 12 | export type Listener = 13 | | { (event?: IpcEvent): void } 14 | | { (arg1?: unknown, event?: IpcEvent): void } 15 | | { (arg1?: unknown, arg2?: unknown, event?: IpcEvent): void } 16 | | { (arg1?: unknown, arg2?: unknown, arg3?: unknown, event?: IpcEvent): void } 17 | | { 18 | ( 19 | arg1?: unknown, 20 | arg2?: unknown, 21 | arg3?: unknown, 22 | arg4?: unknown, 23 | event?: IpcEvent, 24 | ): void; 25 | } 26 | | { 27 | ( 28 | arg1?: unknown, 29 | arg2?: unknown, 30 | arg3?: unknown, 31 | arg4?: unknown, 32 | arg5?: unknown, 33 | event?: IpcEvent, 34 | ): void; 35 | }; 36 | export type Options = { maxTimeoutMs?: number }; 37 | // There's an `any` here it's the only way that the typescript compiler allows you to call listener(...dataArgs, event). 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | type WrappedListener = { (event: IpcEvent, replyChannel: string, ...dataArgs: any[]): void }; 40 | 41 | export default class PromiseIpcBase { 42 | private eventEmitter: IpcMain | IpcRenderer; 43 | 44 | private maxTimeoutMs: number; 45 | 46 | private routeListenerMap: Map; 47 | 48 | private listenerMap: Map; 49 | 50 | constructor(opts: { maxTimeoutMs?: number } | undefined, eventEmitter: IpcMain | IpcRenderer) { 51 | if (opts && opts.maxTimeoutMs) { 52 | this.maxTimeoutMs = opts.maxTimeoutMs; 53 | } // either ipcRenderer or ipcMain 54 | 55 | this.eventEmitter = eventEmitter; 56 | this.routeListenerMap = new Map(); 57 | this.listenerMap = new Map(); 58 | } 59 | 60 | public send( 61 | route: string, 62 | sender: WebContents | IpcRenderer, 63 | ...dataArgs: unknown[] 64 | ): Promise { 65 | return new Promise((resolve, reject) => { 66 | const replyChannel = `${route}#${uuid()}`; 67 | let timeout: NodeJS.Timeout; 68 | let didTimeOut = false; // ipcRenderer will send a message back to replyChannel when it finishes calculating 69 | 70 | this.eventEmitter.once( 71 | replyChannel, 72 | (event: IpcEvent, status: string, returnData: unknown) => { 73 | clearTimeout(timeout); 74 | if (didTimeOut) { 75 | return null; 76 | } 77 | switch (status) { 78 | case 'success': 79 | return resolve(returnData); 80 | case 'failure': 81 | return reject(returnData); 82 | default: 83 | return reject(new Error(`Unexpected IPC call status "${status}" in ${route}`)); 84 | } 85 | }, 86 | ); 87 | sender.send(route, replyChannel, ...dataArgs); 88 | if (this.maxTimeoutMs) { 89 | timeout = setTimeout(() => { 90 | didTimeOut = true; 91 | reject(new Error(`${route} timed out.`)); 92 | }, this.maxTimeoutMs); 93 | } 94 | }); 95 | } 96 | 97 | public on(route: string, listener: Listener): PromiseIpcBase { 98 | const prevListener = this.routeListenerMap.get(route); // If listener has already been added for this route, don't add it again. 99 | if (prevListener === listener) { 100 | return this; 101 | } // Only one listener may be active for a given route. // If two are active promises it won't work correctly - that's a race condition. 102 | if (this.routeListenerMap.has(route)) { 103 | this.off(route, prevListener); 104 | } // This function _wraps_ the listener argument. We maintain a map of // listener -> wrapped listener in order to implement #off(). 105 | const wrappedListener: WrappedListener = (event, replyChannel, ...dataArgs): void => { 106 | // Chaining off of Promise.resolve() means that listener can return a promise, or return 107 | // synchronously -- it can even throw. The end result will still be handled promise-like. 108 | Promise.resolve() 109 | .then(() => listener(...dataArgs, event)) 110 | .then((results) => { 111 | event.sender.send(replyChannel, 'success', results); 112 | }) 113 | .catch((e) => { 114 | event.sender.send(replyChannel, 'failure', serializeError(e)); 115 | }); 116 | }; 117 | this.routeListenerMap.set(route, listener); 118 | this.listenerMap.set(listener, wrappedListener); 119 | this.eventEmitter.on(route, wrappedListener); 120 | return this; 121 | } 122 | 123 | public off(route: string, listener?: Listener): void { 124 | const registeredListener = this.routeListenerMap.get(route); 125 | if (listener && listener !== registeredListener) { 126 | return; // trying to remove the wrong listener, so do nothing. 127 | } 128 | const wrappedListener = this.listenerMap.get(registeredListener); 129 | this.eventEmitter.removeListener(route, wrappedListener); 130 | this.listenerMap.delete(registeredListener); 131 | this.routeListenerMap.delete(route); 132 | } 133 | 134 | public removeListener(route: string, listener?: Listener): void { 135 | this.off(route, listener); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import isRenderer from 'is-electron-renderer'; 2 | import renderer, { RendererProcessType } from './renderer'; 3 | import mainProcess, { MainProcessType } from './mainProcess'; 4 | 5 | const exportedModule: RendererProcessType | MainProcessType = isRenderer ? renderer : mainProcess; 6 | module.exports = exportedModule; 7 | export default exportedModule; 8 | 9 | // Re-export the renderer and main process types for consumer modules to access 10 | export { RendererProcessType } from './renderer'; 11 | export { MainProcessType } from './mainProcess'; 12 | -------------------------------------------------------------------------------- /src/mainProcess.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, WebContents } from 'electron'; 2 | import PromiseIpcBase, { Options } from './base'; 3 | 4 | export class PromiseIpcMain extends PromiseIpcBase { 5 | constructor(opts?: Options) { 6 | super(opts, ipcMain); 7 | } 8 | 9 | // Send requires webContents -- see http://electron.atom.io/docs/api/ipc-main/ 10 | public send(route: string, webContents: WebContents, ...dataArgs: unknown[]): Promise { 11 | return super.send(route, webContents, ...dataArgs); 12 | } 13 | } 14 | 15 | export type MainProcessType = PromiseIpcMain & { 16 | PromiseIpc?: typeof PromiseIpcMain; 17 | PromiseIpcMain?: typeof PromiseIpcMain; 18 | }; 19 | 20 | export const PromiseIpc = PromiseIpcMain; 21 | 22 | const mainExport: MainProcessType = new PromiseIpcMain(); 23 | mainExport.PromiseIpc = PromiseIpcMain; 24 | mainExport.PromiseIpcMain = PromiseIpcMain; 25 | 26 | module.exports = mainExport; 27 | export default mainExport; 28 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; // eslint-disable-line 2 | import PromiseIpcBase, { Options } from './base'; 3 | 4 | export class PromiseIpcRenderer extends PromiseIpcBase { 5 | constructor(opts?: Options) { 6 | super(opts, ipcRenderer); 7 | } 8 | 9 | public send(route: string, ...dataArgs: unknown[]): Promise { 10 | return super.send(route, ipcRenderer, ...dataArgs); 11 | } 12 | } 13 | 14 | export type RendererProcessType = PromiseIpcRenderer & { 15 | PromiseIpc?: typeof PromiseIpcRenderer; 16 | PromiseIpcRenderer?: typeof PromiseIpcRenderer; 17 | }; 18 | 19 | export const PromiseIpc = PromiseIpcRenderer; 20 | 21 | const rendererExport: RendererProcessType = new PromiseIpcRenderer(); 22 | rendererExport.PromiseIpc = PromiseIpcRenderer; 23 | rendererExport.PromiseIpcRenderer = PromiseIpcRenderer; 24 | 25 | module.exports = rendererExport; 26 | export default rendererExport; 27 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "@typescript-eslint/no-explicit-any": 1, 7 | "import/no-extraneous-dependencies": [ 8 | 2, 9 | { 10 | "devDependencies": true 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/import-tests.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | // we are main process by default 3 | import defaultExport, { MainProcessType } from '../src/index'; 4 | 5 | const { PromiseIpc, PromiseIpcMain } = defaultExport as MainProcessType; 6 | 7 | describe('importing the built module', () => { 8 | it('exports a PromiseIpcMain function and PromiseIpc as an alias', () => { 9 | expect(typeof PromiseIpc).to.eql('function'); 10 | expect(typeof PromiseIpcMain).to.eql('function'); 11 | expect(PromiseIpc).to.eql(PromiseIpcMain); 12 | }); 13 | 14 | it('exports an instance of PromiseIpcMain as a default', () => { 15 | expect(defaultExport instanceof PromiseIpcMain).to.eql(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/index-tests.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | const proxyquire = require('proxyquire').noPreserveCache(); 4 | 5 | const renderer = { renderer: true }; 6 | const mainProcess = { mainProcess: true }; 7 | 8 | describe('index', () => { 9 | it('imports the renderer promiseIpc in the renderer environment', () => { 10 | const promiseIpc = proxyquire('../src/index', { 11 | './renderer': renderer, 12 | './mainProcess': mainProcess, 13 | 'is-electron-renderer': true, 14 | }); 15 | expect(promiseIpc).to.eql(renderer); 16 | }); 17 | 18 | it('imports the main process promiseIpc in the mainProcess environment', () => { 19 | const promiseIpc = proxyquire('../src/index', { 20 | './renderer': renderer, 21 | './mainProcess': mainProcess, 22 | 'is-electron-renderer': false, 23 | }); 24 | expect(promiseIpc).to.eql(mainProcess); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/mainProcess-tests.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import lolex from 'lolex'; 4 | import { fail } from 'assert'; 5 | import electronIpcMock from 'electron-ipc-mock'; 6 | import { IpcMainEvent, WebContents, IpcRendererEvent } from 'electron'; 7 | import { MainProcessType } from '../src/index'; 8 | 9 | const proxyquire: any = require('proxyquire'); // eslint-disable-line 10 | 11 | const { ipcRenderer, ipcMain } = electronIpcMock(); 12 | 13 | chai.use(chaiAsPromised); 14 | const uuid = 'totally_random_uuid'; 15 | 16 | // Need a 2-layer proxyquire now because of the base class dependencies. 17 | const Base = proxyquire('../src/base', { 18 | 'uuid/v4': () => uuid, 19 | }); 20 | 21 | const mainProcessDefault: MainProcessType = proxyquire('../src/mainProcess', { 22 | electron: { ipcMain }, 23 | './base': Base, 24 | }); 25 | 26 | const { PromiseIpc } = mainProcessDefault; 27 | 28 | const generateRoute: { (): string } = (function generateRoute() { 29 | let i = 1; 30 | return () => `${i++}`; // eslint-disable-line no-plusplus 31 | })(); 32 | 33 | describe('mainProcess', () => { 34 | it('exports a default that’s an instance of PromiseIpc', () => { 35 | expect(mainProcessDefault).to.be.an.instanceOf(PromiseIpc); 36 | }); 37 | 38 | describe('on', () => { 39 | let mainProcess: MainProcessType; 40 | let route: string; 41 | 42 | beforeEach(() => { 43 | mainProcess = new PromiseIpc(); 44 | route = generateRoute(); 45 | }); 46 | 47 | afterEach(() => { 48 | ipcMain.removeAllListeners(); 49 | ipcRenderer.removeAllListeners(); 50 | }); 51 | 52 | it('when listener returns resolved promise, sends success + value to the renderer', (done) => { 53 | mainProcess.on(route, () => Promise.resolve('foober')); 54 | ipcRenderer.once( 55 | 'replyChannel', 56 | (event: IpcRendererEvent, status: string, result: string) => { 57 | expect([status, result]).to.eql(['success', 'foober']); 58 | done(); 59 | }, 60 | ); 61 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 62 | }); 63 | 64 | it('overrides the previous listener when one is added on the same route', (done) => { 65 | mainProcess.on(route, () => Promise.resolve('foober')); 66 | mainProcess.on(route, () => Promise.resolve('goober')); 67 | ipcRenderer.once( 68 | 'replyChannel', 69 | (event: IpcRendererEvent, status: string, result: string) => { 70 | expect([status, result]).to.eql(['success', 'goober']); 71 | done(); 72 | }, 73 | ); 74 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 75 | }); 76 | 77 | it('when listener synchronously returns, sends success + value to the renderer', (done) => { 78 | mainProcess.on(route, () => 'foober'); 79 | ipcRenderer.once( 80 | 'replyChannel', 81 | (event: IpcRendererEvent, status: string, result: string) => { 82 | expect([status, result]).to.eql(['success', 'foober']); 83 | done(); 84 | }, 85 | ); 86 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 87 | }); 88 | 89 | it('when listener returns rejected promise, sends failure + error to the renderer', (done) => { 90 | mainProcess.on(route, () => Promise.reject(new Error('foober'))); 91 | ipcRenderer.once('replyChannel', (event: IpcRendererEvent, status: string, result: Error) => { 92 | expect(status).to.eql('failure'); 93 | expect(result.name).to.eql('Error'); 94 | expect(result.message).to.eql('foober'); 95 | done(); 96 | }); 97 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 98 | }); 99 | 100 | it('lets listener reject with a simple string', (done) => { 101 | // eslint-disable-next-line prefer-promise-reject-errors 102 | mainProcess.on(route, () => Promise.reject('goober')); 103 | ipcRenderer.once( 104 | 'replyChannel', 105 | (event: IpcRendererEvent, status: string, result: string) => { 106 | expect([status, result]).to.eql(['failure', 'goober']); 107 | done(); 108 | }, 109 | ); 110 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 111 | }); 112 | 113 | it('lets a listener reject with a function', (done) => { 114 | // eslint-disable-next-line prefer-promise-reject-errors 115 | mainProcess.on(route, () => Promise.reject(() => 'yay!')); 116 | ipcRenderer.once( 117 | 'replyChannel', 118 | (event: IpcRendererEvent, status: string, result: string) => { 119 | expect([status, result]).to.eql(['failure', '[Function: anonymous]']); 120 | done(); 121 | }, 122 | ); 123 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 124 | }); 125 | 126 | it('lets a listener reject with a custom error', (done) => { 127 | mainProcess.on(route, () => { 128 | const custom: Error & { [key: string]: any } = new Error('message'); 129 | custom.obj = { foo: 'bar' }; 130 | custom.array = ['one', 'two']; 131 | custom.func = () => 'yay!'; 132 | custom.self = custom; 133 | return Promise.reject(custom); 134 | }); 135 | ipcRenderer.once( 136 | 'replyChannel', 137 | (event: IpcRendererEvent, status: string, result: Error & { [key: string]: any }) => { 138 | expect(status).to.eql('failure'); 139 | expect(result.message).to.eql('message'); 140 | expect(result.obj).to.eql({ foo: 'bar' }); 141 | expect(result.array).to.eql(['one', 'two']); 142 | expect(result.func).to.eql(undefined); 143 | expect(result.self).to.eql('[Circular]'); 144 | done(); 145 | }, 146 | ); 147 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 148 | }); 149 | 150 | it('when listener throws, sends failure + error to the renderer', (done) => { 151 | mainProcess.on(route, () => { 152 | throw new Error('oh no'); 153 | }); 154 | ipcRenderer.once('replyChannel', (event: IpcRendererEvent, status: string, result: Error) => { 155 | expect(status).to.eql('failure'); 156 | expect(result.name).to.eql('Error'); 157 | expect(result.message).to.eql('oh no'); 158 | done(); 159 | }); 160 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 161 | }); 162 | 163 | it('passes the received data args to the listener', (done) => { 164 | // Return all _data_ args, concatenated, but leave off the event arg. 165 | mainProcess.on(route, (...args) => args.slice(0, -1).join(',')); 166 | ipcRenderer.once( 167 | 'replyChannel', 168 | (event: IpcRendererEvent, status: string, result: string) => { 169 | expect([status, result]).to.eql(['success', 'foo,bar,baz']); 170 | done(); 171 | }, 172 | ); 173 | ipcRenderer.send(route, 'replyChannel', 'foo', 'bar', 'baz'); 174 | }); 175 | 176 | it('passes the event to the listener after data args', (done) => { 177 | mainProcess.on(route, (foo: string, bar: string, baz: string, event: IpcMainEvent) => { 178 | expect([foo, bar, baz]).to.eql(['foo', 'bar', 'baz']); 179 | expect(event.sender.send).to.be.instanceOf(Function); 180 | return null; 181 | }); 182 | ipcRenderer.once( 183 | 'replyChannel', 184 | (event: IpcRendererEvent, status: string, result: string) => { 185 | // If there was an error, then that error will be stored in result. 186 | done(result); 187 | }, 188 | ); 189 | ipcRenderer.send(route, 'replyChannel', 'foo', 'bar', 'baz'); 190 | }); 191 | 192 | it('lets you add the same listener twice and does not break', (done) => { 193 | const cb = () => Promise.resolve('foober'); 194 | mainProcess.on(route, cb); 195 | mainProcess.on(route, cb); 196 | ipcRenderer.once( 197 | 'replyChannel', 198 | (event: IpcRendererEvent, status: string, result: string) => { 199 | expect([status, result]).to.eql(['success', 'foober']); 200 | done(); 201 | }, 202 | ); 203 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 204 | }); 205 | }); 206 | 207 | describe('send', () => { 208 | let mockWebContents: WebContents; 209 | const mainProcess = mainProcessDefault; 210 | before((done) => { 211 | ipcMain.once('saveMockWebContentsSend', (event: IpcMainEvent) => { 212 | mockWebContents = event.sender; 213 | done(); 214 | }); 215 | ipcRenderer.send('saveMockWebContentsSend'); 216 | }); 217 | 218 | it('resolves to sent data on success', () => { 219 | const replyChannel = `route#${uuid}`; 220 | ipcRenderer.once('route', (event: IpcRendererEvent) => { 221 | event.sender.send(replyChannel, 'success', 'result'); 222 | }); 223 | const promise = mainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 224 | return expect(promise).to.eventually.eql('result'); 225 | }); 226 | 227 | it('sends the reply channel any additional arguments', () => { 228 | const replyChannel = `route#${uuid}`; 229 | let argumentsAfterEvent: unknown[]; 230 | ipcRenderer.once('route', (event: IpcRendererEvent, ...rest) => { 231 | argumentsAfterEvent = rest; 232 | event.sender.send(replyChannel, 'success', 'result'); 233 | }); 234 | const promise = mainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 235 | return promise.then(() => { 236 | expect(argumentsAfterEvent).to.eql([replyChannel, 'dataArg1', 'dataArg2']); 237 | }); 238 | }); 239 | 240 | it('rejects with the IPC-passed message on failure', () => { 241 | const replyChannel = `route#${uuid}`; 242 | ipcRenderer.once('route', (event) => { 243 | event.sender.send(replyChannel, 'failure', new Error('an error message')); 244 | }); 245 | const promise = mainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 246 | return expect(promise).to.be.rejectedWith(Error, 'an error message'); 247 | }); 248 | 249 | it('rejects if the IPC passes an unrecognized lifecycle event', () => { 250 | const replyChannel = `route#${uuid}`; 251 | ipcRenderer.once('route', (event: IpcRendererEvent) => { 252 | event.sender.send(replyChannel, 'unrecognized', 'an error message'); 253 | }); 254 | const promise = mainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 255 | return expect(promise).to.be.rejectedWith( 256 | Error, 257 | 'Unexpected IPC call status "unrecognized" in route', 258 | ); 259 | }); 260 | 261 | describe('timeouts', () => { 262 | let clock; 263 | 264 | beforeEach(() => { 265 | clock = lolex.install(); 266 | }); 267 | 268 | afterEach(() => { 269 | clock.uninstall(); 270 | }); 271 | 272 | it('fails if it times out', () => { 273 | const timeoutMainProcess = new PromiseIpc({ maxTimeoutMs: 5000 }); 274 | const makePromise = () => 275 | timeoutMainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 276 | 277 | const p = expect(makePromise()).to.be.rejectedWith(Error, 'route timed out.'); 278 | clock.tick(5001); 279 | return p; 280 | }); 281 | 282 | it('swallows a subsequent resolve if it timed out', () => { 283 | const replyChannel = `route#${uuid}`; 284 | ipcRenderer.once('route', (event: IpcRendererEvent) => { 285 | setTimeout(() => { 286 | event.sender.send(replyChannel, 'success', 'a message'); 287 | }, 6000); 288 | }); 289 | const timeoutMainProcess = new PromiseIpc({ maxTimeoutMs: 5000 }); 290 | const makePromise = () => 291 | timeoutMainProcess.send('route', mockWebContents, 'dataArg1', 'dataArg2'); 292 | const p = expect(makePromise()).to.be.rejectedWith(Error, 'route timed out.'); 293 | clock.tick(5001); 294 | clock.tick(1000); 295 | return p; 296 | }); 297 | }); 298 | }); 299 | 300 | describe('off', () => { 301 | let mainProcess: MainProcessType; 302 | let route: string; 303 | 304 | beforeEach(() => { 305 | mainProcess = new PromiseIpc(); 306 | route = generateRoute(); 307 | }); 308 | 309 | afterEach(() => { 310 | ipcMain.removeAllListeners(); 311 | ipcRenderer.removeAllListeners(); 312 | }); 313 | 314 | it('Does not resolve the promise if .off() was called', (done) => { 315 | const listener = () => Promise.resolve('foober'); 316 | mainProcess.on(route, listener); 317 | ipcRenderer.once('replyChannel', () => { 318 | fail('There should be no reply since ".off()" was called.'); 319 | }); 320 | mainProcess.off(route, listener); 321 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 322 | setTimeout(done, 20); 323 | }); 324 | 325 | it('Allows you to call .off() >1 times with no ill effects', (done) => { 326 | const listener = () => Promise.resolve('foober'); 327 | mainProcess.on(route, listener); 328 | ipcRenderer.once('replyChannel', () => { 329 | fail('There should be no reply since ".off()" was called.'); 330 | }); 331 | mainProcess.off(route, listener); 332 | mainProcess.off(route, listener); 333 | mainProcess.off(route, listener); 334 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 335 | setTimeout(done, 20); 336 | }); 337 | 338 | it('Is aliased to removeListener', (done) => { 339 | const listener = () => Promise.resolve('foober'); 340 | mainProcess.on(route, listener); 341 | ipcRenderer.once('replyChannel', () => { 342 | fail('There should be no reply since ".off()" was called.'); 343 | }); 344 | mainProcess.removeListener(route, listener); 345 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 346 | setTimeout(done, 20); 347 | }); 348 | 349 | it('Does not remove listener for route if called with a different listener', (done) => { 350 | const listener = () => Promise.resolve('foober'); 351 | mainProcess.on(route, listener); 352 | ipcRenderer.once('replyChannel', () => { 353 | done(); // should succeed 354 | }); 355 | mainProcess.removeListener(route, () => {}); 356 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 357 | }); 358 | 359 | it('If called with just route, removes the listener', (done) => { 360 | const listener = () => Promise.resolve('foober'); 361 | mainProcess.on(route, listener); 362 | ipcRenderer.once('replyChannel', () => { 363 | fail('There should be no reply since ".off()" was called.'); 364 | }); 365 | mainProcess.removeListener(route); 366 | ipcRenderer.send(route, 'replyChannel', 'dataArg1'); 367 | setTimeout(done, 20); 368 | }); 369 | }); 370 | }); 371 | -------------------------------------------------------------------------------- /test/renderer-tests.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import lolex from 'lolex'; 4 | import { fail } from 'assert'; 5 | import electronIpcMock from 'electron-ipc-mock'; 6 | import { IpcMainEvent, IpcRendererEvent, WebContents } from 'electron'; 7 | import { RendererProcessType } from '../src/index'; 8 | 9 | const proxyquire: any = require('proxyquire'); // eslint-disable-line 10 | 11 | const { ipcRenderer, ipcMain } = electronIpcMock(); 12 | 13 | chai.use(chaiAsPromised); 14 | const uuid = 'totally_random_uuid'; 15 | 16 | const generateRoute: { (): string } = (function generateRoute() { 17 | let i = 1; 18 | return () => `${i++}`; // eslint-disable-line no-plusplus 19 | })(); 20 | 21 | // Need a 2-layer proxyquire now because of the base class dependencies. 22 | const Base = proxyquire('../src/base', { 23 | 'uuid/v4': () => uuid, 24 | }); 25 | 26 | const renderer: RendererProcessType = proxyquire('../src/renderer', { 27 | electron: { ipcRenderer }, 28 | './base': Base, 29 | }); 30 | 31 | const { PromiseIpc } = renderer; 32 | 33 | describe('renderer', () => { 34 | it('exports a default that’s an instance of PromiseIpc', () => { 35 | expect(renderer).to.be.an.instanceOf(PromiseIpc); 36 | }); 37 | 38 | describe('send', () => { 39 | it('resolves to sent data on success', () => { 40 | const replyChannel = `route#${uuid}`; 41 | ipcMain.once('route', (event: IpcMainEvent) => { 42 | event.sender.send(replyChannel, 'success', 'result'); 43 | }); 44 | const promise = renderer.send('route', 'dataArg1', 'dataArg2'); 45 | return expect(promise).to.eventually.eql('result'); 46 | }); 47 | 48 | it('sends the reply channel and any additional arguments', () => { 49 | const replyChannel = `route#${uuid}`; 50 | let argumentsAfterEvent: string[]; 51 | ipcMain.once('route', (event: IpcMainEvent, ...rest: string[]) => { 52 | argumentsAfterEvent = rest; 53 | event.sender.send(replyChannel, 'success', 'result'); 54 | }); 55 | const promise = renderer.send('route', 'dataArg1', 'dataArg2'); 56 | return promise.then(() => { 57 | expect(argumentsAfterEvent).to.eql([replyChannel, 'dataArg1', 'dataArg2']); 58 | }); 59 | }); 60 | 61 | it('rejects with the IPC-passed message on failure', () => { 62 | const replyChannel = `route#${uuid}`; 63 | ipcMain.once('route', (event: IpcMainEvent) => { 64 | event.sender.send(replyChannel, 'failure', new Error('an error message')); 65 | }); 66 | const promise = renderer.send('route', 'dataArg1', 'dataArg2'); 67 | return expect(promise).to.be.rejectedWith(Error, 'an error message'); 68 | }); 69 | 70 | it('rejects if the IPC passes an unrecognized lifecycle event', () => { 71 | const replyChannel = `route#${uuid}`; 72 | ipcMain.once('route', (event: IpcMainEvent) => { 73 | event.sender.send(replyChannel, 'unrecognized', 'an error message'); 74 | }); 75 | const promise = renderer.send('route', 'dataArg1', 'dataArg2'); 76 | return expect(promise).to.be.rejectedWith( 77 | Error, 78 | 'Unexpected IPC call status "unrecognized" in route', 79 | ); 80 | }); 81 | describe('timeouts', () => { 82 | let clock; 83 | 84 | beforeEach(() => { 85 | clock = lolex.install(); 86 | }); 87 | 88 | afterEach(() => { 89 | clock.uninstall(); 90 | }); 91 | 92 | it('fails if it times out', () => { 93 | const timeoutRenderer = new PromiseIpc({ maxTimeoutMs: 5000 }); 94 | const makePromise = () => timeoutRenderer.send('route', 'dataArg1', 'dataArg2'); 95 | const p = expect(makePromise()).to.be.rejectedWith(Error, 'route timed out.'); 96 | clock.tick(5001); 97 | return p; 98 | }); 99 | 100 | it('swallows a subsequent resolve if it timed out', () => { 101 | const replyChannel = `route#${uuid}`; 102 | ipcMain.once('route', (event: IpcMainEvent) => { 103 | setTimeout(() => { 104 | event.sender.send(replyChannel, 'success', 'a message'); 105 | }, 6000); 106 | }); 107 | const timeoutRenderer = new PromiseIpc({ maxTimeoutMs: 5000 }); 108 | const makePromise = () => timeoutRenderer.send('route', 'dataArg1', 'dataArg2'); 109 | const p = expect(makePromise()).to.be.rejectedWith(Error, 'route timed out.'); 110 | clock.tick(5001); 111 | clock.tick(1000); 112 | return p; 113 | }); 114 | }); 115 | }); 116 | 117 | describe('on', () => { 118 | let route: string; 119 | let mockWebContents: WebContents; 120 | before((done) => { 121 | ipcMain.once('saveMockWebContentsSend', (event: IpcMainEvent) => { 122 | mockWebContents = event.sender; 123 | done(); 124 | }); 125 | ipcRenderer.send('saveMockWebContentsSend'); 126 | }); 127 | 128 | beforeEach(() => { 129 | route = generateRoute(); 130 | }); 131 | 132 | afterEach(() => { 133 | ipcMain.removeAllListeners(); 134 | ipcRenderer.removeAllListeners(); 135 | }); 136 | 137 | it('when listener returns resolved promise, sends success + value to the main process', (done) => { 138 | renderer.on(route, () => Promise.resolve('foober')); 139 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 140 | expect([status, result]).to.eql(['success', 'foober']); 141 | done(); 142 | }); 143 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 144 | }); 145 | 146 | it('overrides the previous listener when one is added on the same route', (done) => { 147 | renderer.on(route, () => Promise.resolve('foober')); 148 | renderer.on(route, () => Promise.resolve('goober')); 149 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 150 | expect([status, result]).to.eql(['success', 'goober']); 151 | done(); 152 | }); 153 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 154 | }); 155 | 156 | it('when listener synchronously returns, sends success + value to the main process', (done) => { 157 | renderer.on(route, () => Promise.resolve('foober')); 158 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 159 | expect([status, result]).to.eql(['success', 'foober']); 160 | done(); 161 | }); 162 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 163 | }); 164 | 165 | it('when listener returns rejected promise, sends failure + error to the main process', (done) => { 166 | renderer.on(route, () => Promise.reject(new Error('foober'))); 167 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: Error) => { 168 | expect(status).to.eql('failure'); 169 | expect(result.name).to.eql('Error'); 170 | expect(result.message).to.eql('foober'); 171 | done(); 172 | }); 173 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 174 | }); 175 | 176 | it('lets a listener reject with a simple string', (done) => { 177 | // eslint-disable-next-line prefer-promise-reject-errors 178 | renderer.on(route, () => Promise.reject('goober')); 179 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: Error) => { 180 | expect([status, result]).to.eql(['failure', 'goober']); 181 | done(); 182 | }); 183 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 184 | }); 185 | 186 | it('lets a listener reject with a function', (done) => { 187 | // eslint-disable-next-line prefer-promise-reject-errors 188 | renderer.on(route, () => Promise.reject(() => 'yay!')); 189 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 190 | expect([status, result]).to.eql(['failure', '[Function: anonymous]']); 191 | done(); 192 | }); 193 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 194 | }); 195 | 196 | it('lets a listener reject with a custom error', (done) => { 197 | renderer.on(route, () => { 198 | const custom: Error & { [key: string]: any } = new Error('message'); 199 | custom.obj = { foo: 'bar' }; 200 | custom.array = ['one', 'two']; 201 | custom.func = () => 'yay!'; 202 | custom.self = custom; 203 | return Promise.reject(custom); 204 | }); 205 | ipcMain.once( 206 | 'replyChannel', 207 | (event: IpcMainEvent, status: string, result: Error & { [key: string]: any }) => { 208 | expect(status).to.eql('failure'); 209 | expect(result.message).to.eql('message'); 210 | expect(result.obj).to.eql({ foo: 'bar' }); 211 | expect(result.array).to.eql(['one', 'two']); 212 | expect(result.func).to.eql(undefined); 213 | expect(result.self).to.eql('[Circular]'); 214 | done(); 215 | }, 216 | ); 217 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 218 | }); 219 | 220 | it('when listener throws, sends failure + error to the main process', (done) => { 221 | renderer.on(route, () => { 222 | throw new Error('oh no'); 223 | }); 224 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: Error) => { 225 | expect(status).to.eql('failure'); 226 | expect(result.name).to.eql('Error'); 227 | expect(result.message).to.eql('oh no'); 228 | done(); 229 | }); 230 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 231 | }); 232 | 233 | it('passes the received data args to the listener', (done) => { 234 | // Return all _data_ args, concatenated, but leave off the event arg. 235 | renderer.on(route, (...args) => args.slice(0, -1).join(',')); 236 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 237 | expect([status, result]).to.eql(['success', 'foo,bar,baz']); 238 | done(); 239 | }); 240 | mockWebContents.send(route, 'replyChannel', 'foo', 'bar', 'baz'); 241 | }); 242 | 243 | it('passes the event to the listener after data args', (done) => { 244 | renderer.on(route, (foo: string, bar: string, baz: string, event: IpcRendererEvent) => { 245 | expect([foo, bar, baz]).to.eql(['foo', 'bar', 'baz']); 246 | expect(event.sender.send).to.be.instanceOf(Function); 247 | return null; 248 | }); 249 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 250 | // If there was an error, then that error will be stored in result. 251 | done(result); 252 | }); 253 | mockWebContents.send(route, 'replyChannel', 'foo', 'bar', 'baz'); 254 | }); 255 | 256 | it('lets you add the same listener twice and does not break', (done) => { 257 | const cb = () => Promise.resolve('foober'); 258 | renderer.on(route, cb); 259 | renderer.on(route, cb); 260 | ipcMain.once('replyChannel', (event: IpcMainEvent, status: string, result: string) => { 261 | expect([status, result]).to.eql(['success', 'foober']); 262 | done(); 263 | }); 264 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 265 | }); 266 | }); 267 | 268 | describe('off', () => { 269 | let route: string; 270 | let mockWebContents: WebContents; 271 | before((done) => { 272 | ipcMain.once('saveMockWebContentsSend', (event: IpcMainEvent) => { 273 | mockWebContents = event.sender; 274 | done(); 275 | }); 276 | ipcRenderer.send('saveMockWebContentsSend'); 277 | }); 278 | 279 | beforeEach(() => { 280 | route = generateRoute(); 281 | }); 282 | 283 | afterEach(() => { 284 | ipcMain.removeAllListeners(); 285 | ipcRenderer.removeAllListeners(); 286 | }); 287 | 288 | it('Does not resolve the promise if .off() was called', (done) => { 289 | const listener = () => Promise.resolve('foober'); 290 | renderer.on(route, listener); 291 | ipcMain.once('replyChannel', () => { 292 | fail('There should be no reply since ".off()" was called.'); 293 | }); 294 | renderer.off(route, listener); 295 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 296 | setTimeout(done, 20); 297 | }); 298 | 299 | it('Allows you to call .off() >1 times with no ill effects', (done) => { 300 | const listener = () => Promise.resolve('foober'); 301 | renderer.on(route, listener); 302 | ipcMain.once('replyChannel', () => { 303 | fail('There should be no reply since ".off()" was called.'); 304 | }); 305 | renderer.off(route, listener); 306 | renderer.off(route, listener); 307 | renderer.off(route, listener); 308 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 309 | setTimeout(done, 20); 310 | }); 311 | 312 | it('Is aliased to removeListener', (done) => { 313 | const listener = () => Promise.resolve('foober'); 314 | renderer.on(route, listener); 315 | ipcMain.once('replyChannel', () => { 316 | fail('There should be no reply since ".off()" was called.'); 317 | }); 318 | renderer.removeListener(route, listener); 319 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 320 | setTimeout(done, 20); 321 | }); 322 | 323 | it('Does not remove listener for route if called with a different listener', (done) => { 324 | const listener = () => Promise.resolve('foober'); 325 | renderer.on(route, listener); 326 | ipcMain.once('replyChannel', () => { 327 | done(); // should succeed 328 | }); 329 | renderer.removeListener(route, () => {}); 330 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 331 | }); 332 | 333 | it('If called with just route, removes the listener', (done) => { 334 | const listener = () => Promise.resolve('foober'); 335 | renderer.on(route, listener); 336 | ipcMain.once('replyChannel', () => { 337 | fail('There should be no reply since ".off()" was called.'); 338 | }); 339 | renderer.removeListener(route); 340 | mockWebContents.send(route, 'replyChannel', 'dataArg1'); 341 | setTimeout(done, 20); 342 | }); 343 | }); 344 | }); 345 | -------------------------------------------------------------------------------- /test/require-tests.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); // eslint-disable-line 2 | // we are main process by default 3 | const defaultExport = require('../build/index'); // eslint-disable-line 4 | 5 | const { PromiseIpc, PromiseIpcMain } = defaultExport; 6 | 7 | describe('requiring the built module', () => { 8 | it('sets PromiseIpcMain function property on the export and PromiseIpc as an alias', () => { 9 | expect(typeof PromiseIpc).to.eql('function'); 10 | expect(typeof PromiseIpcMain).to.eql('function'); 11 | expect(PromiseIpc).to.eql(PromiseIpcMain); 12 | }); 13 | 14 | it('exports an instance of PromiseIpcMain', () => { 15 | expect(defaultExport instanceof PromiseIpcMain).to.eql(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | // "compilerOptions": { 4 | // "outDir": "./built", 5 | // "allowJs": true, 6 | // "target": "es6" 7 | // }, 8 | "include": ["./src/**/*"], 9 | "compilerOptions": { 10 | /* Basic Options */ 11 | "target": "ES3" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 12 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 13 | // "lib": [], /* Specify library files to be included in the compilation. */ 14 | // "allowJs": true /* Allow javascript files to be compiled. */, 15 | // "checkJs": true /* Report errors in .js files. */, 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 17 | "declaration": true /* Generates corresponding '.d.ts' file. */, 18 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 19 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 20 | // "outFile": "./", /* Concatenate and emit output to single file. */ 21 | "outDir": "./build" /* Redirect output structure to the directory. */, 22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "composite": true, /* Enable project compilation */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | // "strict": true /* Enable all strict type-checking options. */, 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | } 66 | } 67 | --------------------------------------------------------------------------------