Getting Started with WebdriverIO in 2026

📘 What You'll Learn: By the end of this guide, you'll have a complete WebdriverIO test suite with TypeScript, Page Object Models, parallel execution, visual regression testing, and GitHub Actions CI/CD pipeline.

Why WebdriverIO in 2026?

WebdriverIO has evolved significantly. While Playwright and Cypress dominate headlines, WebdriverIO v9 offers unique advantages:

In this comprehensive guide, I'll walk you through setting up a modern WebdriverIO project from scratch. No fluff, just practical steps you can follow in 30 minutes.

Table of Contents

Prerequisites

Before starting, ensure you have:

💡 Pro Tip: Use nvm (Node Version Manager) to easily switch between Node versions for different projects.

Step 1: Installation & Project Setup

Create Project Structure

terminal
# Create project directory
mkdir webdriverio-demo && cd webdriverio-demo

# Initialize npm project
npm init -y

# Install WebdriverIO CLI
npm install --save-dev @wdio/cli

# Run configuration wizard
npx wdio config

Configuration Wizard Selections

When prompted, choose these options for a modern setup:

Question Recommendation Why?
Where to execute tests? On my local machine Start simple, scale later
Which framework? Mocha Most flexible, familiar syntax
Do you want to use a compiler? TypeScript Type safety, better IntelliSense
Where are your tests? ./test/specs/**/*.ts Standard convention
Reporters? spec, allure spec for CLI, allure for reports
Services? chromedriver Auto-manages Chrome driver
⚠️ Common Mistake: Don't select "Cucumber" unless your team explicitly needs BDD. Mocha/Jasmine give more flexibility for technical users.

Install Additional Dependencies

terminal
# TypeScript essentials
npm install --save-dev typescript ts-node @types/node @types/mocha

# Assertion library
npm install --save-dev @wdio/globals expect-webdriverio

# Page Object support
npm install --save-dev @wdio/allure-reporter allure-commandline

Step 2: Configure TypeScript & Test Runner

Create tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "types": ["node", "@wdio/globals/types", "expect-webdriverio"],
    "outDir": "./dist",
    "rootDir": "./test"
  },
  "include": ["./test/**/*.ts"],
  "exclude": ["node_modules"]
}

Configure wdio.conf.ts

The wizard created wdio.conf.ts. Enhance it with these production-ready settings:

wdio.conf.ts (key sections)
export const config: Options.Testrunner = {
  // Test files
  specs: ['./test/specs/**/*.ts'],
  exclude: [],

  // Parallel execution
  maxInstances: 5,
  capabilities: [{
    browserName: 'chrome',
    'goog:chromeOptions': {
      args: [
        '--headless',  // Remove for debugging
        '--no-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
        '--window-size=1920,1080'
      ]
    },
    acceptInsecureCerts: true
  }],

  // Test runner
  logLevel: 'info',
  bail: 0,
  baseUrl: 'https://the-internet.herokuapp.com',
  waitforTimeout: 10000,
  connectionRetryTimeout: 120000,
  connectionRetryCount: 3,

  // Framework
  framework: 'mocha',
  mochaOpts: {
    ui: 'bdd',
    timeout: 60000,
    require: ['ts-node/register']
  },

  // Reporters
  reporters: [
    'spec',
    ['allure', {
      outputDir: 'allure-results',
      disableWebdriverStepsReporting: false,
      disableWebdriverScreenshotsReporting: false
    }]
  ],

  // Hooks
  beforeTest: async function (test) {
    await browser.maximizeWindow();
  },

  afterTest: async function(test, context, { error }) {
    if (error) {
      await browser.takeScreenshot();
    }
  }
};
📋 Configuration Highlights:

Step 3: Write Your First Test

Create your first test file to verify everything works:

test/specs/example.test.ts
import { expect } from '@wdio/globals';

describe('WebdriverIO Demo', () => {
  it('should have correct page title', async () => {
    // Navigate to page
    await browser.url('https://the-internet.herokuapp.com');

    // Verify title
    const title = await browser.getTitle();
    expect(title).toBe('The Internet');
  });

  it('should click a link and navigate', async () => {
    await browser.url('https://the-internet.herokuapp.com');

    // Find and click link
    const link = await $('a[href="/login"]');
    await link.click();

    // Verify navigation
    await expect(browser).toHaveUrl(
      'https://the-internet.herokuapp.com/login'
    );
  });

  it('should fill a form and submit', async () => {
    await browser.url('https://the-internet.herokuapp.com/login');

    // Fill form
    await $('#username').setValue('tomsmith');
    await $('#password').setValue('SuperSecretPassword!');
    await $('button[type="submit"]').click();

    // Verify success message
    const successMessage = await $('#flash');
    await expect(successMessage).toHaveTextContaining(
      'You logged into a secure area!'
    );
  });
});

Run Your First Test

terminal
# Run tests
npx wdio run wdio.conf.ts

# Generate Allure report
npm install -g allure-commandline
allure generate allure-results --clean && allure open

Expected output:

output
WebdriverIO Demo
    ✓ should have correct page title (1.2s)
    ✓ should click a link and navigate (0.8s)
    ✓ should fill a form and submit (1.5s)

3 passing (3.5s)

Step 4: Implement Page Object Model

Avoid code duplication and improve maintainability with Page Objects:

Create Base Page Class

test/pageobjects/BasePage.ts
export default class BasePage {
  /**
   * Open a page with relative path
   */
  public async open(path: string) {
    await browser.url(path);
  }

  /**
   * Wait for element to be displayed
   */
  public async waitForElement(
    selector: string,
    timeout: number = 10000
  ): Promise {
    const element = await $(selector);
    await element.waitForDisplayed({ timeout });
  }

  /**
   * Get element text
   */
  public async getText(selector: string): Promise {
    const element = await $(selector);
    return await element.getText();
  }

  /**
   * Check if element exists
   */
  public async isElementDisplayed(selector: string): Promise {
    const element = await $(selector);
    return await element.isDisplayed();
  }
}

Create Login Page Object

test/pageobjects/LoginPage.ts
import BasePage from './BasePage';

class LoginPage extends BasePage {
  // Selectors (using getters for lazy evaluation)
  get usernameInput() { return $('#username'); }
  get passwordInput() { return $('#password'); }
  get submitButton() { return $('button[type="submit"]'); }
  get flashMessage() { return $('#flash'); }

  /**
   * Navigate to login page
   */
  async open() {
    await super.open('/login');
  }

  /**
   * Perform login
   */
  async login(username: string, password: string) {
    await this.usernameInput.setValue(username);
    await this.passwordInput.setValue(password);
    await this.submitButton.click();
  }

  /**
   * Get flash message text
   */
  async getFlashMessage(): Promise {
    await this.flashMessage.waitForDisplayed();
    return await this.flashMessage.getText();
  }

  /**
   * Check if logged in successfully
   */
  async isLoginSuccessful(): Promise {
    const message = await this.getFlashMessage();
    return message.includes('You logged into a secure area!');
  }
}

export default new LoginPage();

Use Page Object in Tests

test/specs/login.test.ts
import { expect } from '@wdio/globals';
import LoginPage from '../pageobjects/LoginPage';

describe('Login Feature', () => {
  beforeEach(async () => {
    await LoginPage.open();
  });

  it('should login with valid credentials', async () => {
    await LoginPage.login('tomsmith', 'SuperSecretPassword!');

    const isSuccessful = await LoginPage.isLoginSuccessful();
    expect(isSuccessful).toBe(true);
  });

  it('should show error for invalid credentials', async () => {
    await LoginPage.login('invalid', 'wrong');

    const message = await LoginPage.getFlashMessage();
    expect(message).toContain('Your username is invalid!');
  });

  it('should show error for empty fields', async () => {
    await LoginPage.submitButton.click();

    const message = await LoginPage.getFlashMessage();
    expect(message).toContain('Your username is invalid!');
  });
});
🚀 Pro Tip: Use LocatorLab to auto-generate Page Object classes. Scan a page, click "Generate POM", and get production-ready code in seconds!

Step 5: Parallel Execution & Optimization

Configure Parallel Execution

Speed up test execution by running tests in parallel:

wdio.conf.ts
export const config: Options.Testrunner = {
  // ...

  // Run 5 browser instances in parallel
  maxInstances: 5,

  // Per-capability parallelism
  capabilities: [{
    browserName: 'chrome',
    maxInstances: 5, // 5 Chrome instances
    'goog:chromeOptions': {
      args: ['--headless', '--disable-gpu']
    }
  }],

  // Spec-level parallelism (default)
  specs: [
    './test/specs/login.test.ts',
    './test/specs/checkout.test.ts',
    './test/specs/profile.test.ts'
  ],

  // ...
};

Performance Comparison

Test Suite Size Sequential Parallel (5 workers) Speedup
50 tests 12 min 30s 3 min 15s 3.8x faster
100 tests 25 min 00s 6 min 20s 3.9x faster
200 tests 50 min 00s 12 min 45s 3.9x faster

Optimization Tips

optimization-tips.ts
// ❌ BAD: Sequential waits
await element1.waitForDisplayed();
await element2.waitForDisplayed();
await element3.waitForDisplayed();
// Total: 9 seconds (3s each)

// ✅ GOOD: Parallel waits
await Promise.all([
  element1.waitForDisplayed(),
  element2.waitForDisplayed(),
  element3.waitForDisplayed()
]);
// Total: 3 seconds (parallel)

// ❌ BAD: Multiple separate actions
await input.setValue('john@example.com');
await button.click();
await alert.waitForDisplayed();
// Total: 5 seconds

// ✅ GOOD: Chain actions
await input.setValue('john@example.com');
await button.click();
await browser.waitUntil(
  async () => await alert.isDisplayed(),
  { timeout: 5000, timeoutMsg: 'Alert not displayed' }
);
// Total: 2 seconds (optimized waits)

Step 6: Visual Regression Testing

Catch visual bugs that functional tests miss:

Install Visual Testing Plugin

terminal
npm install --save-dev @wdio/visual-service

Configure Visual Service

wdio.conf.ts
export const config: Options.Testrunner = {
  // ...

  services: [
    'chromedriver',
    [
      'visual',
      {
        baselineFolder: './test/visual/baseline',
        screenshotPath: './test/visual/screenshots',
        formatImageName: '{tag}-{browserName}-{width}x{height}',
        savePerInstance: true,
        autoSaveBaseline: true,
        blockOutStatusBar: true,
        blockOutToolBar: true
      }
    ]
  ],

  // ...
};

Visual Regression Test Example

test/specs/visual.test.ts
import { expect } from '@wdio/globals';

describe('Visual Regression Tests', () => {
  it('should match homepage screenshot', async () => {
    await browser.url('https://the-internet.herokuapp.com');

    // Take screenshot and compare to baseline
    await expect(
      await browser.checkFullPageScreen('homepage')
    ).toEqual(0); // 0% difference
  });

  it('should match login form element', async () => {
    await browser.url('https://the-internet.herokuapp.com/login');

    const loginForm = await $('#login');

    // Compare specific element
    await expect(
      await browser.checkElement(loginForm, 'login-form')
    ).toBeLessThan(5); // Allow 5% difference
  });

  it('should detect CSS changes', async () => {
    await browser.url('https://the-internet.herokuapp.com');

    // This will fail if button styling changes
    const button = await $('a[href="/login"]');
    await expect(
      await browser.checkElement(button, 'login-button')
    ).toEqual(0);
  });
});
⚠️ Important: First test run creates baseline images. Subsequent runs compare against baselines. Review visual diffs carefully before updating baselines - not all changes are bugs!

Step 7: CI/CD with GitHub Actions

Automate test execution on every commit and pull request:

.github/workflows/test.yml
name: WebdriverIO Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run WebdriverIO tests
        run: npm test

      - name: Generate Allure Report
        if: always()
        run: |
          npm install -g allure-commandline
          allure generate allure-results --clean -o allure-report

      - name: Upload Allure Report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: allure-report
          path: allure-report
          retention-days: 30

      - name: Upload Screenshots on Failure
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: screenshots
          path: screenshots
          retention-days: 7

      - name: Comment on PR with Results
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '✅ WebdriverIO tests passed! View [Allure Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
            })

Add npm scripts

package.json
{
  "scripts": {
    "test": "wdio run wdio.conf.ts",
    "test:headed": "HEADLESS=false wdio run wdio.conf.ts",
    "test:spec": "wdio run wdio.conf.ts --spec",
    "report": "allure generate allure-results --clean && allure open",
    "report:generate": "allure generate allure-results --clean -o allure-report",
    "clean": "rm -rf allure-results allure-report screenshots"
  }
}

Best Practices & Tips

1. Use Custom Commands for Reusability

test/utils/customCommands.ts
// Add to wdio.conf.ts before hook
before: async function() {
  // Custom login command
  browser.addCommand('loginAs', async function(username: string, password: string) {
    await browser.url('/login');
    await $('#username').setValue(username);
    await $('#password').setValue(password);
    await $('button[type="submit"]').click();
    await $('#flash').waitForDisplayed();
  });

  // Custom wait for page load
  browser.addCommand('waitForPageLoad', async function() {
    await browser.waitUntil(
      () => browser.execute(() => document.readyState === 'complete'),
      { timeout: 10000, timeoutMsg: 'Page did not load' }
    );
  });
}

// Usage in tests
await browser.loginAs('tomsmith', 'SuperSecretPassword!');
await browser.waitForPageLoad();

2. Environment-Specific Configurations

test/config/environments.ts
export const environments = {
  dev: {
    baseUrl: 'https://dev.example.com',
    timeout: 30000
  },
  staging: {
    baseUrl: 'https://staging.example.com',
    timeout: 20000
  },
  production: {
    baseUrl: 'https://example.com',
    timeout: 10000
  }
};

// In wdio.conf.ts
const env = process.env.ENV || 'dev';
export const config: Options.Testrunner = {
  baseUrl: environments[env].baseUrl,
  waitforTimeout: environments[env].timeout,
  // ...
};

// Run tests: ENV=staging npm test

3. Smart Waiting Strategies

Scenario Recommended Approach Example
Element appears waitForDisplayed() await el.waitForDisplayed()
Element disappears waitForDisplayed({ reverse: true }) await loader.waitForDisplayed({ reverse: true })
Text changes waitUntil() await browser.waitUntil(() => el.getText() === 'Done')
AJAX complete Wait for network idle await browser.waitUntil(() => browser.execute(() => jQuery.active === 0))

4. Test Data Management

test/data/users.json
{
  "validUser": {
    "username": "tomsmith",
    "password": "SuperSecretPassword!"
  },
  "invalidUser": {
    "username": "invalid",
    "password": "wrong"
  },
  "adminUser": {
    "username": "admin",
    "password": "admin123"
  }
}
test/specs/data-driven.test.ts
import users from '../data/users.json';
import LoginPage from '../pageobjects/LoginPage';

describe('Data-Driven Login Tests', () => {
  Object.keys(users).forEach((userType) => {
    it(`should handle ${userType}`, async () => {
      const user = users[userType];
      await LoginPage.open();
      await LoginPage.login(user.username, user.password);

      const message = await LoginPage.getFlashMessage();
      // Add assertions based on user type
    });
  });
});

Conclusion: Your WebdriverIO Journey Starts Here

Congratulations! You now have a production-ready WebdriverIO test suite with:

🚀 Next Steps:
  1. Clone the WebdriverIO examples repo
  2. Install LocatorLab for rapid Page Object generation
  3. Join the WebdriverIO Discord community
  4. Read the official WebdriverIO documentation

Common Questions

Q: Should I choose WebdriverIO over Playwright/Cypress?

Choose WebdriverIO if you need: (1) True cross-browser support including Safari, (2) Mobile app testing with Appium, (3) Flexibility to switch between WebDriver/DevTools protocols. Choose Playwright for modern web apps with auto-waiting and faster execution. Choose Cypress for developer-friendly DX and time-travel debugging.

Q: How do I debug tests locally?

terminal
# Remove --headless flag in wdio.conf.ts, then:
npm run test:headed

# Or use VS Code debugger with this launch.json:
{
  "type": "node",
  "request": "launch",
  "name": "WebdriverIO",
  "program": "${workspaceFolder}/node_modules/@wdio/cli/bin/wdio",
  "args": ["wdio.conf.ts"],
  "console": "integratedTerminal"
}

Q: How do I handle flaky tests?

Use smart waits (waitForDisplayed()), avoid hard waits (browser.pause()), increase timeouts for slow pages, implement retry logic with rerunFlaky: 2 in mocha options, and use LocatorLab's locator health monitoring to catch fragile selectors early.

Ready to build bulletproof test automation? Start your WebdriverIO journey today!

About LocatorLab

LocatorLab is a complete test automation toolkit trusted by QA engineers. Our Chrome extension helps teams write better tests faster with intelligent locator capture, health monitoring, and automated Page Object generation supporting all major frameworks including WebdriverIO.

Share This Article