Back to All Articles
Automation

WebdriverIO — Automation Testing Guide

Honnesh Muppala May 5, 2026 14 min read

What is WebdriverIO?

WebdriverIO (WDIO) is an open-source, Node.js-based test automation framework that supports both W3C WebDriver and the Chrome DevTools Protocol. What makes WebdriverIO unique in the automation landscape is its dual capability: it is equally capable of testing web browsers and native mobile applications via Appium integration — making it the most versatile framework for teams that need a single tool chain for web and mobile.

WebdriverIO is built around a rich plugin ecosystem. Its service architecture allows you to plug in browser drivers, cloud providers (BrowserStack, Sauce Labs), reporters, and Appium — all through configuration rather than custom plumbing. The synchronous API (using Node.js worker threads) lets you write tests without async/await boilerplate, making the code read almost like Selenium's synchronous Python API.

Key features

Architecture Diagram

Test Code (JS/TS)
describe / it
WDIO Test Runner
Mocha / Jasmine / Cucumber
WebDriver / DevTools
W3C or CDP protocol
Browser
Chrome / Firefox / Edge
|
Appium Server
Android / iOS / Desktop
WebdriverIO supports both web browser automation and mobile app testing from a single framework.

Installation: The WDIO Wizard

WebdriverIO provides an interactive setup wizard that configures the entire framework for you:

# Start the interactive configuration wizard
npm init wdio@latest

# The wizard asks:
# ? What type of testing would you like to do? (E2E Testing)
# ? Where is your automation backend? (Local)
# ? Which framework do you want to use? (Mocha)
# ? Do you want to use a compiler? (TypeScript or none)
# ? Where are your test specs located? ./test/specs/**/*.spec.ts
# ? Which reporter(s) do you want to use? (spec)
# ? Do you want to add a service to your test setup? (chromedriver)
# ? What is the base URL? (http://localhost)

# After wizard completes, run your tests:
npx wdio run wdio.conf.ts

The wizard generates a complete wdio.conf.ts, installs all required dependencies, and creates example test files. This is the fastest way to go from zero to running tests in any Node.js automation framework.

Writing Your First Test

WebdriverIO tests use a synchronous-style API where every command blocks until the browser completes it — no await keywords clutter the test body:

// test/specs/login.spec.ts
describe('Login page', () => {

    it('should log in with valid credentials', async () => {
        await browser.url('/login');

        const emailField = await $('[data-testid="email-input"]');
        const passwordField = await $('[data-testid="password-input"]');
        const loginBtn = await $('[data-testid="login-btn"]');

        await emailField.setValue('user@example.com');
        await passwordField.setValue('SecurePass123');
        await loginBtn.click();

        // Assert redirect to dashboard
        await expect(browser).toHaveUrl(expect.stringContaining('/dashboard'));
        const heading = await $('[data-testid="page-heading"]');
        await expect(heading).toBeDisplayed();
        await expect(heading).toHaveText('Dashboard');
    });

    it('should show error for wrong password', async () => {
        await browser.url('/login');

        await $('[data-testid="email-input"]').setValue('user@example.com');
        await $('[data-testid="password-input"]').setValue('WrongPass');
        await $('[data-testid="login-btn"]').click();

        const error = await $('[data-testid="error-message"]');
        await expect(error).toBeDisplayed();
        await expect(error).toHaveTextContaining('Invalid');
    });
});

Key commands

Selectors

WebdriverIO supports a wide range of selector strategies, including some that are unique to the framework:

// CSS selector (most common)
$('.submit-button')
$('#login-form')
$('[data-testid="submit-btn"]')

// XPath
$('//button[text()="Submit"]')

// Link text shorthand (WDIO-specific)
$('=Sign In')          // Exact link text
$('*=Sign')            // Partial link text

// Accessibility ID (used for Appium mobile)
$('~loginButton')      // ~accessibilityLabel for iOS / content-desc for Android

// Tag name
$('input')

// Role-based (using aria)
$('[role="button"][aria-label="Close"]')

// Chained element queries
const form = await $('#login-form');
const submitBtn = await form.$('button[type="submit"]');
Mobile Selector Tip from Virtusa: When building the Appium test suite at Virtusa, I found that ~accessibilityLabel selectors (the $('~id') shorthand in WDIO) were the most reliable locators for cross-platform mobile tests. Android uses content-desc and iOS uses the accessibility label — WDIO's ~ shorthand maps to the right attribute on each platform automatically, letting us write a single selector that works on both. This alone saved hundreds of lines of platform-branching code in our suite.

Mocha Test Structure

WebdriverIO pairs naturally with Mocha, giving you a full suite of hooks for setup and teardown:

describe('Product Checkout', () => {

    // Runs once before all tests in this describe block
    before(async () => {
        // Seed test data, log in via API, etc.
        await browser.url('/login');
        await $('#email').setValue('testuser@example.com');
        await $('#password').setValue('TestPass123');
        await $('button[type="submit"]').click();
    });

    // Runs before each individual test
    beforeEach(async () => {
        await browser.url('/products');
    });

    it('should add a product to the cart', async () => {
        await $('[data-testid="product-card"]:first-child button').click();
        const cartCount = await $('[data-testid="cart-count"]');
        await expect(cartCount).toHaveText('1');
    });

    it('should proceed to checkout', async () => {
        await $('[data-testid="cart-icon"]').click();
        await $('[data-testid="checkout-btn"]').click();
        await expect(browser).toHaveUrl(expect.stringContaining('/checkout'));
    });

    // Runs after each individual test
    afterEach(async () => {
        // Clear cart or reset state
    });

    // Runs once after all tests
    after(async () => {
        // Clean up test data
    });
});

Page Object Pattern

WDIO page objects use ES6 classes with get accessor properties for element definitions. This ensures elements are freshly queried each time (avoiding stale element references):

// test/pageobjects/login.page.ts
class LoginPage {
    // Element accessors — re-queried on each access
    get emailInput()    { return $('[data-testid="email-input"]'); }
    get passwordInput() { return $('[data-testid="password-input"]'); }
    get loginButton()   { return $('[data-testid="login-btn"]'); }
    get errorMessage()  { return $('[data-testid="error-message"]'); }
    get pageTitle()     { return $('[data-testid="page-title"]'); }

    async open() {
        await browser.url('/login');
    }

    async login(email: string, password: string) {
        await (await this.emailInput).setValue(email);
        await (await this.passwordInput).setValue(password);
        await (await this.loginButton).click();
    }

    async getErrorText(): Promise {
        return (await this.errorMessage).getText();
    }
}

export default new LoginPage();

// Usage in spec:
import LoginPage from '../pageobjects/login.page.ts';

it('logs in successfully', async () => {
    await LoginPage.open();
    await LoginPage.login('user@example.com', 'Pass123');
    await expect(browser).toHaveUrl(expect.stringContaining('/dashboard'));
});

Configuration: wdio.conf.ts

All WebdriverIO settings live in wdio.conf.ts. Here is a commented configuration for a typical web testing setup:

// wdio.conf.ts
export const config = {
    runner: 'local',
    baseUrl: 'http://localhost:3000',

    specs: ['./test/specs/**/*.spec.ts'],
    exclude: [],

    maxInstances: 5,           // Run up to 5 browsers in parallel

    capabilities: [{
        browserName: 'chrome',
        'goog:chromeOptions': {
            args: ['--headless', '--disable-gpu', '--window-size=1280,720']
        }
    }],

    logLevel: 'warn',
    waitforTimeout: 10000,      // Default wait for elements (ms)
    connectionRetryTimeout: 30000,
    connectionRetryCount: 3,

    framework: 'mocha',
    mochaOpts: {
        ui: 'bdd',
        timeout: 60000
    },

    reporters: [
        'spec',
        ['allure', {
            outputDir: 'allure-results',
            disableWebdriverStepsReporting: true
        }]
    ],

    services: ['chromedriver'],

    // Global hooks
    beforeSession: async () => { /* setup */ },
    afterSession: async () => { /* teardown */ },
};

Chrome DevTools Service

Add @wdio/devtools-service to unlock Chrome DevTools Protocol features directly in WDIO tests:

// wdio.conf.ts — add to services:
services: ['devtools'],

// In your test:
it('measures page performance', async () => {
    await browser.url('/');

    // Get Core Web Vitals
    const metrics = await browser.getMetrics();
    console.log('LCP:', metrics.largestContentfulPaint);
    console.log('CLS:', metrics.cumulativeLayoutShift);

    // Throttle network to simulate slow 3G
    await browser.throttleNetwork('Regular3G');
    await browser.url('/heavy-page');

    // Get page weight and resource timing
    const performance = await browser.getPageWeight();
    console.log('Page weight:', performance.requestCount);
});

it('checks code coverage', async () => {
    await browser.startTracing();
    await browser.url('/login');
    await $('[data-testid="login-btn"]').click();
    const trace = await browser.endTracing();
    // Analyse trace.traceCategories
});

Appium Integration for Mobile Testing

WebdriverIO's Appium integration is one of its strongest differentiators. The same test runner, the same page object pattern, and the same $/$$ API — just with different capabilities:

// wdio.conf.mobile.ts
export const config = {
    ...require('./wdio.conf').config,

    services: [
        ['appium', {
            command: 'appium',
            args: { relaxedSecurity: true }
        }]
    ],

    capabilities: [{
        platformName: 'Android',
        'appium:deviceName': 'Pixel_6_API_33',
        'appium:automationName': 'UiAutomator2',
        'appium:app': '/path/to/my-app.apk',
        'appium:appPackage': 'com.example.myapp',
        'appium:appActivity': '.MainActivity',
        'appium:noReset': false
    }]
};

// Mobile test (same $() API)
it('should display the home screen after login', async () => {
    await $('~usernameField').setValue('testuser');
    await $('~passwordField').setValue('Pass123');
    await $('~loginButton').click();
    await expect($('~homeScreenTitle')).toBeDisplayed();
});
From Experience at Virtusa: The most significant advantage of WebdriverIO for us at Virtusa was the ability to run web and Android tests from a single Node.js project — sharing page objects, test utilities, and CI configuration. Prior to migrating to WDIO, we maintained separate Selenium (Python) and Appium (Python) projects with duplicated helper code. Consolidating into WDIO reduced our framework maintenance overhead by roughly 40% and made onboarding new QA engineers significantly faster.

Reporters

WebdriverIO ships with a spec reporter built in. For richer reporting, add the Allure or JUnit reporter:

npm install --save-dev @wdio/allure-reporter @wdio/junit-reporter

# In wdio.conf.ts reporters array:
reporters: [
    'spec',
    ['allure', {
        outputDir: 'allure-results',
        disableWebdriverStepsReporting: true,
        disableWebdriverScreenshotsReporting: false
    }],
    ['junit', {
        outputDir: './junit-results',
        outputFileFormat: (opts) => `results-${opts.cid}.xml`
    }]
],

# Generate and open the Allure report:
npx allure generate allure-results --clean
npx allure open

Running Tests

# Run all tests
npx wdio run wdio.conf.ts

# Run a specific spec file
npx wdio run wdio.conf.ts --spec test/specs/login.spec.ts

# Run specs matching a pattern
npx wdio run wdio.conf.ts --spec "**/checkout*"

# Filter by Mocha grep (test name substring)
npx wdio run wdio.conf.ts --mochaOpts.grep "login"

# Run with a specific browser (override capabilities)
npx wdio run wdio.conf.ts --capabilities '[{"browserName":"firefox"}]'

# Run in watch mode (re-runs on file changes)
npx wdio run wdio.conf.ts --watch

GitHub Actions CI

# .github/workflows/wdio.yml
name: WebdriverIO Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  wdio-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Start application
        run: npm run start &
        env:
          NODE_ENV: test

      - name: Wait for server
        run: npx wait-on http://localhost:3000 --timeout 30000

      - name: Run WebdriverIO tests
        run: npx wdio run wdio.conf.ts

      - name: Upload Allure results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: allure-results
          path: allure-results/

Tool Comparison

Feature WebdriverIO Cypress Playwright Selenium
Language JavaScript / TypeScript JavaScript / TypeScript JS, TS, Python, Java, .NET Java, Python, JS, C#, Ruby
Mobile testing Yes — Appium built-in No Emulation only Via Appium (separate)
Browser support All (via WebDriver) Chrome, Firefox, Edge Chromium, Firefox, WebKit All major browsers
Test framework Mocha, Jasmine, Cucumber Built-in (Mocha-like) Built-in + pytest Any (via bindings)
DevTools Protocol Yes (devtools service) Partial Yes (CDP native) No
Learning curve Medium Low Low–Medium High
Ecosystem / plugins Very large (100+ packages) Large Growing fast Largest overall
Best for Web + mobile unified suite JS SPA testing Multi-browser, multi-language Enterprise, legacy projects

Back to Blog
From Experience — Virtusa: Leading a team of 270 testers at Virtusa, we standardised on Appium for real Android device testing and Selenium WebDriver for web regression. The biggest challenge wasn't the tooling — it was consistency across a team that size. We enforced a strict Page Object Model convention and a pre-merge locator review checklist. Within two sprints, flaky test rates dropped significantly and the team achieved a 20% efficiency gain across regression cycles. At that scale, test architecture decisions matter far more than individual test quality.