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.
npx skills add https://github.com/PramodDutta/qaskills --skill 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.
nemo.view) for element location. Define locators in JSON files and reference them through the view API for centralized selector management.describe/it blocks with before/after hooks for setup and teardown.nemo.view._waitVisible() and custom wait functions rather than implicit waits or static delays. Selenium's timing issues require explicit synchronization.beforeEach hooks to prevent test pollution.nemo.view._find(), nemo.view._waitVisible(), or Nemo configuration filesproject-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
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,
},
};
{
"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"
}
}
{
"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"
}
}
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('[email protected]');
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('[email protected]');
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();
}
});
});
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();
}
});
});
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();
}
});
});
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}`);
}
});
// 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 };
data-testid attributes for all selectors. Coordinate with developers to add these attributes, ensuring tests are decoupled from visual styling.nemo.data.defaultTimeout as a baseline but allow individual waits to override for operations that need more or less time.nemo.driver.quit() in after hooks to prevent orphaned browser processes from accumulating.afterEach hooks. Store them with descriptive filenames that include the test name and timestamp.it block should test one behavior. Long tests that verify multiple features are hard to debug and maintain.this.timeout() to set appropriate timeouts per test or suite. The default 2-second timeout is usually too short for browser tests.10. Run tests in headless mode in CI to reduce resource usage. Configure Chrome headless flags in the Nemo configuration based on the CI environment variable.
driver.sleep() for synchronization -- Static waits are slow and unreliable. Use _waitVisible() or Selenium's until conditions instead.driver.quit() in teardown leaves zombie Chrome processes that consume memory and crash CI.div.app > div.main > ul > li:first-child > a break on minor DOM changes.10. Writing monolithic test functions -- Long it blocks that verify multiple behaviors are hard to debug when they fail midway through.
# 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
# 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
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Run Playwright tests at scale with cloud-hosted browsers and integrated Azure portal reporting.
Browser debugging, performance profiling, and automation via Chrome DevTools MCP. Use when user says "debug this page", "take a screenshot", "check network requests", "profile performance", "inspect console errors", or "analyze page load". Do NOT use for full E2E test suites (use playwright-skill) or non-browser debugging.
QA-test a website or web app and return a 1-5 quality score (5 = flawless, 1 = broken) with evidence. Use when the user wants to test, QA, evaluate, score, or "check how good" a site, page, flow, or app — including a local dev server (e.g. "qa test localhost:5173", "does the checkout work?", "rate this landing page"). Drives a real Browser Use cloud browser, tunneling localhost automatically.
Set up component testing with Playwright using a story gallery — scaffold stories and a gallery dev page driven by the built-in mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue.
Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.
Always use browser-harness for any web interaction: automation, scraping, testing, or site/app work.
Run a browser-based UI review of the WordPress.com Help Center across multiple surfaces, looking for visual and behavioral issues. Use when asked to test the Help Center UI.
Take pramoddutta/nemo.js testing from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.