← All skills

Nightwatch.js Skill

E2e testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill nightwatchjs-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Nightwatch.js — Advanced Implementation Playbook

§1 — Production Configuration

// nightwatch.conf.js
module.exports = {
  src_folders: ['tests'],
  page_objects_path: ['pages'],
  custom_commands_path: ['commands'],
  custom_assertions_path: ['assertions'],
  globals_path: 'globals.js',
  output_folder: 'reports',

  webdriver: { start_process: true, port: 9515 },

  test_settings: {
    default: {
      launch_url: process.env.BASE_URL || 'http://localhost:3000',
      desiredCapabilities: {
        browserName: 'chrome',
        'goog:chromeOptions': {
          args: process.env.CI ? ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage'] : [],
          w3c: true,
        },
      },
      screenshots: { enabled: true, on_failure: true, on_error: true, path: 'screenshots' },
      end_session_on_fail: true,
      skip_testcases_on_fail: false,
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        'moz:firefoxOptions': { args: process.env.CI ? ['-headless'] : [] },
      },
    },

    selenium: {
      selenium: {
        start_process: true,
        host: 'localhost',
        port: 4444,
        server_path: require('selenium-server').path,
      },
    },
  },

  test_workers: { enabled: true, workers: 'auto' },
};

Globals File

// globals.js
module.exports = {
  waitForConditionTimeout: 10000,
  retryAssertionTimeout: 5000,
  asyncHookTimeout: 30000,

  before(done) { console.log('Global setup'); done(); },
  after(done) { console.log('Global teardown'); done(); },
  beforeEach(browser, done) { done(); },
  afterEach(browser, done) {
    if (browser.currentTest.results.failed > 0) {
      browser.saveScreenshot(`screenshots/failure-${Date.now()}.png`);
    }
    done();
  },
};

§2 — Page Objects

// pages/loginPage.js
module.exports = {
  url() { return `${this.api.launchUrl}/login`; },

  elements: {
    emailInput: { selector: '[data-testid="email"]' },
    passwordInput: { selector: '[data-testid="password"]' },
    submitBtn: { selector: '[data-testid="login-btn"]' },
    errorMsg: { selector: '.error-message' },
    rememberMe: { selector: '[data-testid="remember-me"]' },
  },

  commands: [{
    login(email, password) {
      return this
        .waitForElementVisible('@emailInput')
        .clearValue('@emailInput').setValue('@emailInput', email)
        .clearValue('@passwordInput').setValue('@passwordInput', password)
        .click('@submitBtn');
    },

    getError() {
      this.waitForElementVisible('@errorMsg');
      return this.getText('@errorMsg');
    },

    assertLoggedIn() {
      return this.waitForElementNotPresent('@emailInput');
    },
  }],
};

// pages/dashboardPage.js
module.exports = {
  elements: {
    welcomeMsg: { selector: '[data-testid="welcome"]' },
    navMenu: { selector: '[data-testid="nav"]' },
    logoutBtn: { selector: '[data-testid="logout"]' },
  },

  commands: [{
    assertLoaded() {
      return this.waitForElementVisible('@welcomeMsg');
    },
    getWelcomeText(callback) {
      return this.getText('@welcomeMsg', callback);
    },
  }],
};

§3 — Test Patterns

// tests/login.test.js
describe('Login @smoke', function() {
  let loginPage, dashboard;

  before(function(browser) {
    loginPage = browser.page.loginPage();
    dashboard = browser.page.dashboardPage();
    loginPage.navigate();
  });

  it('should login with valid credentials', function(browser) {
    loginPage.login('admin@test.com', 'admin123');
    dashboard.assertLoaded();
    dashboard.getWelcomeText(function(result) {
      browser.assert.ok(result.value.includes('Welcome'));
    });
  });

  it('should show error for invalid credentials', function() {
    loginPage.navigate();
    loginPage.login('wrong@test.com', 'wrong');
    loginPage.assert.visible('@errorMsg');
    loginPage.expect.element('@errorMsg').text.to.contain('Invalid');
  });

  after(function(browser) {
    browser.end();
  });
});

// BDD-style with expect
describe('Search Feature @regression', function() {
  it('should return results', function(browser) {
    browser
      .url(`${browser.launch_url}/search`)
      .waitForElementVisible('[data-testid="search-input"]')
      .setValue('[data-testid="search-input"]', 'testing')
      .click('[data-testid="search-btn"]')
      .waitForElementVisible('.results');

    browser.expect.elements('.result-item').count.to.be.above(0);
    browser.expect.element('.result-item:first-child').text.to.contain('testing');
  });
});

§4 — Custom Commands

// commands/loginViaApi.js
module.exports = {
  command: async function(email, password) {
    const response = await fetch(`${this.launchUrl}/api/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    }).then(r => r.json());

    this.setCookie({ name: 'token', value: response.token, path: '/' });
    return this;
  },
};

// commands/waitForNetworkIdle.js
module.exports = {
  command: function(timeout = 5000) {
    this.execute(function() {
      return performance.getEntriesByType('resource')
        .filter(r => !r.responseEnd).length;
    }, [], function(result) {
      this.assert.equal(result.value, 0, 'Network is idle');
    });
    return this;
  },
};

// Usage in tests
browser.loginViaApi('admin@test.com', 'admin123');
browser.url('/dashboard');

§5 — Custom Assertions

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

  this.evaluate = function(value) { return value === expectedCount; };

  this.value = function(result) { return result.value.length; };

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

// Usage: browser.assert.elementCount('.item', 5);

§6 — API Testing (Built-in)

describe('API Tests', function() {
  it('should create a user', async function(browser) {
    const response = await browser.request('POST', '/api/users', {
      body: { name: 'Alice', email: 'alice@test.com' },
      headers: { 'Content-Type': 'application/json' },
    });

    browser.assert.equal(response.status, 201);
    browser.assert.ok(response.body.id);
  });

  it('should verify API + UI integration', async function(browser) {
    // Create via API
    await browser.request('POST', '/api/products', {
      body: { name: 'Widget', price: 9.99 },
    });

    // Verify in UI
    browser.url('/products');
    browser.waitForElementVisible('[data-testid="product-list"]');
    browser.assert.textContains('.product-name', 'Widget');
  });
});

§7 — Component Testing (React/Vue)

// Nightwatch component testing
describe('Button Component', function() {
  it('should render with label', async function(browser) {
    const button = await browser.mountComponent('/src/components/Button.jsx', {
      props: { label: 'Click Me', variant: 'primary' },
    });

    browser.expect.element(button).text.to.equal('Click Me');
    browser.expect.element(button).to.have.css('background-color');
  });
});

§8 — CI/CD Integration

# GitHub Actions
name: Nightwatch Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [default, firefox]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx nightwatch --env ${{ matrix.env }} --tag smoke
        env: { CI: true }
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: nightwatch-results-${{ matrix.env }}
          path: |
            reports/
            screenshots/

§9 — Debugging Quick-Reference

ProblemCauseFix
Element not foundSelector wrong or not visibleUse waitForElementVisible before interaction
Page object command failsMissing return thisAlways return this for chaining
Timeout on waitForElement never appearsIncrease waitForConditionTimeout in globals
Screenshots not savingWrong path configCheck screenshots.path in config
Workers causing conflictsShared state between testsUse test_workers: { enabled: false } to debug
Custom command not foundWrong folder pathVerify custom_commands_path in config
Element stale after navigationPage re-renderedRe-query element after navigation
@ selector not workingNot using page object@ prefix only works in page object context

§10 — Best Practices Checklist

  • ✅ Use Page Objects for all page interactions
  • ✅ Use custom commands for reusable actions (API login, waits)
  • ✅ Use data-testid selectors for stability
  • ✅ Use waitForElementVisible before every interaction
  • ✅ Return this in all page object commands for chaining
  • ✅ Use globals file for shared hooks and timeouts
  • ✅ Use test_workers for parallel execution
  • ✅ Use tags (@smoke, @regression) for selective runs
  • ✅ Capture screenshots on failure in afterEach hook
  • ✅ Use built-in API testing for backend verification
  • ✅ Run headless in CI with --headless=new
  • ✅ Structure: tests/, pages/, commands/, assertions/, globals.js