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'); // containsTag name
const button = await $('<button />'); // first <button>ARIA
const btn = await $('aria/Submit'); // accessible nameCustom 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 offsetKeys
await browser.keys(['Control', 'a']);
await browser.keys(['Tab']);
await browser.keys(['Escape']);
await browser.keys('Hello world'); // type a stringFile 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.tsPage 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');
});
});