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.

Overview

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.

Repo Setup

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…

## For UI testing you only need nightmare as a development dependency
npm install --save-dev mocha
npm install --save-dev nightmare
mkdir test
touch test/test.js
$EDITOR test/test.js ## or open with your favorite editor

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)

Loading a Page

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)
    })
  })
})

Submitting a Form

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)
    })
  })
})

Using the App

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)
    })
  })
})

All Together Now

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!