# SkillPatch skill: nightwatchjs-skill

This skill generates NightwatchJS end-to-end tests in JavaScript, covering basic tests, page objects, and built-in assertions with Selenium WebDriver. It supports both local execution (ChromeDriver/GeckoDriver) and cloud execution via LambdaTest/TestMu, with ready-to-use configuration templates. The skill activates on mentions of "Nightwatch", "NightwatchJS", or "nightwatch test".

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/nightwatchjs-skill
curl -sSL https://skillpatch.dev/install_skill/nightwatchjs-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (4)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/cloud-integration.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: nightwatchjs-skill
description: >
  Generates NightwatchJS E2E tests in JavaScript. Integrated test runner with
  Selenium WebDriver, built-in assertions, and page objects. Use when user
  mentions "Nightwatch", "NightwatchJS", "nightwatch.conf.js". Triggers on:
  "Nightwatch", "NightwatchJS", "nightwatch test".
languages:
  - JavaScript
  - TypeScript
category: e2e-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# NightwatchJS Automation Skill

## Step 1 — Execution Target

```
├─ "cloud", "TestMu", "LambdaTest" → Cloud: nightwatch.conf.js with LT env
├─ "local" → Local: ChromeDriver/GeckoDriver
└─ Default → Local, mention cloud option
```

## Core Patterns

### Basic Test

```javascript
module.exports = {
  'Login with valid credentials': function(browser) {
    browser
      .url('http://localhost:3000/login')
      .waitForElementVisible('#email', 5000)
      .setValue('#email', 'user@test.com')
      .setValue('#password', 'password123')
      .click('button[type="submit"]')
      .waitForElementVisible('.dashboard', 10000)
      .assert.containsText('.welcome', 'Welcome')
      .assert.urlContains('/dashboard')
      .end();
  },

  'Login with invalid credentials shows error': function(browser) {
    browser
      .url('http://localhost:3000/login')
      .waitForElementVisible('#email', 5000)
      .setValue('#email', 'wrong@test.com')
      .setValue('#password', 'wrong')
      .click('button[type="submit"]')
      .waitForElementVisible('.error-message', 5000)
      .assert.containsText('.error-message', 'Invalid credentials')
      .end();
  }
};
```

### Page Objects

```javascript
// pages/loginPage.js
module.exports = {
  url: '/login',
  elements: {
    emailInput: '#email',
    passwordInput: '#password',
    loginButton: 'button[type="submit"]',
    errorMessage: '.error-message',
  },
  commands: [{
    login(email, password) {
      return this
        .setValue('@emailInput', email)
        .setValue('@passwordInput', password)
        .click('@loginButton');
    }
  }]
};

// tests/loginTest.js
module.exports = {
  'Login test': function(browser) {
    const login = browser.page.loginPage();
    login.navigate()
      .login('user@test.com', 'password123');
    browser.assert.urlContains('/dashboard');
  }
};
```

### Assertions

```javascript
browser.assert.visible(selector);
browser.assert.not.visible(selector);
browser.assert.containsText(selector, 'text');
browser.assert.urlContains('/path');
browser.assert.titleContains('Page Title');
browser.assert.elementPresent(selector);
browser.assert.cssClassPresent(selector, 'active');
browser.assert.value('#input', 'expected');
browser.assert.attributeEquals(selector, 'href', '/link');
browser.assert.elementsCount('li', 5);
```

### TestMu AI Cloud Config

For full capabilities and shared reference, see [reference/cloud-integration.md](reference/cloud-integration.md).

```javascript
// nightwatch.conf.js
module.exports = {
  test_settings: {
    default: {
      launch_url: 'http://localhost:3000',
      desiredCapabilities: { browserName: 'chrome' }
    },
    lambdatest: {
      selenium: {
        host: 'hub.lambdatest.com',
        port: 80
      },
      desiredCapabilities: {
        browserName: 'chrome',
        browserVersion: 'latest',
        'LT:Options': {
          platform: 'Windows 11',
          build: 'Nightwatch Build',
          name: 'Login Tests',
          user: process.env.LT_USERNAME,
          accessKey: process.env.LT_ACCESS_KEY,
          video: true, console: true, network: true
        }
      }
    }
  },
  page_objects_path: ['pages/'],
};
```

## Setup: `npm install nightwatch --save-dev`
## Run: `npx nightwatch` or `npx nightwatch --env lambdatest`

## Deep Patterns

For advanced patterns, debugging guides, CI/CD integration, and best practices,
see `reference/playbook.md`.

````


### `reference/advanced-patterns.md`

````markdown
# NightwatchJS — Advanced Patterns & Playbook

## Page Objects

```javascript
// pages/loginPage.js
module.exports = {
  url: '/login',
  elements: {
    emailInput: { selector: '#email' },
    passwordInput: { selector: '#password' },
    submitBtn: { selector: 'button[type="submit"]' },
    errorMsg: { selector: '.error-message' }
  },
  commands: [{
    login(email, password) {
      return this
        .setValue('@emailInput', email)
        .setValue('@passwordInput', password)
        .click('@submitBtn')
        .waitForElementVisible('.dashboard', 5000);
    },
    assertError(message) {
      return this.assert.containsText('@errorMsg', message);
    }
  }]
};

// Test using page object
module.exports = {
  'Login test': (browser) => {
    const login = browser.page.loginPage();
    login.navigate()
      .login('admin@test.com', 'password123')
      .assert.urlContains('/dashboard');
  }
};
```

## Custom Commands & Assertions

```javascript
// custom-commands/dragAndDrop.js
module.exports.command = function(source, target) {
  this.moveToElement(source, 10, 10)
    .mouseButtonDown(0)
    .moveToElement(target, 10, 10)
    .mouseButtonUp(0);
  return this;
};

// custom-assertions/hasMinLength.js
exports.assertion = function(selector, minLength, msg) {
  this.formatMessage = () => msg || `Element ${selector} has min length ${minLength}`;
  this.expected = () => `>= ${minLength} characters`;
  this.evaluate = (value) => value.length >= minLength;
  this.value = (result) => result.value;
  this.command = (callback) => this.api.getValue(selector, callback);
};

// Usage: browser.assert.hasMinLength('#password', 8)
```

## API Testing

```javascript
module.exports = {
  'API: Create user': async (browser) => {
    const response = await new Promise((resolve) => {
      browser.perform(() => {
        const http = require('http');
        const req = http.request({ hostname: 'localhost', port: 3000,
          path: '/api/users', method: 'POST',
          headers: { 'Content-Type': 'application/json' }
        }, (res) => {
          let data = '';
          res.on('data', (c) => data += c);
          res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data) }));
        });
        req.write(JSON.stringify({ name: 'Alice' }));
        req.end();
      });
    });
    browser.assert.equal(response.status, 201);
  }
};
```

## Configuration

```javascript
// nightwatch.conf.js
module.exports = {
  src_folders: ['tests'],
  page_objects_path: ['pages'],
  custom_commands_path: ['custom-commands'],
  custom_assertions_path: ['custom-assertions'],
  test_settings: {
    default: {
      launch_url: 'http://localhost:3000',
      desiredCapabilities: { browserName: 'chrome' },
      screenshots: { enabled: true, on_failure: true, path: 'screenshots' },
      globals: { waitForConditionTimeout: 10000, retryAssertionTimeout: 5000 }
    }
  }
};
```

## Anti-Patterns

- ❌ `browser.pause(5000)` — use `waitForElementVisible` or `waitForConditionTimeout`
- ❌ Raw selectors in test files — use page objects with `@element` syntax
- ❌ Callback hell — use `async/await` with Nightwatch 2+
- ❌ Missing `after` hook for browser cleanup

````


### `reference/cloud-integration.md`

````markdown
# Nightwatch.js — TestMu AI Cloud Integration

For full device catalog, capabilities, and LT:Options reference, see [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

## Configuration

Add a `lambdatest` (or `lt`) environment in `nightwatch.conf.js`:

```javascript
// nightwatch.conf.js
module.exports = {
  test_settings: {
    default: {
      launch_url: 'http://localhost:3000',
      desiredCapabilities: { browserName: 'chrome' }
    },
    lambdatest: {
      selenium: {
        host: 'hub.lambdatest.com',
        port: 80
      },
      desiredCapabilities: {
        browserName: 'chrome',
        browserVersion: 'latest',
        'LT:Options': {
          platform: 'Windows 11',
          build: 'Nightwatch Build',
          name: 'Nightwatch Tests',
          user: process.env.LT_USERNAME,
          accessKey: process.env.LT_ACCESS_KEY,
          video: true,
          console: true,
          network: true
        }
      }
    }
  },
  page_objects_path: ['pages/'],
};
```

## Run on Cloud

```bash
export LT_USERNAME=your_username
export LT_ACCESS_KEY=your_access_key
npx nightwatch --env lambdatest
```

## Parallel / Multiple Browsers

Define multiple environments (e.g. `lambdatest_chrome`, `lambdatest_firefox`) with different `desiredCapabilities`, then run with `--env lambdatest_chrome,lambdatest_firefox` or use a matrix in CI. For capability values (platforms, browsers), see [shared/testmu-cloud-reference.md](../../shared/testmu-cloud-reference.md).

````


### `reference/playbook.md`

````markdown
# Nightwatch.js — Advanced Implementation Playbook

## §1 — Production Configuration

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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)

```javascript
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)

```javascript
// 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

```yaml
# 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

| Problem | Cause | Fix |
|---------|-------|-----|
| Element not found | Selector wrong or not visible | Use `waitForElementVisible` before interaction |
| Page object command fails | Missing `return this` | Always return `this` for chaining |
| Timeout on waitFor | Element never appears | Increase `waitForConditionTimeout` in globals |
| Screenshots not saving | Wrong path config | Check `screenshots.path` in config |
| Workers causing conflicts | Shared state between tests | Use `test_workers: { enabled: false }` to debug |
| Custom command not found | Wrong folder path | Verify `custom_commands_path` in config |
| Element stale after navigation | Page re-rendered | Re-query element after navigation |
| `@` selector not working | Not 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`

````
