On this page7 sections
CommandsIntermediate6-8 min reference

WebDriverIO Commands

A practical reference for WebdriverIO v8+ in TypeScript. The same APIs work in plain JavaScript with import-style differences.

Selectors

WebdriverIO's $() returns a single element; $$() returns an array. Both auto-detect the strategy from the selector syntax.

CSS

const submit  = await $('button.submit');
const items   = await $$('.item');                // array of elements
const first   = await $$('.item')[0];
const heading = await $('section h2:first-child');

ID

const username = await $('#username');

XPath — leading / or ( triggers XPath mode

const submitBtn = await $('//button[@type="submit"]');
const firstRow  = await $('(//table//tr)[2]');

Text — exact and partial

const exact   = await $('=Sign In');     // exact match anywhere on the page
const partial = await $('*=Sign');       // contains

Tag name

const button = await $('<button />');    // first <button>

ARIA

const btn = await $('aria/Submit');      // accessible name

Custom locator strategy

browser.addLocatorStrategy('test-id', (selector) =>
  document.querySelectorAll(`[data-testid="${selector}"]`));
 
const submit = await $('test-id=submit');
const all    = await $$('test-id=item');

Chained selectors

const submit = await $('form#checkout').$('button[type=submit]');
const rows   = await $('table.users').$$('tbody tr');

React component selectors (with @wdio/devtools-service)

const userCard = await browser.react$('UserCard',
  { props: { userId: 42 } });
const allCards = await browser.react$$('UserCard');

Actions

Pointer

await $('button').click();
await $('button').doubleClick();
await $('button').moveTo();              // hover
await $('row').click({ button: 'right' });

Form input

const email = await $('#email');
await email.setValue('ada@example.com');     // clears, then types
await email.addValue(' (test)');             // appends
await email.clearValue();

Select / dropdown

const country = await $('select#country');
await country.selectByVisibleText('United States');
await country.selectByIndex(2);
await country.selectByAttribute('value', 'US');

Scroll

await $('footer').scrollIntoView();
await $('footer').scrollIntoView({ block: 'center' });

Drag and drop

const source = await $('#draggable');
const target = await $('#dropzone');
await source.dragAndDrop(target);
await source.dragAndDrop({ x: 200, y: 100 });   // by offset

Keys

await browser.keys(['Control', 'a']);
await browser.keys(['Tab']);
await browser.keys(['Escape']);
await browser.keys('Hello world');               // type a string

File upload

import path from 'path';
 
const filePath = await browser.uploadFile(path.resolve('./fixtures/avatar.png'));
await $('input[type="file"]').setValue(filePath);

Execute JavaScript

const title = await browser.execute(() => document.title);
 
const dataAttr = await browser.execute(
  (selector: string) => document.querySelector(selector)?.dataset.userId,
  '#user-card');
 
await browser.executeAsync((done) => {
  setTimeout(() => done('async result'), 500);
});

Assertions (Expect)

@wdio/expect ships in WDIO and adds element-aware matchers on top of Jest-style expect. All matchers are auto-retrying — they re-poll the element until it passes or the timeout hits.

Visibility

await expect($('.banner')).toBeDisplayed();
await expect($('.banner')).not.toBeDisplayed();
await expect($('.banner')).toBeExisting();

Interactability

await expect($('button')).toBeClickable();
await expect($('button')).toBeEnabled();
await expect($('button')).toBeDisabled();

Text and value

await expect($('h1')).toHaveText('Welcome back');
await expect($('h1')).toHaveTextContaining('Welcome');
await expect($('h1')).toHaveText(/^Welcome/);
await expect($('input')).toHaveValue('ada@example.com');
await expect($('input')).toHaveValueContaining('@');

Attributes

await expect($('a.home')).toHaveAttr('href', '/');
await expect($('img')).toHaveAttr('alt');                   // attribute exists
await expect($('input')).toHaveElementProperty('checked', true);

Class / count / state

await expect($('.tab.active')).toHaveElementClass('selected');
await expect($('ul')).toHaveChildren(3);
await expect($('input[type=checkbox]')).toBeSelected();
await expect($('input')).toBeFocused();

Browser-level

await expect(browser).toHaveUrl('https://app.example.com/dashboard');
await expect(browser).toHaveUrlContaining('/dashboard');
await expect(browser).toHaveTitle('Dashboard — Example');

Custom timeout / message

await expect($('.spinner')).not.toBeDisplayed({
  timeout: 10_000,
  timeoutMsg: 'Spinner never went away',
  interval: 500,
});

Browser Commands

await browser.url('/login');                     // relative to baseUrl
await browser.url('https://other.example.com');
await browser.getUrl();
await browser.getTitle();
 
await browser.refresh();
await browser.back();
await browser.forward();
 
await browser.newWindow('https://example.com', {
  type: 'tab',
  windowName: 'help',
});
await browser.switchWindow('Dashboard');         // by title or URL substring
 
// Cookies
await browser.setCookies([{ name: 'session', value: 'abc' }]);
await browser.getCookies();
await browser.deleteCookies(['session']);
await browser.deleteAllCookies();
 
// Window size
await browser.setWindowSize(1280, 720);
const { width, height } = await browser.getWindowSize();
await browser.maximizeWindow();

browser.pause(1000) exists but avoid it in production tests — it's the #1 source of flake. Use waitUntil or auto-retrying assertions instead.

Waits

WebdriverIO assertions auto-retry, so explicit waits are mostly only needed for non-element conditions.

Element-level

await $('.dialog').waitForDisplayed({ timeout: 5_000 });
await $('.dialog').waitForDisplayed({ reverse: true });   // wait for it to disappear
await $('button').waitForClickable();
await $('.row').waitForExist();
await $('input').waitForEnabled();

Browser-level — waitUntil

await browser.waitUntil(
  async () => (await browser.getTitle()) === 'Dashboard',
  {
    timeout: 5_000,
    timeoutMsg: 'Title never became Dashboard',
    interval: 250,
  },
);
 
// Wait for a network-driven state change
await browser.waitUntil(
  async () => (await $$('.user-row')).length >= 10,
  { timeout: 10_000, timeoutMsg: 'Users never finished loading' },
);

Default timeout

waitforTimeout in wdio.conf.ts sets the global default for all wait commands (default 5_000 ms).

Configuration (wdio.conf.ts)

Capabilities

import { Options } from '@wdio/types';
 
export const config: Options.Testrunner = {
  runner: 'local',
  specs: ['./test/specs/**/*.ts'],
 
  baseUrl: 'https://app.example.com',
  waitforTimeout: 10_000,
  connectionRetryTimeout: 120_000,
 
  maxInstances: 4,            // run up to 4 specs in parallel
 
  capabilities: [
    {
      browserName: 'chrome',
      'goog:chromeOptions': {
        args: process.env.HEADLESS === '1' ? ['--headless=new'] : [],
      },
    },
    { browserName: 'firefox' },
    { browserName: 'safari' },
  ],
 
  logLevel: 'warn',
  framework: 'mocha',
  mochaOpts: { ui: 'bdd', timeout: 60_000 },
 
  reporters: [
    'spec',
    ['allure', { outputDir: 'allure-results' }],
    ['junit', { outputDir: 'junit-results', outputFileFormat: o => `results-${o.cid}.xml` }],
  ],
 
  services: [
    'chromedriver',
    'visual',
    ['intercept', {}],
  ],
 
  // Hooks
  before: function () {
    require('expect-webdriverio');
  },
  beforeTest: async function () {
    await browser.deleteAllCookies();
  },
  afterTest: async function ({ passed }) {
    if (!passed) await browser.takeScreenshot();
  },
};

TypeScript via ts-node

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "types": ["@wdio/globals/types", "@wdio/mocha-framework", "expect-webdriverio"]
  }
}

wdio.conf.ts is auto-loaded by ts-node when WebdriverIO sees a .ts extension.

Run

npx wdio run wdio.conf.ts
npx wdio run wdio.conf.ts --spec ./test/specs/login.spec.ts
HEADLESS=1 npx wdio run wdio.conf.ts

Page Object Pattern

Base page — shared helpers

// pages/Page.ts
export abstract class Page {
  abstract get path(): string;
 
  async open(): Promise<this> {
    await browser.url(this.path);
    return this;
  }
}

A specific page

// pages/LoginPage.ts
import { Page } from './Page';
 
export class LoginPage extends Page {
  get path() { return '/login'; }
 
  // Selectors as getters — re-resolved on each access
  get email()  { return $('[data-testid="email"]'); }
  get pass()   { return $('[data-testid="password"]'); }
  get submit() { return $('[data-testid="submit"]'); }
  get error()  { return $('[data-testid="error"]'); }
 
  async loginAs(email: string, password: string): Promise<void> {
    await this.email.setValue(email);
    await this.pass.setValue(password);
    await this.submit.click();
  }
 
  async expectError(message: string): Promise<void> {
    await expect(this.error).toHaveText(message);
  }
}

Use in a test

import { LoginPage } from '../pages/LoginPage';
 
describe('Login', () => {
  const login = new LoginPage();
 
  beforeEach(async () => {
    await login.open();
  });
 
  it('signs in with valid credentials', async () => {
    await login.loginAs('ada@example.com', 'secret');
    await expect(browser).toHaveUrlContaining('/dashboard');
  });
 
  it('shows error on bad password', async () => {
    await login.loginAs('ada@example.com', 'wrong');
    await login.expectError('Invalid email or password');
  });
});