Qaskills Nemo.js Testing
Comprehensive Nemo.js test automation skill for PayPal's Selenium-based Node.js testing framework featuring view-driven locators, flexible configuration, Mocha integration, and scalable browser automation patterns.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/nemojs-testing" ~/.claude/skills/pramoddutta-qaskills-nemo-js-testing && rm -rf "$T"
manifest:
seed-skills/nemojs-testing/SKILL.mdsource content
Nemo.js Testing
You are an expert QA engineer specializing in Nemo.js test automation. When the user asks you to write, review, debug, or set up Nemo.js-related tests or configurations, follow these detailed instructions.
Nemo.js is PayPal's Selenium-based test automation framework for Node.js. It provides a configuration-driven approach to browser automation with a view-based locator system, lifecycle management, and deep Mocha integration.
Core Principles
- Configuration-Driven Setup -- Nemo uses JSON/JS configuration files to define browser capabilities, server settings, and plugin configurations. Keep all environment-specific values in config rather than test code.
- View-Based Locator System -- Use Nemo's view system (
) for element location. Define locators in JSON files and reference them through the view API for centralized selector management.nemo.view - Mocha Integration -- Nemo integrates tightly with Mocha for test structure, lifecycle hooks, and reporting. Use
/describe
blocks withit
/before
hooks for setup and teardown.after - Explicit Waits -- Use
and custom wait functions rather than implicit waits or static delays. Selenium's timing issues require explicit synchronization.nemo.view._waitVisible() - Plugin Architecture -- Extend Nemo's capabilities through plugins for screenshot capture, data management, and custom utilities. Keep test logic clean by delegating cross-cutting concerns to plugins.
- Test Isolation -- Each test must be independent. Create fresh browser sessions or clear state in
hooks to prevent test pollution.beforeEach - Locator Abstraction -- Never hardcode selectors in test files. Define all locators in view JSON files and access them through the view API for maintainability.
When to Use This Skill
- When working with an existing Nemo.js test suite
- When setting up Nemo.js for a Node.js-based project
- When writing Selenium-based browser tests with Mocha in the Nemo ecosystem
- When configuring Nemo views and locator files
- When debugging Nemo.js test failures
- When working with
,nemo.view._find()
, or Nemo configuration filesnemo.view._waitVisible()
Project Structure
project-root/ ├── nemo.config.js # Main Nemo configuration ├── test/ │ ├── functional/ # Test spec files │ │ ├── auth/ │ │ │ ├── login.test.js │ │ │ └── registration.test.js │ │ ├── checkout/ │ │ │ └── purchase.test.js │ │ └── search/ │ │ └── product-search.test.js │ ├── views/ # View locator definitions │ │ ├── login.json │ │ ├── dashboard.json │ │ ├── checkout.json │ │ └── search.json │ ├── plugins/ # Custom Nemo plugins │ │ ├── screenshot-plugin.js │ │ └── data-helper.js │ ├── fixtures/ # Test data │ │ └── test-users.json │ └── helpers/ # Utility functions │ ├── wait-helpers.js │ └── api-helpers.js ├── reports/ # Test reports ├── screenshots/ # Captured screenshots └── package.json
Configuration
nemo.config.js
module.exports = { plugins: { view: { module: 'nemo-view', arguments: ['test/views'], }, }, driver: { browser: process.env.BROWSER || 'chrome', builders: { forBrowser: [process.env.BROWSER || 'chrome'], withCapabilities: [ { browserName: process.env.BROWSER || 'chrome', chromeOptions: { args: process.env.CI ? ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] : [], }, }, ], }, server: process.env.SELENIUM_SERVER || undefined, }, data: { baseUrl: process.env.BASE_URL || 'http://localhost:3000', defaultTimeout: 10000, }, };
View Locator Files
login.json
{ "usernameInput": { "locator": "[data-testid='username-input']", "type": "css" }, "passwordInput": { "locator": "[data-testid='password-input']", "type": "css" }, "submitButton": { "locator": "[data-testid='login-submit']", "type": "css" }, "errorMessage": { "locator": "[data-testid='login-error']", "type": "css" }, "rememberCheckbox": { "locator": "[data-testid='remember-me']", "type": "css" }, "forgotPasswordLink": { "locator": "a[href='/forgot-password']", "type": "css" } }
dashboard.json
{ "welcomeMessage": { "locator": "[data-testid='welcome-message']", "type": "css" }, "widgetContainer": { "locator": "[data-testid='dashboard-widgets']", "type": "css" }, "userAvatar": { "locator": "[data-testid='user-avatar']", "type": "css" }, "logoutButton": { "locator": "[data-testid='logout-btn']", "type": "css" }, "notificationBadge": { "locator": "[data-testid='notification-badge']", "type": "css" } }
Writing Tests
Basic Authentication Test
const assert = require('assert'); describe('User Authentication', function () { this.timeout(30000); let nemo; before(async function () { nemo = this.nemo; await nemo.driver.get(`${nemo.data.baseUrl}/login`); }); it('should login with valid credentials', async function () { const loginView = nemo.view.login; await loginView.usernameInput().waitVisible(nemo.data.defaultTimeout); await loginView.usernameInput().clear(); await loginView.usernameInput().sendKeys('testuser@example.com'); await loginView.passwordInput().clear(); await loginView.passwordInput().sendKeys('SecurePass123!'); await loginView.submitButton().click(); // Wait for dashboard to load const dashView = nemo.view.dashboard; await dashView.welcomeMessage().waitVisible(nemo.data.defaultTimeout); const welcomeText = await dashView.welcomeMessage().getText(); assert.ok(welcomeText.includes('Welcome'), `Expected welcome text, got: ${welcomeText}`); }); it('should show error for invalid credentials', async function () { await nemo.driver.get(`${nemo.data.baseUrl}/login`); const loginView = nemo.view.login; await loginView.usernameInput().waitVisible(nemo.data.defaultTimeout); await loginView.usernameInput().clear(); await loginView.usernameInput().sendKeys('invalid@example.com'); await loginView.passwordInput().clear(); await loginView.passwordInput().sendKeys('wrongpassword'); await loginView.submitButton().click(); await loginView.errorMessage().waitVisible(nemo.data.defaultTimeout); const errorText = await loginView.errorMessage().getText(); assert.ok( errorText.includes('Invalid email or password'), `Expected error message, got: ${errorText}` ); }); after(async function () { if (nemo && nemo.driver) { await nemo.driver.quit(); } }); });
Working with Multiple Elements
describe('Product Listing', function () { this.timeout(30000); let nemo; before(async function () { nemo = this.nemo; await nemo.driver.get(`${nemo.data.baseUrl}/products`); }); it('should display product cards', async function () { // Find multiple elements using _finds const productCards = await nemo.view._finds('[data-testid="product-card"]'); assert.ok(productCards.length > 0, 'Expected at least one product card'); // Verify each card has required elements for (const card of productCards) { const title = await card.findElement({ css: '[data-testid="product-title"]' }); const titleText = await title.getText(); assert.ok(titleText.length > 0, 'Product title should not be empty'); const price = await card.findElement({ css: '[data-testid="product-price"]' }); const priceText = await price.getText(); assert.ok(priceText.match(/\$[\d.]+/), `Expected price format, got: ${priceText}`); } }); it('should filter products by search query', async function () { const searchInput = await nemo.view._find('[data-testid="search-input"]'); await searchInput.clear(); await searchInput.sendKeys('laptop'); const searchBtn = await nemo.view._find('[data-testid="search-submit"]'); await searchBtn.click(); // Wait for results to update await nemo.view._waitVisible('[data-testid="search-results"]', nemo.data.defaultTimeout); const results = await nemo.view._finds('[data-testid="product-card"]'); assert.ok(results.length > 0, 'Expected search results'); }); after(async function () { if (nemo && nemo.driver) { await nemo.driver.quit(); } }); });
Custom Wait Patterns
describe('Dynamic Content', function () { this.timeout(30000); let nemo; before(async function () { nemo = this.nemo; }); it('should wait for loading spinner to disappear', async function () { await nemo.driver.get(`${nemo.data.baseUrl}/dashboard`); // Wait for spinner to appear first try { await nemo.view._waitVisible('[data-testid="loading-spinner"]', 2000); } catch (e) { // Spinner may already be gone on fast loads } // Wait for spinner to disappear const { until, By } = require('selenium-webdriver'); await nemo.driver.wait( until.stalenessOf( await nemo.driver.findElement(By.css('[data-testid="loading-spinner"]')).catch(() => null) ), 15000, 'Loading spinner did not disappear' ); // Verify content loaded await nemo.view._waitVisible('[data-testid="dashboard-content"]', 10000); }); it('should wait for specific text in element', async function () { await nemo.driver.get(`${nemo.data.baseUrl}/status`); const { until } = require('selenium-webdriver'); const statusElement = await nemo.view._find('[data-testid="status-text"]'); await nemo.driver.wait(async () => { const text = await statusElement.getText(); return text.includes('Connected'); }, 15000, 'Expected status to show Connected'); }); after(async function () { if (nemo && nemo.driver) { await nemo.driver.quit(); } }); });
Screenshot Capture
const fs = require('fs'); const path = require('path'); async function captureScreenshot(nemo, testName) { const screenshot = await nemo.driver.takeScreenshot(); const filename = `${testName.replace(/\s+/g, '-')}-${Date.now()}.png`; const filepath = path.join(__dirname, '../../screenshots', filename); fs.writeFileSync(filepath, screenshot, 'base64'); return filepath; } // Usage in tests afterEach(async function () { if (this.currentTest.state === 'failed') { const screenshotPath = await captureScreenshot(nemo, this.currentTest.title); console.log(`Screenshot saved: ${screenshotPath}`); } });
Reusable Helper Functions
// test/helpers/wait-helpers.js async function waitForUrlContains(nemo, urlFragment, timeout = 10000) { const { until } = require('selenium-webdriver'); await nemo.driver.wait(until.urlContains(urlFragment), timeout, `URL did not contain "${urlFragment}" within ${timeout}ms`); } async function waitForElementCount(nemo, selector, expectedCount, timeout = 10000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const elements = await nemo.view._finds(selector); if (elements.length === expectedCount) return; await nemo.driver.sleep(250); } throw new Error(`Expected ${expectedCount} elements for "${selector}", timed out after ${timeout}ms`); } async function clearAndType(nemo, selector, text) { const element = await nemo.view._find(selector); await element.clear(); await element.sendKeys(text); } module.exports = { waitForUrlContains, waitForElementCount, clearAndType };
Best Practices
- Define all locators in view JSON files -- Never hardcode CSS selectors or XPath in test files. The view system centralizes locator management and simplifies maintenance.
- Use
attributes for all selectors. Coordinate with developers to add these attributes, ensuring tests are decoupled from visual styling.data-testid - Set explicit timeouts on all waits -- Use
as a baseline but allow individual waits to override for operations that need more or less time.nemo.data.defaultTimeout - Implement proper teardown -- Always call
innemo.driver.quit()
hooks to prevent orphaned browser processes from accumulating.after - Use environment variables for base URLs, browser selection, and credentials. This allows the same test suite to run across development, staging, and production.
- Capture screenshots on failure using
hooks. Store them with descriptive filenames that include the test name and timestamp.afterEach - Keep tests atomic -- Each
block should test one behavior. Long tests that verify multiple features are hard to debug and maintain.it - Use Mocha's
to set appropriate timeouts per test or suite. The default 2-second timeout is usually too short for browser tests.this.timeout() - Create reusable helper functions for common patterns like login, navigation, and data verification. Import them across test files to reduce duplication.
- Run tests in headless mode in CI to reduce resource usage. Configure Chrome headless flags in the Nemo configuration based on the
environment variable.CI
Anti-Patterns
- Using
for synchronization -- Static waits are slow and unreliable. Usedriver.sleep()
or Selenium's_waitVisible()
conditions instead.until - Hardcoding selectors in test files -- Duplicating selectors across tests means a single UI change requires updates in many files.
- Not cleaning up browser instances -- Forgetting
in teardown leaves zombie Chrome processes that consume memory and crash CI.driver.quit() - Sharing state between tests -- Tests that rely on side effects from previous tests break when run in isolation or in different order.
- Using overly specific CSS selectors -- Selectors like
break on minor DOM changes.div.app > div.main > ul > li:first-child > a - Not setting Mocha timeouts -- Default 2-second timeouts cause false failures on legitimate browser interactions that take longer.
- Ignoring element staleness -- After page navigation or AJAX updates, previously found elements may become stale. Re-query elements after state changes.
- Putting test data in code -- Hardcoded usernames, passwords, and product IDs make tests environment-dependent. Use fixtures and environment variables.
- Not using the view system -- Bypassing Nemo's view abstraction defeats the framework's key benefit of centralized locator management.
- Writing monolithic test functions -- Long
blocks that verify multiple behaviors are hard to debug when they fail midway through.it
CLI Reference
# Run all tests with Mocha npx mocha test/functional/**/*.test.js --timeout 30000 --recursive # Run specific test file npx mocha test/functional/auth/login.test.js --timeout 30000 # Run tests matching a pattern npx mocha test/functional/**/*.test.js --grep "login" --timeout 30000 # Run with reporter npx mocha test/functional/**/*.test.js --timeout 30000 --reporter spec # Run in watch mode npx mocha test/functional/**/*.test.js --timeout 30000 --watch
Setup
# Install Nemo and dependencies npm install --save-dev nemo nemo-view # Install Selenium WebDriver npm install --save-dev selenium-webdriver # Install Chrome driver npm install --save-dev chromedriver # Install Mocha npm install --save-dev mocha # Install assertion library npm install --save-dev chai