UI testing with Nightmare
Nightmare is a browser automation library for node.js, designed to be much simpler and easier to use than Phantomjs. We originally built Nightmare to create integration logos with 99Designs Tasks before they had an API, and we still use it in Sherlock. But the vast majority of Nightmare developers—now 55k+ downloads per month—use it for web UI testing and crawling.
This article is a quick introduction to using Nightmare for web UI testing. It uses Mocha as the testing framework, but you could similarly use Jest.
Nightmare’s API methods are designed to mimic real user actions:
.goto(url)
.type(elementSelector, text)
.click(elementSelector)
This makes testing with Nightmare very similar to how a human tester would navigate, click and type into your actual web app. In the next few sections we’ll dive into how to set your repo, then how to test page loads, submitting forms, and interacting with an app.
First we need to install mocha
and nightmare
, and make sure our basic test harness is working.
Starting on the command line in your repo folder…
In test/test.js
you can get started with:
const Nightmare = require('nightmare') const assert = require('assert')
describe('Load a Page', function() { // Recommended: 5s locally, 10s to remote server, 30s from airplane ¯_(ツ)_/¯ this.timeout('30s')
let nightmare = null beforeEach(() => { nightmare = new Nightmare() })
describe('/ (Home Page)', () => { it('should load without error', done => { // your actual testing urls will likely be `http://localhost:port/path` nightmare.goto('https://gethoodie.com') .end() .then(function (result) { done() }) .catch(done) }) }) })
Add mocha as the test script to your package.json
:
"scripts": { "test": "mocha" }
Finally, to test this complete setup you can run npm test
on the command line…
npm test > Load a Page > ✓ should load a web page (12223ms) > 1 passing (12s)
Most web products have a set of public pages used for documentation, support, marketing, authentication and signup. Here’s how you can test that these pages load successfully:
describe('Public Pages', function() { // Recommended: 5s locally, 10s to remote server, 30s from airplane ¯_(ツ)_/¯ this.timeout('30s')
let nightmare = null beforeEach(() => { nightmare = new Nightmare() })
describe('/ (Home Page)', () => { it('should load without error', done => { // your actual testing urls will likely be `http://localhost:port/path` nightmare.goto('https://gethoodie.com') .end() .then(function (result) { done() }) .catch(done) }) })
describe('/auth (Login Page)', () => { it('should load without error', done => { nightmare.goto('https://gethoodie.com/auth') .end() .then(result => { done() }) .catch(done) }) }) })
This example tests that Hoodie’s login function fails with bad credentials. It’s always worth testing failed states as well as successful states. 🤖
describe('Login Page', function () { this.timeout('30s')
let nightmare = null beforeEach(() => { // show true lets you see wth is actually happening :) nightmare = new Nightmare({ show: true }) })
describe('given bad data', () => { it('should fail', done => { nightmare .goto('https://gethoodie.com/auth') .on('page', (type, message) => { if (type == 'alert') done() }) .type('.login-email-input', 'notgonnawork') .type('.login-password-input', 'invalid password') .click('.login-submit') .wait(2000) .end() .then() .catch(done) }) }) })
This example is more involved, and includes signing up with text fields, select fields, and clicking and waiting through a flow that spans multiple pages.
describe('Using the App', function () { this.timeout('60s')
let nightmare = null beforeEach(() => { // show true lets you see wth is actually happening :) nightmare = new Nightmare({ show: true }) })
describe('signing up and finishing setup', () => { it('should work without timing out', done => { nightmare .goto('https://gethoodie.com/auth') .type('.signup-email-input', 't'+Math.round(Math.random()*100000)+'@test.com') .type('.signup-password-input', 'valid password') .type('.signup-password-confirm-input', 'valid password') .click('.signup-submit') .wait(2000) .select('.sizes-jeans-select', '30W x 30L') .select('.sizes-shoes-select', '9.5') .click('.sizes-submit') .wait('.shipit') // this selector only appears on the catalog page .end() .then(result => { done() }) .catch(done) }) }) })
The final example ties all these together into a cleanly formatted test/test.js
:
const Nightmare = require('nightmare') const assert = require('assert')
describe('UI Flow Tests', function() { this.timeout('60s')
let nightmare = null beforeEach(() => { nightmare = new Nightmare({ show: true }) })
describe('Public Pages', function() { describe('/ (Home Page)', () => { it('should load without error', done => { // your actual testing urls will likely be `http://localhost:port/path` nightmare.goto('https://gethoodie.com') .end() .then(function (result) { done() }) .catch(done) }) }) describe('/auth (Login Page)', () => { it('should load without error', done => { nightmare.goto('https://gethoodie.com/auth') .end() .then(result => { done() }) .catch(done) }) }) })
describe('Login Page', function () { describe('given bad data', () => { it('should fail', done => { nightmare .goto('https://gethoodie.com/auth') .on('page', (type, message) => { if (type == 'alert') done() }) .type('.login-email-input', 'notgonnawork') .type('.login-password-input', 'invalid password') .click('.login-submit') .wait(2000) .end() .then() .catch(done) }) }) })
describe('Using the App', function () { describe('signing up and finishing setup', () => { it('should work without timing out', done => { nightmare .goto('https://gethoodie.com/auth') .type('.signup-email-input', 'test+'+Math.round(Math.random()*1000000)+'@test.com') .type('.signup-password-input', 'valid password') .type('.signup-password-confirm-input', 'valid password') .click('.signup-submit') .wait(2000) .select('.sizes-jeans-select', '30W x 30L') .select('.sizes-shoes-select', '9.5') .click('.sizes-submit') .wait('.shipit') // this selector only appears on the catalog page .end() .then(result => { done() }) .catch(done) }) }) }) })
If you have additional questions or want to join the 90+ people who have contributed to Nightmare, head over to the Github repo. Happy testing.
Our annual look at how attitudes, preferences, and experiences with personalization have evolved over the past year.