Introduction to Nightwatch.js

Nightwatch.js is a powerful Node.js-based end-to-end testing framework that provides a simple yet effective syntax for writing browser automation (as discussed in BDD: From Requirements to Automation) tests. Built on WebDriver and supporting modern protocols like WebDriver BiDi and Chrome DevTools, Nightwatch offers an excellent solution for JavaScript developers.

Setup and Configuration

// nightwatch.conf.js
module.exports = {
  src_folders: ['tests'],
  page_objects_path: ['page_objects'],
  custom_commands_path: ['custom_commands'],
  custom_assertions_path: ['custom_assertions'],

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

  test_settings: {
    default: {
      launch_url: 'https://example.com',
      desiredCapabilities: {
        browserName: 'chrome',
        chromeOptions: {
          args: ['--headless', '--no-sandbox']
        }
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome'
      }
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox'
      },
      webdriver: {
        server_path: require('geckodriver').path
      }
    }
  }
};

Page Object Pattern

// page_objects/loginPage.js
module.exports = {
  url: 'https://example.com/login',

  elements: {
    usernameInput: {
      selector: '#username'
    },
    passwordInput: {
      selector: '#password'
    },
    loginButton: {
      selector: 'button[type="submit"]'
    },
    errorMessage: {
      selector: '.error-message'
    }
  },

  commands: [{
    login(username, password) {
      return this
        .navigate()
        .waitForElementVisible('@usernameInput')
        .setValue('@usernameInput', username)
        .setValue('@passwordInput', password)
        .click('@loginButton');
    },

    verifyLoginSuccess() {
      return this
        .assert.urlContains('/dashboard')
        .assert.visible('.welcome-message');
    },

    verifyLoginError(expectedMessage) {
      return this
        .assert.visible('@errorMessage')
        .assert.containsText('@errorMessage', expectedMessage);
    }
  }]
};

Custom Commands

// custom_commands/loginAs.js
module.exports = class LoginAs {
  async command(userType) {
    const users = {
      admin: { username: 'admin@example.com', password: 'admin123' },
      user: { username: 'user@example.com', password: 'user123' },
      guest: { username: 'guest@example.com', password: 'guest123' }
    };

    const credentials = users[userType];

    const loginPage = this.api.page.loginPage();
    await loginPage.login(credentials.username, credentials.password);

    return this;
  }
};

// custom_commands/waitForAjax.js
module.exports = class WaitForAjax {
  async command(timeout = 5000) {
    await this.api.executeAsync(function(done) {
      if (window.jQuery) {
        const checkAjax = () => {
          if (jQuery.active === 0) {
            done(true);
          } else {
            setTimeout(checkAjax, 100);
          }
        };
        checkAjax();
      } else {
        done(true);
      }
    });

    return this;
  }
};

Custom Assertions

// custom_assertions/elementCountEquals.js
exports.assertion = function(selector, expectedCount) {
  this.message = `Testing if element <${selector}> count equals ${expectedCount}`;
  this.expected = expectedCount;

  this.pass = value => value === expectedCount;

  this.value = result => result.value.length;

  this.command = callback => {
    return this.api.elements('css selector', selector, callback);
  };
};

// Usage in tests
browser.assert.elementCountEquals('.product-item', 10);

Parallel Execution

// nightwatch.conf.js - Parallel configuration
module.exports = {
  test_workers: {
    enabled: true,
    workers: 4
  },

  test_settings: {
    default: {
      desiredCapabilities: {
        browserName: 'chrome',
        'goog:chromeOptions': {
          args: ['--headless']
        }
      }
    }
  }
};

// Run tests in parallel
// npx nightwatch --workers=4

Best Practices

1. Organized Test Structure

// tests/e2e/shopping/addToCart.test.js
describe('Shopping Cart', function() {
  before(browser => {
    browser.page.loginPage().login('user@example.com', 'pass123');
  });

  it('should add product to cart', function(browser) {
    const productPage = browser.page.productPage();

    productPage
      .navigate()
      .waitForElementVisible('@productList')
      .clickProduct('Laptop Pro')
      .clickAddToCart()
      .assert.containsText('@cartCount', '1');
  });

  it('should update quantity', function(browser) {
    const cartPage = browser.page.cartPage();

    cartPage
      .navigate()
      .setQuantity('Laptop Pro', 3)
      .assert.containsText('@cartTotal', '$2,999.97');
  });

  after(browser => browser.end());
});

2. Data-Driven Testing

// tests/data-driven/loginValidation.test.js
const testData = require('../data/loginTestData.json');

describe('Login Validation', function() {
  testData.forEach(data => {
    it(`should handle login with ${data.scenario}`, function(browser) {
      const loginPage = browser.page.loginPage();

      loginPage
        .navigate()
        .login(data.username, data.password);

      if (data.shouldSucceed) {
        loginPage.verifyLoginSuccess();
      } else {
        loginPage.verifyLoginError(data.expectedError);
      }
    });
  });
});

3. API Testing Integration

// tests/api/userManagement.test.js
describe('User Management API', function() {
  it('should create user via API and verify in UI', async function(browser) {
    // Create user via API
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      body: JSON.stringify({
        username: 'newuser@example.com',
        password: 'pass123'
      })
    });

    const user = await response.json();

    // Verify in UI
    browser
      .loginAs('admin')
      .page.usersPage()
      .searchUser(user.username)
      .assert.visible(`[data-user-id="${user.id}"]`);
  });
});

Feature Comparison

FeatureNightwatch.jsCypressPlaywright
LanguageJavaScriptJavaScriptJavaScript, TypeScript, Python
WebDriverYes (W3C)No (Own protocol)No (Own protocol)
Cross-browserExcellentLimitedExcellent
Parallel TestingBuilt-inPaid (Dashboard)Built-in
Page ObjectsBuilt-inManualManual
Custom CommandsBuilt-inBuilt-inFixtures
AssertionsBuilt-in + CustomChai + CustomExpect API
Time Travel DebugNoYesYes
Network StubbingLimitedExcellentExcellent
Video RecordingPluginBuilt-inBuilt-in
Setup ComplexityLowVery LowLow

Practical Use Case: E-Commerce Testing

// tests/e2e/completePurchase.test.js
describe('Complete Purchase Flow', function() {
  let orderId;

  before(browser => {
    browser.maximizeWindow();
  });

  it('should complete guest checkout', function(browser) {
    const homePage = browser.page.homePage();
    const productPage = browser.page.productPage();
    const cartPage = browser.page.cartPage();
    const checkoutPage = browser.page.checkoutPage();

    // Browse and add products
    homePage
      .navigate()
      .searchFor('Laptop');

    productPage
      .clickProduct('Laptop Pro 15')
      .clickAddToCart()
      .assert.containsText('@addedMessage', 'Added to cart');

    // Proceed to checkout
    cartPage
      .navigate()
      .proceedToCheckout();

    // Fill checkout form
    checkoutPage
      .fillGuestInfo({
        email: 'guest@example.com',
        firstName: 'John',
        lastName: 'Doe',
        address: '123 Main St',
        city: 'San Francisco',
        state: 'CA',
        zip: '94105'
      })
      .selectShipping('standard')
      .fillPaymentInfo({
        cardNumber: '4532123456789010',
        expiry: '12/25',
        cvv: '123',
        name: 'John Doe'
      })
      .placeOrder();

    // Verify order
    checkoutPage
      .assert.visible('@confirmationMessage')
      .getText('@orderNumber', result => {
        orderId = result.value;
        console.log('Order created:', orderId);
      });
  });

  it('should verify order in account', function(browser) {
    const ordersPage = browser.page.ordersPage();

    browser
      .loginAs('admin')
      .pause(1000);

    ordersPage
      .navigate()
      .searchOrder(orderId)
      .assert.visible(`[data-order-id="${orderId}"]`)
      .assert.containsText(`[data-order-id="${orderId}"] .status`, 'Confirmed');
  });

  after(browser => browser.end());
});

CI/CD Integration

# .github/workflows/nightwatch.yml
name: Nightwatch Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        browser: [chrome, firefox]
        node: [16, 18]

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}

      - name: Install dependencies
        run: npm ci

      - name: Run Nightwatch tests
        run: npx nightwatch --env ${{ matrix.browser }} --workers=4

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: nightwatch-reports-${{ matrix.browser }}
          path: tests_output/

Conclusion

Nightwatch.js provides a mature, well-documented solution for end-to-end testing with excellent WebDriver support and built-in features like page objects and parallel execution. Its straightforward syntax and extensive customization options make it ideal for teams familiar with Node.js looking for reliable browser automation (as discussed in Cucumber BDD Automation: Complete Guide to Behavior-Driven Development Testing).

Key advantages:

  • WebDriver Standards: Full W3C WebDriver compliance
  • Page Objects: Built-in pattern support
  • Customization: Easy custom commands and assertions
  • Parallel Execution: Native support without additional costs
  • Cross-browser: Excellent multi-browser support

Best suited for teams prioritizing WebDriver compatibility, page object patterns, and straightforward Node.js-based automation (as discussed in Cypress Deep Dive: Architecture, Debugging, and Network Stubbing Mastery) without the complexity of modern alternatives like Playwright or Cypress.