DEV Community

Alex Spinov
Alex Spinov

Posted on

WebdriverIO Has a Free API — Browser + Mobile Testing in One Framework

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
Enter fullscreen mode Exit fullscreen mode

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");
  });
});
Enter fullscreen mode Exit fullscreen mode

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");
  });
});
Enter fullscreen mode Exit fullscreen mode

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");
  });
});
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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");
  });
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)