seed-skills/webdriverio-e2e/SKILL.md
Build WebdriverIO E2E suites — wdio.conf.ts setup, $ and $$ selectors, auto-wait and waitUntil, Mocha framework structure, page objects, parallel capabilities, and services for visual testing and Appium mobile.
npx skillsauth add PramodDutta/qaskills WebdriverIO E2E TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent write and configure WebdriverIO (WDIO) end-to-end tests: a correct wdio.conf.ts, $/$$ selector usage with auto-waiting, waitUntil for custom conditions, Mocha-structured specs, page objects, parallel execution via maxInstances and multiple capabilities, and service wiring (visual regression, Appium for mobile). Trigger it when a repo contains @wdio/cli in devDependencies, a wdio.conf.* file, or the user asks for WebdriverIO/WDIO tests.
$('button').click() retries until the element is interactable (governed by waitforTimeout). browser.pause() in committed code is a bug, not a fix.$ returns a chainable element, not a handle. Re-locating happens on each command, so stale-element errors are rare. Store the selector chain, never an awaited snapshot, in page objects.button=Submit, *=partial), then data-testid via [data-testid="x"], then CSS. Reach for XPath only for parent-axis traversal.maxInstances + the capabilities array fan out across browsers; specs must not share accounts or mutable server state.@wdio/visual-service), Appium (@wdio/appium-service), and Selenium Grid wiring belong in services:, not hand-rolled in hooks.npm init wdio@latest . # interactive scaffold
# or manual:
npm install --save-dev @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/spec-reporter tsx
// wdio.conf.ts
import type { Options } from '@wdio/types';
export const config: Options.Testrunner = {
runner: 'local',
specs: ['./test/specs/**/*.ts'],
maxInstances: 5,
capabilities: [
{
browserName: 'chrome',
'goog:chromeOptions': {
args: process.env.CI ? ['--headless=new', '--disable-gpu', '--window-size=1366,900'] : [],
},
},
],
logLevel: 'warn',
baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
waitforTimeout: 10_000, // default $ auto-wait budget
connectionRetryTimeout: 120_000,
framework: 'mocha',
reporters: ['spec'],
mochaOpts: { ui: 'bdd', timeout: 60_000 },
// Fail fast in CI, keep full runs locally
bail: process.env.CI ? 1 : 0,
afterTest: async function (_test, _context, { passed }) {
if (!passed) {
await browser.takeScreenshot(); // attached to the runner log dir
}
},
};
// test/specs/login.spec.ts
import { browser, $, expect } from '@wdio/globals';
describe('login', () => {
beforeEach(async () => {
await browser.url('/login'); // resolves against baseUrl
});
it('signs in with valid credentials', async () => {
await $('[data-testid="email"]').setValue('[email protected]');
await $('[data-testid="password"]').setValue('s3cret!');
await $('button=Sign in').click(); // text selector, auto-waits
// expect-webdriverio assertions retry until timeout — no manual waits
await expect($('h1')).toHaveText('Dashboard');
await expect(browser).toHaveUrl(expect.stringContaining('/dashboard'));
});
it('shows a validation error for a bad password', async () => {
await $('[data-testid="email"]').setValue('[email protected]');
await $('[data-testid="password"]').setValue('wrong');
await $('button=Sign in').click();
const alert = $('[role="alert"]');
await expect(alert).toBeDisplayed();
await expect(alert).toHaveText(expect.stringContaining('Invalid credentials'));
});
});
$$ for collections:
const rows = $$('[data-testid="cart-row"]');
await expect(rows).toBeElementsArrayOfSize(3);
const titles = await rows.map((row) => row.$('.title').getText());
Use only when no built-in matcher fits (e.g., polling app state):
await browser.waitUntil(
async () => (await $('[data-testid="job-status"]').getText()) === 'COMPLETE',
{
timeout: 30_000,
interval: 500,
timeoutMsg: 'job never reached COMPLETE',
},
);
// test/pageobjects/login.page.ts
import { $, browser } from '@wdio/globals';
class LoginPage {
// getters return fresh chainable selectors — never cache awaited elements
get email() { return $('[data-testid="email"]'); }
get password() { return $('[data-testid="password"]'); }
get submit() { return $('button=Sign in'); }
async open() {
await browser.url('/login');
}
async login(email: string, password: string) {
await this.email.setValue(email);
await this.password.setValue(password);
await this.submit.click();
}
}
export default new LoginPage();
// usage
import LoginPage from '../pageobjects/login.page';
it('logs in', async () => {
await LoginPage.open();
await LoginPage.login('[email protected]', 's3cret!');
await expect($('h1')).toHaveText('Dashboard');
});
// wdio.conf.ts (excerpt)
maxInstances: 6,
capabilities: [
{ browserName: 'chrome', 'goog:chromeOptions': { args: ['--headless=new'] } },
{ browserName: 'firefox', 'moz:firefoxOptions': { args: ['-headless'] }, maxInstances: 2 },
],
Each spec file runs in its own worker; maxInstances caps concurrency globally, the per-capability maxInstances caps per browser. Shard further in CI with --spec globs per job.
// visual regression
// npm i -D @wdio/visual-service
services: [['visual', {
baselineFolder: './test/baseline',
screenshotPath: './test/screenshots',
blockOutStatusBar: true,
}]],
// in a spec
await expect(browser).toMatchFullPageSnapshot('dashboard', { misMatchTolerance: 0.2 });
// Appium mobile (native or mobile web)
// npm i -D @wdio/appium-service
services: ['appium'],
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'Pixel_8_API_34',
'appium:app': './apps/app-release.apk',
}],
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run start:test & # app under test
- run: npx wait-on http://localhost:3000
- run: npx wdio run wdio.conf.ts
env: { CI: 'true' }
- uses: actions/upload-artifact@v4
if: failure()
with: { name: wdio-screenshots, path: ./test/screenshots }
waitforTimeout at 10-15s; raise per-call ({ timeout } arg) for known-slow flows instead of globally.expect-webdriverio matchers (toHaveText, toBeDisplayed) — they retry; bare getText() + chai does not.before hooks, not UI click-throughs.checkout-applies-coupon.spec.ts, not test1.spec.ts.browser.pause(3000) anywhere in committed code — replace with a matcher or waitUntil.const el = await $(sel) in a variable across navigations — re-locate via getters.wdio.web.conf.ts / wdio.mobile.conf.ts sharing a base.wdio.conf.ts|js or @wdio/cli dependencydevelopment
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.