# Page Object Model

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Test Design and Quality
Source: https://www.devtutorials.ai/learn/playwright/test-design/page-object-model

> Why repeated locators make tests hard to change, what a page object is, writing a page class with locators and actions, using it in tests, pages that lead to other pages, component objects for repeated parts, page objects as fixtures, where assertions belong, folder structure, and common anti-patterns.

## Page Object Model

- As a test suite grows, the same locators and steps appear in many files.
- When the page changes, you must fix every copy. Miss one, and a test breaks.
- The **Page Object Model** (POM) puts each page's locators and actions in one class. Tests use the class.

**Simple lesson:** a page object is one class per page. It knows how to find things and do things on that page.

### A Real-World Example

- Two tests both log in through the form. The login steps are copied.
- Now the team renames the **Log in** button to **Sign in**. Both tests break, and both need the same fix.

- With 50 tests, that would be 50 copies. A page object keeps it to one.

### What You Will Learn

1. The problem POM solves, and how it works.
2. Writing a page class with locators and actions.
3. Using page objects in tests.
4. Pages that lead to other pages.
5. Component objects for repeated parts.
6. Page objects as fixtures.
7. Where assertions go, and common anti-patterns.

## What a Page Object Is

### One Class per Page

- A page object is a **class**. It gets the `page` in its constructor.
- It has **locators** as properties: `emailInput`, `loginButton`.
- It has **actions** as methods: `goto()`, `login(email, password)`.
- Tests call the methods. They do not need to know the locators.

<figure className="plw-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-plw-pom" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-pom">Animation: three test files all call loginPage.login(). The button text changes from Log in to Sign in, and only one line inside the LoginPage class is updated. All three tests stay green</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <rect x="15" y="20" width="170" height="44" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="30" y="47" style={{ fill: "var(--plw-blue)" }}>login.spec.ts</text>
    <rect x="15" y="88" width="170" height="44" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="30" y="115" style={{ fill: "var(--plw-blue)" }}>orders.spec.ts</text>
    <rect x="15" y="156" width="170" height="44" rx="8" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} /><text x="30" y="183" style={{ fill: "var(--plw-blue)" }}>account.spec.ts</text>
  </g>
  <g style={{ stroke: "var(--plw-purple)", strokeWidth: 2 }}><line x1="185" y1="42" x2="300" y2="100" /><line x1="185" y1="110" x2="300" y2="110" /><line x1="185" y1="178" x2="300" y2="120" /></g>
  <rect x="300" y="55" width="320" height="115" rx="12" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} />
  <text x="315" y="78" style={{ fill: "var(--plw-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>class LoginPage</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11 }}>
    <text x="315" y="102" style={{ fill: "var(--ink2)" }}>emailInput = getByLabel('Email')</text>
    <text x="315" y="124" style={{ fill: "var(--ink2)" }}>button = getByRole('button', {"{"} name:</text>
    <text x="555" y="124" style={{ fill: "var(--plw-red)", fontWeight: 700 }}>'Log in'<animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.4;0.45;0.95;1" dur="8s" repeatCount="indefinite" /></text>
    <text x="555" y="124" opacity="0" style={{ fill: "var(--plw-green)", fontWeight: 700 }}>'Sign in'<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.45;0.95;1" dur="8s" repeatCount="indefinite" /></text>
    <text x="315" y="146" style={{ fill: "var(--ink2)" }}>login(email, password)</text>
  </g>
  <text x="460" y="200" textAnchor="middle" opacity="0" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>One line changed. Every test still passes.<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.55;0.95;1" dur="8s" repeatCount="indefinite" /></text>
</svg>
<figcaption>Many tests share one page object. A page change means one fix in one place.</figcaption>
</figure>

### Your First Page Object

- Open the **Explorer**. The class lives in `pages/login-page.ts`. The test imports it.
- `readonly` means the property is set once, in the constructor.

- Try it: change `'Log in'` in the class to `'Sign in'` and run. Both tests fail in the same place, and one fix repairs both.

Locators are lazy. Creating them in the constructor does not search the page. They look for elements only when an action or assertion uses them.

**Simple lesson:** locators as properties, actions as methods, one class per page.

## Building a Set of Page Objects

### Pages That Lead to Other Pages

- An action that opens a new page can **return** that page's object.
- `productsPage.openCart()` returns a `CartPage`. The test reads like the user's journey.

- `addToCart(name)` takes the product name. One method works for every product.

### Component Objects

- Some parts repeat on many pages: a header, a product card, a date picker.
- Model them as **components**. A component object takes a **root locator** and finds things inside it.
- Page objects can then use the component, so the header logic is written once.

**Simple lesson:** a component object wraps one repeated part, found from a root locator.

## Page Objects as Fixtures

### No More new in Every Test

- Writing `new LoginPage(page)` in every test is repetition too.
- A **fixture** can create the page object for you, as in **Fixtures**. The test just asks for `loginPage`.

- The fixture also opens the login page, so each test starts in the right place.

## Assertions and Good Design

### Where Assertions Belong

- Most teams keep `expect` **in the tests**. Then each test shows clearly what it checks.
- A page object can still offer a **small check helper** for something many tests verify, like "the account page has loaded".
- Name such helpers `expect...` so a reader knows they assert.

### Folder Structure

- Keep page objects out of the `tests` folder, so Playwright does not look for tests in them.
- A common layout:

### Anti-Patterns to Avoid

- **God objects**: one huge class for the whole site. Split it by page and component.
- **Hidden waits**: `waitForTimeout()` inside methods. Playwright already waits. Use web-first assertions in tests.
- **Every click as a method**: `clickLoginButton()`, `clickEmail()`. Methods should be user tasks, like `login()`.
- **Deep inheritance**: `BasePage` then `ShopPage` then `ProductsPage`. Prefer small components you combine.
- **Returning raw data for checks**: `getTitleText()` that returns a string loses auto-waiting. Expose the locator and use `toHaveText()`.

A method like `async getCount() { return await this.count.textContent(); }` reads the text once, with no retry. A test that checks it right after a click can be flaky. Expose `this.count` and use `await expect(products.count).toHaveText('6 products')`.

POM is not required. A small suite with a few tests can stay simple. Add page objects when you see the same locators in three or more places.

**Simple lesson:** small classes, user-level methods, locators exposed, assertions mostly in tests.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Write a page object for your login page and use it in every test that logs in.
- Move repeated locators from three specs into one page object.
- Provide your page objects through fixtures.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- A page object is a class with the page's locators and actions.
- Tests call methods like `login()`. When the page changes, one class changes.
- Methods that open another page can return that page's object.
- Component objects wrap repeated parts from a root locator.
- Fixtures can create page objects for each test.
- Keep `expect` in tests, avoid sleeps and huge classes, and expose locators instead of strings.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Page objects inside the `tests` folder with `.spec` names | Playwright tries to run them as tests | Put them in `pages/` and `components/` |
| Returning `textContent()` for checks | No auto-retry, flaky tests | Expose the locator, assert with `toHaveText()` |
| `waitForTimeout()` inside page methods | Every test gets slower | Rely on auto-waiting and web-first assertions |
| One method per click | Classes get huge and tests stay noisy | Methods for user tasks, like `login()` |
| A single class for the whole site | Hard to read and change | One class per page, plus components |

## Check Yourself

## Exercises

## What to Remember

- One class per page: locators as properties, user tasks as methods.
- When the page changes, fix the class, not every test.
- Return the next page object from navigation methods.
- Components wrap repeated parts. Fixtures hand out page objects.
- Keep `expect` in tests, expose locators, and never hide sleeps.

Next: **Visual, Snapshot, and Accessibility Testing**
