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 dependencytesting
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
testing
Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
development
Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.
development
Test Node.js HTTP APIs in-process with SuperTest — request(app) without binding a port, chained .expect assertions, auth headers, JSON body validation, and Jest integration with proper async/await patterns.