Nightwatch.js Tutorial: Browser Automation & E2E Testing

Nightwatch.js Tutorial: Browser Automation & E2E Testing

Browser automation used to mean wrestling with Selenium's verbose Java APIs. Nightwatch.js changed that for JavaScript teams by wrapping WebDriver in a clean, Node.js-native API. Since its 2014 debut, it has grown into a full-featured E2E testing framework with built-in test runner, parallel execution, and first-class TypeScript support.

This tutorial covers everything you need to go from zero to running Nightwatch.js tests in CI.

What Is Nightwatch.js?

Nightwatch.js is an open-source E2E testing framework for web applications. It uses the W3C WebDriver API (or Chrome DevTools Protocol via @nightwatch/selenium-server) to control real browsers — Chrome, Firefox, Edge, and Safari.

Key characteristics:

  • Node.js native — write tests in JavaScript or TypeScript
  • Built-in test runner — no need for Mocha or Jest wrappers
  • Parallel execution — run tests across multiple browsers simultaneously
  • BDD syntax support — describe/it blocks or custom DSL
  • Selenium or CDP — choose your driver per project needs

Nightwatch v3 (released 2023) added native support for the Chrome DevTools Protocol, making it significantly faster for Chromium-based testing.

Installation

npm init nightwatch@latest my-project

The interactive setup wizard asks which browsers, test runner, and language you prefer. It generates a complete project structure.

For an existing project:

npm install nightwatch --save-dev
npm install chromedriver --save-dev

Add to package.json:

{
  "scripts": {
    "test": "nightwatch",
    "test:chrome": "nightwatch --env chrome",
    "test:parallel": "nightwatch --parallel"
  }
}

Configuration

Nightwatch reads nightwatch.conf.js (or .ts) in the project root:

module.exports = {
  src_folders: ['tests'],
  output_folder: 'reports',

  webdriver: {
    start_process: true,
    server_path: require('chromedriver').path,
    port: 4444,
  },

  test_settings: {
    default: {
      desiredCapabilities: {
        browserName: 'chrome',
        'goog:chromeOptions': {
          args: ['--headless', '--disable-gpu', '--no-sandbox'],
        },
      },
    },
    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
      },
    },
    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
      },
    },
  },
};

Writing Your First Test

Nightwatch tests export an object with test methods. Each method receives the browser object — your gateway to all browser interactions:

// tests/homepage.js
module.exports = {
  'Homepage loads and shows navigation': function (browser) {
    browser
      .url('https://example.com')
      .waitForElementVisible('body', 1000)
      .assert.titleContains('Example')
      .assert.visible('nav')
      .assert.elementCount('nav a', 4)
      .end();
  },

  'Contact form submits successfully': function (browser) {
    browser
      .url('https://example.com/contact')
      .waitForElementVisible('form', 2000)
      .setValue('input[name="email"]', 'test@example.com')
      .setValue('textarea[name="message"]', 'Hello from Nightwatch')
      .click('button[type="submit"]')
      .waitForElementVisible('.success-message', 3000)
      .assert.textContains('.success-message', 'Thank you')
      .end();
  },
};

Run with:

npx nightwatch tests/homepage.js

Using Hooks

Nightwatch provides before, after, beforeEach, and afterEach hooks:

module.exports = {
  before: function (browser) {
    // Runs once before all tests in this file
    browser.maximizeWindow();
  },

  beforeEach: function (browser) {
    // Runs before each test
    browser.url('https://example.com');
  },

  afterEach: function (browser) {
    // Cleanup after each test
    browser.clearCookies();
  },

  after: function (browser) {
    // Runs once after all tests
    browser.end();
  },

  'Test A': function (browser) {
    browser.assert.visible('.hero');
  },

  'Test B': function (browser) {
    browser.assert.visible('.features');
  },
};

Async/Await Syntax

Nightwatch v3 supports async/await, making tests more readable:

describe('User authentication', function () {
  it('should log in successfully', async function (browser) {
    await browser.url('https://app.example.com/login');
    await browser.setValue('#email', 'user@example.com');
    await browser.setValue('#password', 'secret123');
    await browser.click('button[type="submit"]');
    await browser.waitForElementVisible('.dashboard', 3000);
    
    const title = await browser.getTitle();
    browser.assert.strictEqual(title, 'Dashboard - My App');
  });

  it('should show error for wrong password', async function (browser) {
    await browser.url('https://app.example.com/login');
    await browser.setValue('#email', 'user@example.com');
    await browser.setValue('#password', 'wrongpassword');
    await browser.click('button[type="submit"]');
    await browser.waitForElementVisible('.error-message', 2000);
    
    const errorText = await browser.getText('.error-message');
    browser.assert.strictEqual(errorText, 'Invalid credentials');
  });
});

Page Object Model

For maintainable test suites, use Page Objects to encapsulate page-specific selectors and actions:

// pages/loginPage.js
const loginCommands = {
  login(email, password) {
    return this
      .waitForElementVisible('@emailInput')
      .setValue('@emailInput', email)
      .setValue('@passwordInput', password)
      .click('@submitButton');
  },
};

module.exports = {
  url: 'https://app.example.com/login',
  commands: [loginCommands],
  elements: {
    emailInput: { selector: '#email' },
    passwordInput: { selector: '#password' },
    submitButton: { selector: 'button[type="submit"]' },
    errorMessage: { selector: '.error-message' },
    successMessage: { selector: '.success-message' },
  },
};
// tests/login.js
describe('Login', function () {
  it('authenticates valid user', async function (browser) {
    const loginPage = browser.page.loginPage();
    await loginPage.navigate();
    await loginPage.login('user@example.com', 'password123');
    await browser.waitForElementVisible('.dashboard', 3000);
  });
});

Register pages in nightwatch.conf.js:

module.exports = {
  page_objects_path: ['pages'],
  // ...
};

Assertions

Nightwatch has two assertion styles:

Standard assertions:

browser
  .assert.visible('#menu')                    // element is visible
  .assert.hidden('#overlay')                  // element is hidden
  .assert.textContains('.title', 'Welcome')   // text contains substring
  .assert.textEquals('.title', 'Welcome')     // exact text match
  .assert.valueEquals('#input', 'hello')      // input value
  .assert.elementCount('.item', 5)            // element count
  .assert.urlContains('/dashboard')           // current URL
  .assert.cssProperty('.btn', 'color', 'rgb(0, 128, 0)');

Expect-style (Chai-based):

browser.expect.element('#title').text.to.contain('Welcome');
browser.expect.element('#counter').value.to.equal('42');
browser.expect.element('.button').to.be.visible;
browser.expect.element('#modal').to.not.be.present;
browser.expect.url().to.contain('/success');

Running Tests in Parallel

Enable parallel execution in config:

module.exports = {
  test_workers: {
    enabled: true,
    workers: 'auto', // or a number
  },
};

Or on the command line:

nightwatch --parallel

To run across multiple browsers simultaneously:

nightwatch --env chrome,firefox,edge

CI/CD Integration

GitHub Actions:

name: E2E Tests
on: [push, pull_request]

jobs:
  nightwatch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx nightwatch --headless
        env:
          CI: true

GitLab CI:

e2e-tests:
  image: node:18
  before_script:
    - apt-get update && apt-get install -y chromium-driver
    - npm ci
  script:
    - npx nightwatch --headless
  artifacts:
    paths:
      - reports/
    when: always

Common Commands Reference

// Navigation
browser.url('https://example.com');
browser.back();
browser.forward();
browser.refresh();

// Interaction
browser.click('#button');
browser.setValue('#input', 'text');
browser.clearValue('#input');
browser.submitForm('form');
browser.moveToElement('#hover-target', 0, 0);

// Waiting
browser.waitForElementVisible('#element', 5000);
browser.waitForElementPresent('#element', 5000);
browser.waitForElementNotVisible('#spinner', 5000);
browser.pause(500); // explicit wait (avoid when possible)

// Getting values
browser.getText('.message', function(result) {
  console.log(result.value);
});
browser.getAttribute('#link', 'href', function(result) {
  console.log(result.value);
});

// Screenshots
browser.saveScreenshot('screenshot.png');

Integrating with HelpMeTest

Nightwatch covers deterministic UI flows well, but what about monitoring those flows 24/7 after deployment?

HelpMeTest complements Nightwatch by running Robot Framework + Playwright tests continuously against production:

# Install HelpMeTest CLI
curl -fsSL https://helpmetest.com/install <span class="hljs-pipe">| bash
helpmetest login

While Nightwatch catches regressions in CI, HelpMeTest catches incidents in production — different tools solving different parts of the reliability problem. At $100/month flat for unlimited tests, it's cost-effective alongside an existing Nightwatch suite.

Nightwatch vs Playwright vs Cypress

Feature Nightwatch Playwright Cypress
Language JS/TS JS/TS/Python/Java/.NET JS/TS
Browser support Chrome, Firefox, Edge, Safari Chrome, Firefox, Safari, Edge Chrome, Firefox, Edge
WebDriver Yes (W3C) No (CDP/protocol) No
  • | API style | Chained / async-await | async-await | chained promises | | Parallel execution | ✅ | ✅ | ✅ (paid) | | Built-in test runner | ✅ | ✅ | ✅ |

Nightwatch remains a strong choice when you need WebDriver compatibility or want a framework that feels native to Node.js test suites.

Conclusion

Nightwatch.js provides a pragmatic approach to browser automation: built-in test runner, WebDriver compatibility, and clean JavaScript API. Its v3 release with CDP support and async/await makes it competitive with modern alternatives.

Start with the setup wizard, write your first test against a real URL, and expand with page objects as your suite grows. Pair Nightwatch in CI with continuous monitoring in production for complete coverage across the software delivery lifecycle.

Read more