lecture 7 of 160 completed

Page object patterns beyond the basics

Loadable components, fluent chaining, and inheritance versus composition. Basic POM does not survive two hundred pages.

Basic page objects hold locators and methods. That works for twenty pages. At two hundred, three problems appear that the basic pattern has no answer for.

Problem one: a page object hands you an object for a page that has not loaded.

const page = new ContactsPage(driver);
await page.delete('Dana Fox');       // was the page ever loaded?

Nothing checked. If navigation failed, the failure surfaces inside delete as a missing element, blaming the wrong thing.

The loadable component pattern makes a page responsible for proving it loaded:

class BasePage {
  constructor(driver) { this.driver = driver; }

  async load() {
    await this.navigate();
    await this.waitUntilLoaded();
    return this;
  }

  async waitUntilLoaded() {
    throw new Error(`${this.constructor.name} must implement waitUntilLoaded`);
  }
}

class ContactsPage extends BasePage {
  async navigate() { await this.driver.get(`${config.baseUrl}/contacts`); }

  async waitUntilLoaded() {
    await waits.forVisible(this.driver, By.css('[data-testid="contact-table"]'),
      'The contacts table never appeared');
  }
}

Now await new ContactsPage(driver).load() either gives you a loaded page or fails saying the contacts table never appeared. The failure names the real problem at the real moment.

Problem two: navigation between pages is untyped and unclear.

A method that clicks something leading elsewhere should hand you the page you arrived at:

class LoginPage extends BasePage {
  async signIn(email, password) {
    await this.type(this.emailField, email);
    await this.type(this.passwordField, password);
    await this.click(this.submitButton);
    return new DashboardPage(this.driver).waitUntilLoaded();
  }
}
const dashboard = await new LoginPage(driver).load().then((p) => p.signIn(user, pass));

This is fluent chaining, and its value is more than style. In a typed language your editor now tells you what is reachable from where, and a test cannot accidentally call a dashboard method while on the login page. The page model encodes the application's navigation graph.

Do not overdo it. Chains four calls deep become unreadable, and a failure mid-chain is harder to locate. Two or three steps is the useful range.

Problem three: inheritance collapses under its own weight.

The instinct is a hierarchy: BasePageAuthenticatedPageAdminPageAdminContactsPage. It works until a page needs behaviour from two branches, and then you are stuck.

⚠️ Prefer composition over deep inheritance.

Inheritance for what a page is (every page loads and navigates). Composition for what a page has (this page has a data grid, a filter bar and a modal).

A page object with three components is easy to change. A class four levels deep is not, because you cannot tell what a change affects without reading the whole chain.

class ContactsPage extends BasePage {
  constructor(driver) {
    super(driver);
    this.grid = new DataGrid(driver, By.css('[data-testid="contact-table"]'));
    this.filters = new FilterBar(driver, By.css('[data-testid="filters"]'));
  }

  async delete(name) {
    await this.grid.rowAction(name, 'Delete');
    await new ConfirmDialog(this.driver).confirm();
  }
}

One level of inheritance, three composed components. That structure is the subject of the next lecture.

The rule that keeps page objects reusable: no assertions inside them.

// wrong: the page decides what success means
async deleteAndVerify(name) { ... }

// right: the page acts, the test judges
async delete(name) { ... }
async hasRow(name) { return (await this.driver.findElements(this.rowFor(name))).length > 0; }

The first cannot be used in a test about deletion being refused. Page objects expose state; tests assert on it.

Remember this: pages prove they loaded, navigation returns the next page, compose components rather than deepening inheritance, and never assert inside a page object.

Reference: Page object models.

now prove it

Reading is the setup. Solve this problem in the editor. We break the app on purpose to check your test would catch it.