by thetestingacademy
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 @qaskills/cli add nemojs-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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('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();
}
});
});
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.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.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
- name: Install QA Skills
run: npx @qaskills/cli add nemojs-testing10 of 29 agents supported