webdriverio-testing
Authors WebdriverIO E2E tests - `npm init wdio@latest` scaffolding, services architecture (sauce, browserstack, appium, devtools), reporters (spec, allure, junit), built-in Mocha/Jasmine/Cucumber framework integrations. WebdriverIO sits between Selenium (W3C protocol) and Playwright (modern API) - Selenium-protocol-compatible with rich plugin ecosystem. Use when the team needs WebDriver protocol + service-based device-farm integration.
Install with skills.sh (any agent)
npx skills add testland/qa --skill webdriverio-testingwebdriverio-testing
Overview
WebdriverIO (wdio) is a JavaScript / TypeScript E2E framework built on the W3C WebDriver protocol. It differentiates from Selenium by:
When to use
For pure-modern E2E, Playwright is simpler. For non-WebDriver architecture, Cypress.
Step 1 - Scaffold
npm init wdio@latestInteractive prompts pick:
What lands: wdio.conf.ts + tests/specs/*.e2e.ts + package.json updates.
Step 2 - Configure
// wdio.conf.ts
export const config: WebdriverIO.Config = {
runner: 'local',
specs: ['./tests/specs/**/*.e2e.ts'],
exclude: [],
maxInstances: 4,
capabilities: [
{
browserName: 'chrome',
'goog:chromeOptions': { args: ['--headless=new'] },
},
{
browserName: 'firefox',
'moz:firefoxOptions': { args: ['-headless'] },
},
],
baseUrl: 'http://localhost:3000',
services: ['chromedriver', 'geckodriver'],
framework: 'mocha',
reporters: ['spec', ['junit', { outputDir: 'reports/junit' }]],
mochaOpts: { ui: 'bdd', timeout: 30000 },
};maxInstances: 4 runs 4 specs in parallel.
Step 3 - Author a test (Mocha)
// tests/specs/checkout.e2e.ts
import { browser, $ } from '@wdio/globals';
import { expect } from 'chai';
describe('Checkout flow', () => {
beforeEach(async () => {
await browser.url('/login');
await $('[data-testid=email]').setValue('user@example.com');
await $('[data-testid=password]').setValue('pwd');
await $('button[type=submit]').click();
await expect($('h1=Welcome')).toBeDisplayed();
});
it('completes checkout', async () => {
await browser.url('/products/BOOK-001');
await $('[data-testid=add-to-cart]').click();
await expect($('[data-testid=cart-count]')).toHaveText('1');
await browser.url('/checkout');
await $('[name=card]').setValue('4242 4242 4242 4242');
await $('button=Place order').click();
await expect($('h1=Order confirmed')).toBeDisplayed();
});
});The $ selector returns a wdio element (Promise-wrapped); methods auto-wait. WDIO selectors have shortcuts:
Step 4 - Services
services: [
// Local browser drivers
'chromedriver',
'geckodriver',
// Cloud device farms
['browserstack', { user: 'USER', key: 'KEY' }],
['sauce', { user: 'USER', key: 'KEY' }],
// Mobile
['appium', { command: 'appium', args: { port: 4723 } }],
// DevTools (Puppeteer-style fast browser)
'devtools',
];Services handle setup / teardown; tests connect via the configured capability.
Step 5 - Run
# Run all specs
npx wdio run wdio.conf.ts
# Specific spec
npx wdio run wdio.conf.ts --spec ./tests/specs/checkout.e2e.ts
# Watch mode
npx wdio run wdio.conf.ts --watchStep 6 - Cucumber framework (BDD)
// wdio.conf.ts
framework: 'cucumber',
specs: ['./features/**/*.feature'],
cucumberOpts: {
require: ['./features/step-definitions/**/*.ts'],
backtrace: false,
requireModule: ['ts-node/register'],
timeout: 60000,
},# features/checkout.feature
Feature: Checkout
Scenario: Complete a successful checkout
Given I am logged in as "user@example.com"
When I add "BOOK-001" to my cart
And I complete checkout
Then I see the order confirmation// features/step-definitions/checkout.steps.ts
import { Given, When, Then } from '@wdio/cucumber-framework';
import { $ } from '@wdio/globals';
Given(/^I am logged in as "(.+)"$/, async (email) => {
await browser.url('/login');
await $('[data-testid=email]').setValue(email);
await $('[data-testid=password]').setValue('pwd');
await $('button[type=submit]').click();
});
// ... etc.Pairs with cucumber-testing (in the qa-bdd plugin) conventions for the Gherkin layer.
Step 7 - CI integration
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- run: npx wdio run wdio.conf.ts
- uses: actions/upload-artifact@v4
if: always()
with:
name: wdio-reports
path: reports/JUnit XML in reports/junit/ feeds junit-xml-analysis (in the qa-test-reporting plugin).
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Forgetting await on commands | Returns a Promise; subsequent steps race. | Always await (TS strict mode helps catch). |
browser.pause(2000) | Flaky; defeats wdio's auto-waits. | Trust assertion auto-waits; explicit waits when needed via waitFor*. |
Massive wdio.conf.ts | Hard to navigate; merge conflicts. | Split into env-specific configs; share via spread. |
| Mixing services that conflict (chromedriver + selenium-standalone) | Driver conflict. | Pick one approach. |
| Running mobile + web in same wdio.conf.ts | Tightly coupled config; hard to maintain. | Separate configs per stack. |
Limitations
References
Related skills
browserstack-automate
Author and run E2E tests on BrowserStack Automate - cloud grid covering 3000+ real device + browser combinations. Covers BROWSERSTACK_USERNAME + ACCESS_KEY auth, hub URL https://hub-cloud.browserstack.com/wd/hub, W3C capabilities + bstack:options (projectName, buildName, sessionName), BrowserStackLocal for testing against localhost / internal environments, parallel session limits, and CI integration. Use for cross-browser regression on real devices + browsers - distinct from running a single test framework locally, and from a matrix runner limited to the browser engines bundled on the local machine.
cypress-testing
Authors and improves Cypress E2E tests - installs Cypress, configures `cypress.config.ts`, authors `cy.*` command chains, refactors existing specs (`cy.wait(ms)` sleeps into assertions, repeated flows into `cy.session` custom commands), and debugs with the time-travel GUI; Cypress Cloud for parallel runs and recording. Use for both greenfield test authoring and improving hand-written specs already in the codebase. For automated refactor of raw Cypress Studio recordings specifically, use a dedicated codegen-review pass.
lambdatest-automate
Author and run E2E tests on LambdaTest - cloud grid for cross-browser + real-device testing with W3C WebDriver, Cypress, Playwright, and Appium support. Covers LT_USERNAME + LT_ACCESS_KEY auth, hub URL hub.lambdatest.com/wd/hub, W3C capabilities + LT:Options dict (build, name, project, smartUI, network, console, video, tunnel), LambdaTest Tunnel for internal apps. Use for cross-browser regression with LambdaTest as the cloud grid; complements BrowserStack + Sauce Labs.
playwright-testing
Authors and remediates Playwright E2E tests across Chromium, Firefox, WebKit - `npm init playwright@latest` scaffolding, `playwright.config.ts` browser projects, accessibility-first locators (`getByRole`/`getByLabelText`) to replace brittle CSS selectors, web-first assertions to eliminate `waitForTimeout` flakiness, Page Object pattern, trace viewer debugging, sharded parallel execution with merged HTML reporting, and GitHub Actions CI integration. Use for new test authoring, flakiness remediation, and CI setup; for reviewing codegen output specifically, use a dedicated codegen-review pass.
puppeteer-testing
Authors browser automation scripts using Puppeteer - Chrome / Chromium-only headless / headed automation, Page object via `page.*` API, network interception, PDF generation, screenshot capture, scraping. Distinct from Playwright (Puppeteer's older sibling, Chrome-only) - use Puppeteer for Chrome-only browser automation tasks (scraping, generating PDFs from HTML, screenshot pipelines) where Playwright's multi-browser support is unneeded overhead. Use when a project already depends on `puppeteer` / `puppeteer-core`, or when a Chrome-only script must emit PDFs, screenshots, or scraped data rather than assert on a page.
saucelabs-automate
Author and run E2E tests on Sauce Labs - cloud grid for cross-browser + real-device testing with W3C WebDriver, Cypress, Playwright, and Appium support. Covers SAUCE_USERNAME + SAUCE_ACCESS_KEY auth, regional hub URLs (us-west-1 / us-east-4 / eu-central-1), W3C capabilities, sauce:options dict (build, name, screenResolution, tunnelName), Sauce Connect Proxy for internal-environment testing. Use for cross-browser regression with Sauce Labs as the cloud grid; complements BrowserStack + LambdaTest as alternative providers.
selenium-testing
Authors Selenium WebDriver tests in any of its 6+ supported languages (Java, Python, JavaScript, C#, Ruby, Kotlin, PHP) - picks the appropriate language binding, configures WebDriver per browser, uses `By.*` locators with the team's accessibility-first preference where supported, runs locally + via Selenium Grid for distributed execution, parses results to JUnit XML. Use for legacy Selenium-locked stacks; new projects pick Playwright or Cypress.
testcafe-testing
Authors TestCafe E2E tests - `npm install testcafe`, fixture/test syntax, `Selector` API for queries, automatic-waits, no WebDriver required (TestCafe injects scripts via a proxy), supports any browser including remote / cloud farms. Use when the team prefers a no-WebDriver architecture and one of TestCafe's specific features (e.g., role-based auth) matters.
web-e2e-overview
Teaches web end-to-end testing from first principles: what browser-driven E2E covers and how it differs from unit and integration tests, a decision table for choosing between Playwright, Cypress, Selenium WebDriver, WebdriverIO, Puppeteer, TestCafe and the BrowserStack / Sauce Labs / LambdaTest cloud grids based on files already present in the repo, install and first-run commands for each, and the flakiness traps (fixed sleeps, CSS and XPath selectors, state shared between tests) that sink new suites. Use when a web application has no E2E coverage yet, when picking or replacing an E2E framework, or when a first browser test needs to go green end to end.