Lesson 9 / 25

Page Objects and Custom Fixtures

Reusable abstractions for pages and setup.

Encapsulate pages, inject them as fixtures

A Page Object is a class that wraps one page or component: it holds locators and exposes intention-revealing methods such as addTodo(text), so tests read like user stories and a changed selector is fixed in one place. Playwright's test.extend creates custom fixtures: a fixture function receives other fixtures, prepares a value, passes it to use(value), and can clean up after use returns. Fixtures are created only for the tests that request them. Keep assertions mostly in tests (or in clearly named page-object methods) so failures stay readable.

A page object provided by a fixture

test.extend with setup and teardown.

// tests/fixtures.ts
import { test as base, expect, type Page, type Locator } from '@playwright/test';

export class TodoPage {
  readonly input: Locator;
  readonly items: Locator;
  constructor(private readonly page: Page) {
    this.input = page.getByPlaceholder('What needs to be done?');
    this.items = page.getByRole('listitem');
  }
  async goto() { await this.page.goto('/todos'); }
  async addTodo(text: string) {
    await this.input.fill(text);
    await this.input.press('Enter');
  }
}

export const test = base.extend<{ todoPage: TodoPage }>({
  todoPage: async ({ page }, use) => {
    const todoPage = new TodoPage(page);
    await todoPage.goto();
    await use(todoPage);
    // teardown would go here
  },
});
export { expect };

// tests/todo.spec.ts
import { test, expect } from './fixtures';

test('adds two todos', async ({ todoPage }) => {
  await todoPage.addTodo('Buy milk');
  await todoPage.addTodo('Walk dog');
  await expect(todoPage.items).toHaveCount(2);
});

Do not over-abstract

A page object per screen is usually enough. Deep inheritance trees of page objects make tests harder to follow than the duplication they remove.

Quick check: In a Playwright fixture, what happens after `await use(value)` returns?

  • The browser is reinstalled
  • The test has not started yet
  • The test has finished, so teardown code can run
  • The fixture is shared with every other worker
Answer

The test has finished, so teardown code can run — Code before use is setup; code after is teardown.