Automated Testing

    From ChromeDriver - WebDriver for Chrome:

    There are a few ways that you can set up testing using WebDriver.

    (WDIO) is a test automation framework that provides a Node.js package for testing with WebDriver. Its ecosystem also includes various plugins (e.g. reporter and services) that can help you put together your test setup.

    Install the testrunner

    First you need to run the WebdriverIO starter toolkit in your project root directory:

    • npm
    • Yarn

    This installs all necessary packages for you and generates a wdio.conf.js configuration file.

    Connect WDIO to your Electron app

    Update the capabilities in your configuration file to point to your Electron app binary:

    wdio.conf.js

    1. export.config = {
    2. // ...
    3. capabilities: [{
    4. browserName: 'chrome',
    5. 'goog:chromeOptions': {
    6. binary: '/path/to/your/electron/binary', // Path to your Electron binary.
    7. args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/
    8. }
    9. }]
    10. // ...
    11. }

    Run your tests

    1. $ npx wdio run wdio.conf.js

    With Selenium

    Selenium is a web automation framework that exposes bindings to WebDriver APIs in many languages. Their Node.js bindings are available under the selenium-webdriver package on NPM.

    Run a ChromeDriver server

    In order to use Selenium with Electron, you need to download the electron-chromedriver binary, and run it:

    • npm
    • Yarn
    1. yarn add --dev electron-chromedriver
    2. ./node_modules/.bin/chromedriver
    3. Starting ChromeDriver (v2.10.291558) on port 9515
    4. Only local connections are allowed.

    Remember the port number 9515, which will be used later.

    Connect Selenium to ChromeDriver

    Next, install Selenium into your project:

    • npm
    • Yarn
    1. npm install --save-dev selenium-webdriver
    1. yarn add --dev selenium-webdriver

    Usage of selenium-webdriver with Electron is the same as with normal websites, except that you have to manually specify how to connect ChromeDriver and where to find the binary of your Electron app:

    test.js

    Using a custom test driver

    It’s also possible to write your own custom driver using Node.js’ built-in IPC-over-STDIO. Custom test drivers require you to write additional app code, but have lower overhead and let you expose custom methods to your test suite.

    To create a custom driver, we’ll use Node.js’ child_process API. The test suite will spawn the Electron process, then establish a simple messaging protocol:

    1. const childProcess = require('child_process')
    2. const electronPath = require('electron')
    3. const env = { /* ... */ }
    4. const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
    5. const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })
    6. // listen for IPC messages from the app
    7. appProcess.on('message', (msg) => {
    8. // ...
    9. })
    10. // send an IPC message to the app
    11. appProcess.send({ my: 'message' })

    From within the Electron app, you can listen for messages and send replies using the Node.js API:

    main.js

    1. // listen for messages from the test suite
    2. process.on('message', (msg) => {
    3. // ...
    4. })
    5. // send a message to the test suite
    6. process.send({ my: 'message' })

    We can now communicate from the test suite to the Electron app using the appProcess object.

    For convenience, you may want to wrap appProcess in a driver object that provides more high-level functions. Here is an example of how you can do this. Let’s start by creating a TestDriver class:

    testDriver.js

    1. class TestDriver {
    2. constructor ({ path, args, env }) {
    3. this.rpcCalls = []
    4. // start child process
    5. env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages
    6. this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
    7. // handle rpc responses
    8. this.process.on('message', (message) => {
    9. // pop the handler
    10. const rpcCall = this.rpcCalls[message.msgId]
    11. if (!rpcCall) return
    12. this.rpcCalls[message.msgId] = null
    13. // reject/resolve
    14. if (message.reject) rpcCall.reject(message.reject)
    15. else rpcCall.resolve(message.resolve)
    16. })
    17. this.isReady = this.rpc('isReady').catch((err) => {
    18. console.error('Application failed to start', err)
    19. this.stop()
    20. })
    21. }
    22. // simple RPC call
    23. // to use: driver.rpc('method', 1, 2, 3).then(...)
    24. async rpc (cmd, ...args) {
    25. // send rpc request
    26. const msgId = this.rpcCalls.length
    27. this.process.send({ msgId, cmd, args })
    28. return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
    29. }
    30. stop () {
    31. this.process.kill()
    32. }
    33. }
    34. module.exports = { TestDriver };

    In your app code, can then write a simple handler to receive RPC calls:

    main.js

    Then, in your test suite, you can use your TestDriver class with the test automation framework of your choosing. The following example uses ava, but other popular choices like Jest or Mocha would work as well:

    1. const test = require('ava')
    2. const electronPath = require('electron')
    3. const { TestDriver } = require('./testDriver')
    4. const app = new TestDriver({
    5. path: electronPath,
    6. args: ['./app'],
    7. env: {
    8. NODE_ENV: 'test'
    9. }
    10. })
    11. test.before(async t => {
    12. await app.isReady
    13. })
    14. test.after.always('cleanup', async t => {