Developing Apps with Electron and Spectron (Part 3): Making The First Test Pass

In part two of this series, a failing test was written which asserted that the app was running and that the main app window was visible.

Of course, it failed because no app exists yet.

In this post, a basic Electron app will be created and the test will then pass.

Creating an Electron App

This app is based heavily upon the sample application detailed in the quick-start section of the Electron website. There is one additional, and very important caveat, that is required to make Spectron function correctly and this is also detailed below.

Setup

First, edit the main.js file and require the relevant dependencies:

const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')

Then, create a global reference to the window object:

let win

Creating the Window

Now, create a createWindow function:

function createWindow() {

    ...

}

Into it instantiate a new window object:

    win = new BrowserWindow({
        width: 800,
        height: 600
    })

Tell the window to load the markup contained in the index.html file:

    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

And add a method which will dereference the window object when the app is closed:

    win.on('closed', () => {
        win = null
    })

In full, the createWindow function should look like this:

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600
    })

    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

    win.on('closed', () => {
        win = null
    })
}

Important Caveat

In the example app from the Electron website, the createWindow function also includes the line:

win.webContents.openDevTools()

which opens the dev tools on app startup.

It is ESSENTIAL that this line is not included in your app because it will cause Spectron to crash.

App Ready

After the createWindow function declaration, it is now time to actually open the window by listening for the app.on('ready') event:

app.on('ready', createWindow)

And close things down when the window is closed by listening for the app.on('close') event:

app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

Using a Mac

Finally, the example gives a Mac-specific option:

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow()
    }
})

although this hasn’t been necessary for me, as I am developing on a machine running Linux.

The whole file

If you’ve been following along, your file should now look like this:

const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')

let win

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600
    })

    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

    win.on('closed', () => {
        win = null
    })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow()
    }
})

Create the Markup

The very last step is to open the index.html file and create the markup for the app window:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
</head>

<body>
    <h1>Hello World!</h1>
    We are using node
    <script>document.write(process.versions.node)</script>, Chrome
    <script>document.write(process.versions.chrome)</script>, and Electron
    <script>document.write(process.versions.electron)</script>.
</body>

</html>

Running the Test

Now, when the test is run, it should pass:

node test/sample-test.js

This time, we should receive no hanging or message and this indicates that the test has passed.

What Next?

Now we have a (very) basic app that essentially does nothing. However, what we have achieved is the ability to write tests for an Electron app and then use them to drive its development.

The next step will be a refinement. Spectron is simply a library that allows the tests to interact with an electron app. Up until now the example has used the build in Node assertion library but, as you saw when you ran the above test, this doesn’t offer much feedback.

In part four we will be installing Mocha into out project and using that to replicate what we have done so far but with the power of a full testing framework.