WebdriverIO is the next-gen browser and mobile testing framework for Node.js — with first-class support for Chrome DevTools, WebDriver BiDi, and Appium for mobile testing.
Why WebdriverIO?
- Multi-protocol — WebDriver, DevTools, and WebDriver BiDi support
- Mobile testing — built-in Appium integration (iOS + Android)
- Component testing — React, Vue, Svelte, Lit, Preact, SolidJS
- Visual regression — screenshot comparison built in
- AI-powered selectors — find elements using natural language
- Extensible — 100+ community plugins
Quick Start
# Interactive setup wizard
npm init wdio@latest
# Or manual install
npm install @wdio/cli
npx wdio config
Your First Test
// test/specs/login.test.js
describe("Login Page", () => {
it("should login with valid credentials", async () => {
await browser.url("/login");
await $("#email").setValue("user@example.com");
await $("#password").setValue("password123");
await $("button[type=submit]").click();
await expect(browser).toHaveUrl(expect.stringContaining("/dashboard"));
await expect($(".welcome-message")).toHaveText("Welcome back!");
});
it("should show error for invalid credentials", async () => {
await browser.url("/login");
await $("#email").setValue("wrong@example.com");
await $("#password").setValue("wrongpassword");
await $("button[type=submit]").click();
const error = await $(".error-message");
await expect(error).toBeDisplayed();
await expect(error).toHaveText("Invalid credentials");
});
});
Component Testing (React)
// test/component/Counter.test.jsx
import { render } from "@testing-library/react";
import Counter from "../../src/components/Counter";
describe("Counter Component", () => {
it("increments on click", async () => {
render(<Counter />);
const count = await $("[data-testid=count]");
await expect(count).toHaveText("0");
await $("[data-testid=increment]").click();
await expect(count).toHaveText("1");
await $("[data-testid=increment]").click();
await expect(count).toHaveText("2");
});
});
Mobile Testing (Appium Integration)
// wdio.conf.js — mobile configuration
export const config = {
runner: "local",
specs: ["./test/mobile/**/*.test.js"],
capabilities: [{
platformName: "Android",
"appium:deviceName": "Pixel_6",
"appium:app": "./app-release.apk",
"appium:automationName": "UiAutomator2",
}],
services: ["appium"],
};
// test/mobile/app.test.js
describe("Mobile App", () => {
it("should navigate to settings", async () => {
await $("~settings-button").click();
await expect($("~settings-title")).toHaveText("Settings");
});
it("should toggle dark mode", async () => {
await $("~dark-mode-toggle").click();
const body = await $("~app-container");
await expect(body).toHaveAttribute("data-theme", "dark");
});
});
Network Mocking
describe("API Mocking", () => {
it("mocks API responses", async () => {
// Mock the API before navigating
const mock = await browser.mock("**/api/users", { method: "get" });
mock.respond([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
await browser.url("/users");
const users = await $$(".user-card");
await expect(users).toBeElementsArrayOfSize(2);
await expect(users[0]).toHaveText(expect.stringContaining("Alice"));
});
it("simulates network errors", async () => {
const mock = await browser.mock("**/api/data");
mock.abort("Failed");
await browser.url("/data");
await expect($(".error-state")).toBeDisplayed();
});
});
Visual Regression Testing
describe("Visual Tests", () => {
it("matches homepage screenshot", async () => {
await browser.url("/");
// Compare full page
await expect(browser).toMatchFullPageSnapshot("homepage");
// Compare specific element
await expect($(".hero-section")).toMatchElementSnapshot("hero");
});
it("matches across viewports", async () => {
// Desktop
await browser.setWindowSize(1920, 1080);
await browser.url("/");
await expect(browser).toMatchFullPageSnapshot("homepage-desktop");
// Mobile
await browser.setWindowSize(375, 812);
await expect(browser).toMatchFullPageSnapshot("homepage-mobile");
});
});
Page Objects (Best Practice)
// test/pageobjects/login.page.js
class LoginPage {
get emailInput() { return $("#email"); }
get passwordInput() { return $("#password"); }
get submitBtn() { return $("button[type=submit]"); }
get errorMessage() { return $(".error-message"); }
async open() {
await browser.url("/login");
}
async login(email, password) {
await this.emailInput.setValue(email);
await this.passwordInput.setValue(password);
await this.submitBtn.click();
}
}
export default new LoginPage();
WebdriverIO vs Cypress vs Playwright
| Feature | WebdriverIO | Cypress | Playwright |
|---|---|---|---|
| Mobile testing | Built-in (Appium) | No | Experimental |
| Browsers | All + mobile | Chrome, FF, Edge | Chrome, FF, Safari |
| Protocol | WebDriver + DevTools | Custom | CDP + BiDi |
| Component test | Yes (6 frameworks) | Yes (React, Vue) | Yes |
| Visual regression | Built-in | Plugin | Plugin |
| Parallel | Built-in | Dashboard (paid) | Built-in |
Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.
Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.
Top comments (0)