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-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 <span class="hljs-pipe">| bash
helpmetest loginWhile 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.