spectron
Version:
Easily test your Electron apps using ChromeDriver and WebdriverIO.
522 lines (389 loc) • 15.7 kB
Markdown
# <img src="https://cloud.githubusercontent.com/assets/378023/15063284/cf544f2c-1383-11e6-9336-e13bd64b1694.png" width="60px" align="center" alt="Spectron icon"> Spectron
[](https://travis-ci.org/electron/spectron)
[](https://ci.appveyor.com/project/kevinsawicki/spectron/branch/master)
<br>
[](http://standardjs.com/)
[](https://david-dm.org/electron/spectron)
<br>
[](https://opensource.org/licenses/MIT)
[](https://www.npmjs.com/packages/spectron)
[](https://www.npmjs.com/packages/spectron)
Easily test your [Electron](http://electron.atom.io) apps using
[ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver) and
[WebdriverIO](http://webdriver.io).
This minor version of this library tracks the minor version of the Electron
versions released. So if you are using Electron `1.0.x` you would want to use
a `spectron` dependency of `~3.0.0` in your `package.json` file.
Learn more from [this presentation](https://speakerdeck.com/kevinsawicki/testing-your-electron-apps-with-chromedriver).
:rotating_light: Upgrading from `1.x` to `2.x`/`3.x`? Read the [changelog](https://github.com/electron/spectron/blob/master/CHANGELOG.md).
## Using
```sh
npm install --save-dev spectron
```
Spectron works with any testing framework but the following example uses
[mocha](https://mochajs.org):
```js
var Application = require('spectron').Application
var assert = require('assert')
describe('application launch', function () {
this.timeout(10000)
beforeEach(function () {
this.app = new Application({
path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
})
return this.app.start()
})
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop()
}
})
it('shows an initial window', function () {
return this.app.client.getWindowCount().then(function (count) {
assert.equal(count, 1)
})
})
})
```
## Application API
Spectron exports an `Application` class that when configured, can start and
stop your Electron application.
### new Application(options)
Create a new application with the following options:
* `path` - String path to the application executable to launch. **Required**
* `args` - Array of arguments to pass to the executable.
See [here](https://sites.google.com/a/chromium.org/chromedriver/capabilities)
for details on the Chrome arguments.
* `cwd`- String path to the working directory to use for the launched
application. Defaults to `process.cwd()`.
* `env` - Object of additional environment variables to set in the launched
application.
* `host` - String host name of the launched `chromedriver` process.
Defaults to `'localhost'`.
* `port` - Number port of the launched `chromedriver` process.
Defaults to `9515`.
* `nodePath` - String path to a `node` executable to launch ChromeDriver with.
Defaults to `process.execPath`.
* `connectionRetryCount` - Number of retry attempts to make when connecting
to ChromeDriver. Defaults to `10` attempts.
* `connectionRetryTimeout` - Number in milliseconds to wait for connections
to ChromeDriver to be made. Defaults to `30000` milliseconds.
* `quitTimeout` - Number in milliseconds to wait for application quitting.
Defaults to `1000` milliseconds.
* `requireName` - Custom property name to use when requiring modules. Defaults
to `require`. This should only be used if your application deletes the main
`window.require` function and assigns it to another property name on `window`.
* `startTimeout` - Number in milliseconds to wait for ChromeDriver to start.
Defaults to `5000` milliseconds.
* `waitTimeout` - Number in milliseconds to wait for calls like
`waitUntilTextExists` and `waitUntilWindowLoaded` to complete.
Defaults to `5000` milliseconds.
* `debuggerAddress` - String address of a Chrome debugger server to connect to.
* `chromeDriverLogPath` - String path to file to store ChromeDriver logs in.
Setting this option enables `--verbose` logging when starting ChromeDriver.
### Node Integration
The Electron helpers provided by Spectron require accessing the core Electron
APIs in the renderer processes of your application. So if your Electron
application has `nodeIntegration` set to `false` then you'll need to expose a
`require` window global to Spectron so it can access the core Electron APIs.
You can do this by adding a [`preload`][preload] script that does the following:
```js
if (process.env.NODE_ENV === 'test') {
window.electronRequire = require
}
```
Then create the Spectron `Application` with the `requireName` option set to
`'electronRequire'` and then runs your tests via `NODE_ENV=test npm test`.
**Note:** This is only required if you tests are accessing any Electron APIs.
You don't need to do this if you are only accessing the helpers on the `client`
property which do not require Node integration.
### Properties
#### client
Spectron uses [WebdriverIO](http://webdriver.io) and exposes the managed
`client` property on the created `Application` instances.
The full `client` API provided by WebdriverIO can be found
[here](http://webdriver.io/api.html).
Several additional commands are provided specific to Electron.
All the commands return a `Promise`.
So if you wanted to get the text of an element you would do:
```js
app.client.getText('#error-alert').then(function (errorText) {
console.log('The #error-alert text content is ' + errorText)
})
```
#### electron
The `electron` property is your gateway to accessing the full Electron API.
Each Electron module is exposed as a property on the `electron` property
so you can think of it as an alias for `require('electron')` from within your
app.
So if you wanted to access the [clipboard](http://electron.atom.io/docs/latest/api/clipboard)
API in your tests you would do:
```js
app.electron.clipboard.writeText('pasta')
.electron.clipboard.readText().then(function (clipboardText) {
console.log('The clipboard text is ' + clipboardText)
})
```
#### browserWindow
The `browserWindow` property is an alias for `require('electron').remote.getCurrentWindow()`.
It provides you access to the current [BrowserWindow](http://electron.atom.io/docs/latest/api/browser-window/)
and contains all the APIs.
So if you wanted to check if the current window is visible in your tests you
would do:
```js
app.browserWindow.isVisible().then(function (visible) {
console.log('window is visible? ' + visible)
})
```
It is named `browserWindow` instead of `window` so that it doesn't collide
with the WebDriver command of that name.
##### capturePage
The async `capturePage` API is supported but instead of taking a callback it
returns a `Promise` that resolves to a `Buffer` that is the image data of
screenshot.
```js
app.browserWindow.capturePage().then(function (imageBuffer) {
fs.writeFile('page.png', imageBuffer)
})
```
#### webContents
The `webContents` property is an alias for `require('electron').remote.getCurrentWebContents()`.
It provides you access to the [WebContents](http://electron.atom.io/docs/latest/api/web-contents/)
for the current window and contains all the APIs.
So if you wanted to check if the current window is loading in your tests you
would do:
```js
app.webContents.isLoading().then(function (visible) {
console.log('window is loading? ' + visible)
})
```
##### savePage
The async `savePage` API is supported but instead of taking a callback it
returns a `Promise` that will raise any errors and resolve to `undefined` when
complete.
```js
app.webContents.savePage('/Users/kevin/page.html', 'HTMLComplete')
.then(function () {
console.log('page saved')
}).catch(function (error) {
console.error('saving page failed', error.message)
})
```
#### mainProcess
The `mainProcess` property is an alias for `require('electron').remote.process`.
It provides you access to the main process's [process](https://nodejs.org/api/process.html)
global.
So if you wanted to get the `argv` for the main process in your tests you would
do:
```js
app.mainProcess.argv().then(function (argv) {
console.log('main process args: ' + argv)
})
```
Properties on the `process` are exposed as functions that return promises so
make sure to call `mainProcess.env().then(...)` instead of
`mainProcess.env.then(...)`.
#### rendererProcess
The `rendererProcess` property is an alias for `global.process`.
It provides you access to the renderer process's [process](https://nodejs.org/api/process.html)
global.
So if you wanted to get the environment variables for the renderer process in
your tests you would do:
```js
app.rendererProcess.env().then(function (env) {
console.log('main process args: ' + env)
})
```
### Methods
#### start()
Starts the application. Returns a `Promise` that will be resolved when the
application is ready to use. You should always wait for start to complete
before running any commands.
#### stop()
Stops the application. Returns a `Promise` that will be resolved once the
application has stopped.
#### restart()
Stops the application and then starts it. Returns a `Promise` that will be
resolved once the application has started again.
#### client.getMainProcessLogs()
Gets the `console` log output from the main process. The logs are cleared
after they are returned.
Returns a `Promise` that resolves to an array of string log messages
```js
app.client.getMainProcessLogs().then(function (logs) {
logs.forEach(function (log) {
console.log(log)
})
})
```
#### client.getRenderProcessLogs()
Gets the `console` log output from the render process. The logs are cleared
after they are returned.
Returns a `Promise` that resolves to an array of log objects.
```js
app.client.getRenderProcessLogs().then(function (logs) {
logs.forEach(function (log) {
console.log(log.message)
console.log(log.source)
console.log(log.level)
})
})
```
#### client.getSelectedText()
Get the selected text in the current window.
```js
app.client.getSelectedText().then(function (selectedText) {
console.log(selectedText)
})
```
#### client.getWindowCount()
Gets the number of open windows.
`<webview>` tags are also counted as separate windows.
```js
app.client.getWindowCount().then(function (count) {
console.log(count)
})
```
#### client.waitUntilTextExists(selector, text, [timeout])
Waits until the element matching the given selector contains the given
text. Takes an optional timeout in milliseconds that defaults to `5000`.
```js
app.client.waitUntilTextExists('#message', 'Success', 10000)
```
#### client.waitUntilWindowLoaded([timeout])
Wait until the window is no longer loading. Takes an optional timeout
in milliseconds that defaults to `5000`.
```js
app.client.waitUntilWindowLoaded(10000)
```
#### client.windowByIndex(index)
Focus a window using its index from the `windowHandles()` array.
`<webview>` tags can also be focused as a separate window.
```js
app.client.windowByIndex(1)
```
## Continuous Integration
### On Travis CI
You will want to add the following to your `.travis.yml` file when building on
Linux:
```yml
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3 # give xvfb some time to start
```
Check out Spectron's [.travis.yml](https://github.com/electron/spectron/blob/master/.travis.yml)
file for a production example.
### On AppVeyor
You will want to add the following to your `appveyor.yml` file:
```yml
os: unstable
```
Check out Spectron's [appveyor.yml](https://github.com/electron/spectron/blob/master/appveyor.yml)
file for a production example.
## Test Library Examples
### With Chai As Promised
WebdriverIO is promise-based and so it pairs really well with the
[Chai as Promised](https://github.com/domenic/chai-as-promised) library that
builds on top of [Chai](http://chaijs.com).
Using these together allows you to chain assertions together and have fewer
callback blocks. See below for a simple example:
```sh
npm install --save-dev chai
npm install --save-dev chai-as-promised
```
```js
var Application = require('spectron').Application
var chai = require('chai')
var chaiAsPromised = require('chai-as-promised')
var path = require('path')
chai.should()
chai.use(chaiAsPromised)
describe('application launch', function () {
beforeEach(function () {
this.app = new Application({
path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
})
return this.app.start()
})
beforeEach(function () {
chaiAsPromised.transferPromiseness = this.app.transferPromiseness
})
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop()
}
})
it('opens a window', function () {
return this.app.client.waitUntilWindowLoaded()
.getWindowCount().should.eventually.equal(1)
.browserWindow.isMinimized().should.eventually.be.false
.browserWindow.isDevToolsOpened().should.eventually.be.false
.browserWindow.isVisible().should.eventually.be.true
.browserWindow.isFocused().should.eventually.be.true
.browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0)
.browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0)
})
})
```
### With AVA
Spectron works with [AVA](https://github.com/avajs/ava), which allows you
to write your tests in ES2015+ without doing any extra work.
```js
import test from 'ava';
import {Application} from 'spectron';
test.beforeEach(t => {
t.context.app = new Application({
path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
});
return t.context.app.start();
});
test.afterEach(t => {
return t.context.app.stop();
});
test(t => {
return t.context.app.client.waitUntilWindowLoaded()
.getWindowCount().then(count => {
t.is(count, 1);
}).browserWindow.isMinimized().then(min => {
t.false(min);
}).browserWindow.isDevToolsOpened().then(opened => {
t.false(opened);
}).browserWindow.isVisible().then(visible => {
t.true(visible);
}).browserWindow.isFocused().then(focused => {
t.true(focused);
}).browserWindow.getBounds().then(bounds => {
t.true(bounds.width > 0);
t.true(bounds.height > 0);
});
});
```
AVA has built-in support for [async functions](https://github.com/avajs/ava#async-function-support), which simplifies async operations:
```js
import test from 'ava';
import {Application} from 'spectron';
test.beforeEach(async t => {
t.context.app = new Application({
path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
});
await t.context.app.start();
});
test.afterEach.always(async t => {
await t.context.app.stop();
});
test(async t => {
const app = t.context.app;
await app.client.waitUntilWindowLoaded();
const win = app.browserWindow;
t.is(await app.client.getWindowCount(), 1);
t.false(await win.isMinimized());
t.false(await win.isDevToolsOpened());
t.true(await win.isVisible());
t.true(await win.isFocused());
const {width, height} = await win.getBounds();
t.true(width > 0);
t.true(height > 0);
});
```
[preload]: http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions