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.
Install to Claude Code
npx -y skills add PramodDutta/qaskills --skill webdriverio-e2e --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Webdriverio E2e?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pramoddutta-webdriverio-e2e)More formats (shields.io, HTML) on the badges page.
---
name: WebdriverIO E2E Testing
description: 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.
version: 1.0.0
author: thetestingacademy
license: MIT
tags: [webdriverio, wdio, e2e, selenium, appium, mocha, page-object, parallel, typescript]
testingTypes: [e2e, integration]
frameworks: [webdriverio, mocha, appium]
languages: [typescript, javascript]
domains: [web, mobile]
agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp]
---
# WebdriverIO E2E Testing
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.
## Core Principles
1. **WDIO commands auto-wait — do not add manual pauses.** `$('button').click()` retries until the element is interactable (governed by `waitforTimeout`). `browser.pause()` in committed code is a bug, not a fix.
2. **`$` 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.
3. **Use WDIO selector strengths in priority order:** accessibility-ish text selectors (`button=Submit`, `*=partial`), then `data-testid` via `[data-testid="x"]`, then CSS. Reach for XPath only for parent-axis traversal.
4. **One spec = one user-visible behavior.** WDIO workers isolate per spec file; long multi-journey specs serialize your suite and hide which behavior broke.
5. **Parallelism is config, not code.** `maxInstances` + the `capabilities` array fan out across browsers; specs must not share accounts or mutable server state.
6. **Services do the heavy lifting.** Visual diffs (`@wdio/visual-service`), Appium (`@wdio/appium-service`), and Selenium Grid wiring belong in `services:`, not hand-rolled in hooks.
## Setup
```bash
npm init wdio@latest . # interactive scaffold
# or manual:
npm install --save-dev @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/spec-reporter tsx
```
```typescript
// 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
}
},
};
```
## Selectors and Auto-Wait
```typescript
// 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('user@example.com');
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('user@example.com');
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:
```typescript
const rows = $$('[data-testid="cart-row"]');
await expect(rows).toBeElementsArrayOfSize(3);
const titles = await rows.map((row) => row.$('.title').getText());
```
## waitUntil for Custom Conditions
Use only when no built-in matcher fits (e.g., polling app state):
```typescript
await browser.waitUntil(
async () => (await $('[data-testid="job-status"]').getText()) === 'COMPLETE',
{
timeout: 30_000,
interval: 500,
timeoutMsg: 'job never reached COMPLETE',
},
);
```
## Page Objects
```typescript
// 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();
```
```typescript
// usage
import LoginPage from '../pageobjects/login.page';
it('logs in', async () => {
await LoginPage.open();
await LoginPage.login('user@example.com', 's3cret!');
await expect($('h1')).toHaveText('Dashboard');
});
```
## Parallel + Multi-Browser Capabilities
```typescript
// 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.
## Services
```typescript
// visual regression
// npm i -D @wdio/visual-service
services: [['visual', {
baselineFolder: './test/baseline',
screenshotPath: './test/screenshots',
blockOutStatusBar: true,
}]],
```
```typescript
// in a spec
await expect(browser).toMatchFullPageSnapshot('dashboard', { misMatchTolerance: 0.2 });
```
```typescript
// 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',
}],
```
## CI (GitHub Actions)
```yaml
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 }
```
## Best Practices
- Keep `waitforTimeout` at 10-15s; raise per-call (`{ timeout }` arg) for known-slow flows instead of globally.
- Use `expect-webdriverio` matchers (`toHaveText`, `toBeDisplayed`) — they retry; bare `getText()` + chai does not.
- Reset state via API calls in `before` hooks, not UI click-throughs.
- Name specs by behavior: `checkout-applies-coupon.spec.ts`, not `test1.spec.ts`.
- Pin browser versions in CI (chrome-for-testing) to stop drive-by breakage.
## Anti-Patterns
1. `browser.pause(3000)` anywhere in committed code — replace with a matcher or `waitUntil`.
2. Caching `const el = await $(sel)` in a variable across navigations — re-locate via getters.
3. One mega-spec covering login→cart→checkout→refund — kills parallelism and triage.
4. Asserting with non-retrying chai on async UI — flake factory.
5. Driving Appium and desktop web in one capability set without separating configs — split `wdio.web.conf.ts` / `wdio.mobile.conf.ts` sharing a base.
## When to Trigger This Skill
- "Write WebdriverIO tests for X" / "add a wdio spec"
- Repo has `wdio.conf.ts|js` or `@wdio/cli` dependency
- Migrating Selenium JS or Protractor suites to WDIO
- Setting up visual regression or Appium through WDIO services
Scanned 9/5/2026
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!