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
- Dual protocol support — WebDriver for full browser compatibility; Chrome DevTools Protocol for speed, network control, and performance metrics
- Mobile testing — Appium integration via
@wdio/appium-servicefor Android, iOS, and desktop apps - Rich plugin ecosystem — 100+ plugins for reporters, services, and framework integrations
- Framework agnostic — Works with Mocha, Jasmine, or Cucumber as the test runner
- TypeScript first — Full TypeScript support with no extra config
- Interactive REPL —
browser.debug()drops you into a live browser session during a test run for exploration
Architecture Diagram
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
browser.url(path)— Navigate to a URL (relative to baseUrl)$(selector)— Single element query (returns Element)$$(selector)— Multiple element query (returns Element[])element.setValue(text)— Clear and type into a fieldelement.click()— Click an elementelement.getText()— Get visible textelement.getAttribute(name)— Get an attribute valueexpect(element).toBeDisplayed()— Assert element is visible
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"]');
~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();
});
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