Nightwatch.js Tutorial: Browser Automation & E2E Testing
Browser automation used to mean wrestling with Selenium's verbose Java APIs.
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-projectThe 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-devAdd 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.jsUsing 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 --parallelTo run across multiple browsers simultaneously:
nightwatch --env chrome,firefox,edgeCI/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: trueGitLab 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: alwaysCommon 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 loginWhile 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.