webdriverio-e2e-testing

star 148

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.

PramodDutta By PramodDutta schedule Updated 6/12/2026

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

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
    }
  },
};

Selectors and Auto-Wait

// 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:

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):

await browser.waitUntil(
  async () => (await $('[data-testid="job-status"]').getText()) === 'COMPLETE',
  {
    timeout: 30_000,
    interval: 500,
    timeoutMsg: 'job never reached COMPLETE',
  },
);

Page Objects

// 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('user@example.com', 's3cret!');
  await expect($('h1')).toHaveText('Dashboard');
});

Parallel + Multi-Browser Capabilities

// 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

// 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',
}],

CI (GitHub Actions)

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
Install via CLI
npx skills add https://github.com/PramodDutta/qaskills --skill webdriverio-e2e-testing
Repository Details
star Stars 148
call_split Forks 15
navigation Branch main
article Path SKILL.md
More from Creator