WebdriverIO (WDIO) is a browser and mobile automation framework for Node.js. It's built on the WebDriver protocol — the same standard that powers Selenium — but wraps it in a modern, developer-friendly API. If you're building a test suite for a large enterprise product, or you need to test both web and native mobile apps, WDIO is worth serious consideration.
What WebdriverIO Is
At its core, WDIO is a JavaScript/TypeScript test runner that:
- Talks to browsers via the WebDriver BiDi protocol by default since v9 (falls back to classic WebDriver, or CDP for Chrome/Edge, when needed)
- Provides a clean, chainable API for browser interaction
- Has a built-in test runner with parallelism, retries, and reporters
- Supports web, native iOS, and native Android testing through Appium
- Integrates with all major testing frameworks: Mocha, Jasmine, and Cucumber
It's been around since 2012 and is actively maintained by a large community.
WDIO vs Playwright vs Cypress
All three are legitimate choices. Here's the honest comparison:
| Feature | WebdriverIO | Playwright | Cypress |
|---|---|---|---|
| Language | JS/TS only | JS/TS, Python, Java, C# | JS/TS only |
| Protocol | WebDriver BiDi (v9+), classic WebDriver, or CDP | CDP (proprietary) | In-browser |
| Mobile testing | ✅ (Appium) | ❌ | ❌ |
| Multi-tab | ✅ | ✅ | ⚠️ Limited |
| Cross-origin | ✅ | ✅ (via cy.origin()) | ⚠️ Limited |
| Speed | Moderate | Fast | Fast (headed) |
| Learning curve | Steeper | Moderate | Gentle |
| Enterprise features | Excellent | Good | Good |
Choose WebdriverIO when:
- You need mobile testing (iOS/Android) alongside web
- Your team works in a Cucumber/BDD workflow
- You're on a Selenium/Java background and want to modernise
- You need extensive cross-browser coverage including Safari on real devices
- Your organisation already uses WDIO
Choose Playwright when:
- You need multi-language support (Python, Java, C#)
- Speed is a top priority
- You want the most modern CDP-based approach
Choose Cypress when:
- Your stack is purely JavaScript frontend
- Developer experience and fast feedback loops are most important
The WDIO Architecture
WDIO has several components:
Your Test File
↓
WDIO Test Runner ← orchestrates workers, retries, reports
↓
WebdriverIO API ← browser.$(), browser.url(), etc.
↓
WebDriver BiDi (default) / classic WebDriver / CDP
↓
Browser Driver ← chromedriver, geckodriver, safaridriver
↓
Browser ← Chrome, Firefox, Safari, Edge
For mobile testing, Appium sits between WDIO and the mobile device, translating WebDriver commands to native gestures.
Why the protocol matters, not just which one is used
Since v9, WDIO automatically negotiates the WebDriver BiDi protocol for every new session instead of classic WebDriver. The difference isn't cosmetic: classic WebDriver is request-response only — your test asks, the browser answers — while BiDi is WebSocket-based and lets the browser push events to your session as they happen. That's what makes several newer WDIO features possible at all, including cross-browser request mocking (not just Chromium), fake timers for testing time-dependent code, and automatic dialog suppression instead of a hanging test waiting for you to manually accept a popup.
If you need to force classic WebDriver — an older Selenium Grid, a corporate proxy, a driver that hasn't caught up — there's an explicit opt-out capability:
capabilities: [
{
browserName: 'chrome',
'wdio:enforceWebDriverClassic': true,
},
],
You won't need this for anything in this tutorial, but it's worth knowing it exists if you're upgrading an older test suite.
Synchronous-Style Code
Modern WDIO (v9, current) uses async/await throughout — every browser command returns a promise, and you await it directly instead of chaining callbacks:
// WDIO style — reads synchronously
it('should login', async () => {
await browser.url('https://www.saucedemo.com')
await $('#user-name').setValue('standard_user')
await $('#password').setValue('secret_sauce')
await $('#login-button').click()
await expect(browser).toHaveUrl(expect.stringContaining('/inventory'))
})
The await is explicit, unlike Cypress. But WDIO also has automatic waiting built in — by default it waits up to 3 seconds for elements to become interactable.
Key Concepts
browser
browser is the global object for browser-level commands:
await browser.url('https://example.com') // navigate
await browser.getTitle() // get page title
await browser.getUrl() // get current URL
await browser.pause(1000) // wait (avoid this)
await browser.takeScreenshot() // screenshot
$ and $$
$ finds one element. $$ finds multiple. They accept CSS selectors:
const username = await $('#user-name')
const allItems = await $$('.inventory_item')
Or you can use browser.$() and browser.$$() — same thing.
Element Interactions
const input = await $('#user-name')
await input.setValue('standard_user')
await input.click()
await input.getText()
await input.getAttribute('placeholder')
await input.isDisplayed()
await input.waitForDisplayed({ timeout: 5000 })
What We'll Build
This tutorial uses SauceDemo throughout. Credentials:
standard_user/secret_sauce— normal userlocked_out_user/secret_sauce— locked out (useful for error testing)performance_glitch_user/secret_sauce— slow UI (useful for timeout testing)
We'll cover:
- Installation & Configuration — getting WDIO up and running
- Writing Tests — selectors and commands
- Assertions —
expect-webdriveriomatchers - Page Object Model — structuring tests for maintainability
- Advanced Interactions — browser control beyond the basics
- Flaky Tests — retry strategies and diagnosis
- CI/CD — running WDIO in GitHub Actions
Mobile testing with Appium is WDIO's other big use case (see above), but it's a large enough topic to deserve its own guide — this tutorial stays focused on web.
Let's start with installation.