¶ The WebdriverIO Page Object Pattern
WebdriverIO recommends page objects built with getters. A getter runs $() every time it's used, so you always get a fresh element, and you avoid stale element errors.
¶ Project Structure
wdio-e2e/
├── wdio.conf.ts
├── tsconfig.json
├── test/
│ ├── pageobjects/
│ │ ├── page.ts # base class
│ │ ├── login.page.ts
│ │ └── secure.page.ts
│ ├── specs/
│ │ ├── login.e2e.ts
│ │ └── checkout.e2e.ts
│ └── data/
│ └── users.json
└── package.json
¶ Base Page
// test/pageobjects/page.ts
import { browser } from '@wdio/globals';
export default class Page {
public open(path: string) {
return browser.url(`/${path}`);
}
}
¶ Login Page
// test/pageobjects/login.page.ts
import { $ } from '@wdio/globals';
import Page from './page';
class LoginPage extends Page {
public get inputUsername() { return $('#username'); }
public get inputPassword() { return $('#password'); }
public get btnSubmit() { return $('button[type="submit"]'); }
public get flash() { return $('#flash'); }
public async login(username: string, password: string) {
await this.inputUsername.setValue(username);
await this.inputPassword.setValue(password);
await this.btnSubmit.click();
}
public open() {
return super.open('login');
}
}
export default new LoginPage();
The module exports an instance (new LoginPage()), so specs use it directly without creating objects.
¶ Spec Using Page Objects
// test/specs/login.e2e.ts
import { expect } from '@wdio/globals';
import LoginPage from '../pageobjects/login.page';
import SecurePage from '../pageobjects/secure.page';
describe('Login', () => {
it('logs in with valid credentials', async () => {
await LoginPage.open();
await LoginPage.login('tomsmith', 'SuperSecretPassword!');
await expect(SecurePage.flash).toBeDisplayed();
await expect(SecurePage.flash).toHaveText(expect.stringContaining('You logged into a secure area!'));
});
it('rejects an invalid password', async () => {
await LoginPage.open();
await LoginPage.login('tomsmith', 'wrong');
await expect(LoginPage.flash).toHaveText(expect.stringContaining('Your password is invalid!'));
});
});
¶ Guidelines
- One page object per page; one component object per repeated UI part (header, modal, table).
- Getters for elements,
asyncmethods for user actions. - Keep test data in
test/data/*.jsonor in factories, not inside page objects. - Keep assertions in specs, so a page object can be reused by many tests with different expectations.
Links: Page Object Pattern · Example project