Page Object Model

By Syncfusion®, Inc. · Last updated

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.

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

login.spec.ts orders.spec.ts account.spec.ts

class LoginPage

emailInput = getByLabel('Email') button = getByRole('button', name: 'Log in' 'Sign in' login(email, password)

One line changed. Every test still passes.

Many tests share one page object. A page change means one fix in one place.

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

MistakeWhat happensFix
Page objects inside the tests folder with .spec namesPlaywright tries to run them as testsPut them in pages/ and components/
Returning textContent() for checksNo auto-retry, flaky testsExpose the locator, assert with toHaveText()
waitForTimeout() inside page methodsEvery test gets slowerRely on auto-waiting and web-first assertions
One method per clickClasses get huge and tests stay noisyMethods for user tasks, like login()
A single class for the whole siteHard to read and changeOne 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