Q2 of 17 · Framework design
How do you structure a Page Object Model in practice — what goes in a base page class and how do you handle navigation between pages?
Framework designJuniorframework-designpombase-pagefluent-interface
Short answer
Short answer: A BasePage class holds the driver/page reference and shared utilities (wait helpers, screenshot capture). Individual page classes extend BasePage and return new Page Objects from action methods that result in a navigation (the Fluent Interface pattern).
Detail
Base page class:
// pages/BasePage.ts
export class BasePage {
constructor(protected readonly page: Page) {}
async waitForUrl(pattern: string | RegExp) {
await this.page.waitForURL(pattern);
}
async getTitle(): Promise<string> {
return this.page.title();
}
}
Page class that returns the next PO on navigation:
// pages/LoginPage.ts
export class LoginPage extends BasePage {
async login(email: string, password: string): Promise<DashboardPage> {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="login-btn"]');
await this.waitForUrl('/dashboard');
return new DashboardPage(this.page);
}
}
// In the test:
const loginPage = new LoginPage(page);
const dashboard = await loginPage.login('user@example.com', 'pass');
// dashboard is now a DashboardPage with type safety
await dashboard.navigateToOrders();
Fluent interface — each action that navigates returns the destination Page Object. This gives you:
- Type safety — the compiler knows what page you're on after each action
- Readable test chains:
await login.login(…).then(d => d.goToProfile())
Directory structure:
tests/
pages/
BasePage.ts
LoginPage.ts
DashboardPage.ts
CheckoutPage.ts
specs/
login.spec.ts
checkout.spec.ts
// WHAT INTERVIEWERS LOOK FOR
BasePage for shared utilities. Fluent interface — methods return the next PO. Type-safe navigation chain. Directory structure.