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.