# Page Objects and Custom Fixtures — Playwright & Cypress

Source: https://www.skillbyai.com/en/e2e-testing/pw-pom

> 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.

```typescript
// 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.

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

- [ ] The browser is reinstalled
- [ ] The test has not started yet
- [x] 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.
