Organize Flows With Page Objects
As a suite grows, the same selectors and steps get copy-pasted across tests, and one markup change breaks them all. A page object puts the selectors and actions for a screen behind named methods, so tests read like intent (loginAs(...)) and a UI change is a one-line fix in a single class. This recipe wraps the live login example in a LoginPage.
ts
import { Browser } from 'craftdriver';
class LoginPage {
constructor(private readonly browser: Browser) {}
async open() {
await this.browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
}
async loginAs(username: string, password: string) {
await this.browser.getByLabel('Username').fill(username);
await this.browser.getByLabel('Password').fill(password);
await this.browser.getByRole('button', { name: 'Sign in' }).click();
}
async expectWelcome(username: string) {
await this.browser.expect('#welcome').toContainText(`Welcome back, ${username}!`);
}
}A test then reads as a sequence of intentions, with no selectors in sight:
ts
const loginPage = new LoginPage(browser);
await loginPage.open();
await loginPage.loginAs('alice', 'secret');
await loginPage.expectWelcome('alice');Notes
- Expose intent (
loginAs) rather than mechanics (fill,click) so tests survive markup changes. - Keep assertions the test cares about in the test; put reusable expectations (like
expectWelcome) on the page object. - Pass the
browserin rather than launching inside the page object, so lifecycle stays in your Vitest hooks.