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 | bash
helpmetest login

While Nightwatch catches regressions in CI, HelpMeTest catches incidents in production — different tools solving different parts of the reliability problem. With usage-based pricing 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

Start now free