Page Object Model

Why Page Object Model exists, where it helps, and why it cannot rescue poor UI automation by itself.

Updated:

Contents

Page Object Model separates interface interaction from a test scenario's purpose. A Page Object knows how to find a field, fill in a form, and wait for a result. The test itself explains the behavior being checked and why the result is correct.

The pattern helps when multiple scenarios use the same elements or actions. After markup changes, the team updates a locator in one place. However, a page object that hides all business logic and hundreds of assertions becomes harder to maintain: the test looks short, but failure causes disappear into shared code.

An object's responsibilities

A good object exposes actions and observations: entering an address, submitting a form, and retrieving an error message. It need not correspond to an entire page. Headers, carts, and confirmation dialogs are often better represented as separate components with their own locators.

Give the object a clear search scope. Do not distinguish two "Save" buttons in different dialogs by arbitrarily selecting the first match. Select the appropriate container first, then the element within it. This reflects user interaction structure and reduces fragility.

Where assertions belong

The scenario should explicitly show its main expected result. If a test is named "shows an empty-name error," the relevant action should be followed by a specific message assertion. The object may expose the message locator without always asserting success after every submission.

An object may contain readiness checks, such as the presence of a screen's main element. These are technical prerequisites. Distinguish them from business expectations: the same form submission method may serve positive and negative scenarios.

In a negative scenario, save can return the form state without throwing an exception because no success message appeared. However, technical failures such as a missing button must remain visible. Avoid converting them into a generic false return, which erases the distinction between an expected product rejection and a broken interaction. The object supplies facts; the scenario determines whether they meet its particular expectation. This lets one form support error and successful-save checks without skipAssertion or expectFailure flags.

A minimal standalone example

The example uses Playwright Test and a local HTML page held in browser memory. After installing @playwright/test and a suitable browser, save the file as profile.spec.js and run npx playwright test profile.spec.js. No external website, account, or server is needed; the training form is deliberately simple.

javascript
const { test, expect } = require('@playwright/test');
class ProfileForm {
  constructor(page) {
    this.name = page.getByLabel('Name');
    this.save = page.getByRole('button', { name: 'Save' });
    this.message = page.getByRole('status');
  }
  async submit(name) {
    await this.name.fill(name);
    await this.save.click();
  }
}
test('does not save an empty name', async ({ page }) => {
  await page.setContent(`
    <label>Name <input id="name"></label>
    <button id="save">Save</button><p role="status"></p>
    <script>
      document.querySelector('#save').onclick = () => {
        const name = document.querySelector('#name').value.trim();
        document.querySelector('[role=status]').textContent =
          name ? 'Saved' : 'Enter a name';
      };
    </script>`);
  const form = new ProfileForm(page);
  await form.submit('');
  await expect(form.message).toHaveText('Enter a name');
});

The code demonstrates separated responsibilities: the object handles interaction and the test specifies the expected error. It does not establish real-product profile persistence. That requires a separate scenario with a real API and isolated data, rather than expanding this training check through hidden assumptions.

A practical real-project scenario

In a test account area, create a separate user named Anna. Open editing, replace the name with spaces, and submit. Expect a clear error and no successful save. Then enter Maria and check confirmation, reopening, and the actual saved value.

Both outcomes can use the same form object. An API call or subsequent retrieval supplies separate persistence evidence. Checking only the text "Saved" misses cases where the frontend reports success but the backend record remains unchanged.

Locators and waits

Prefer stable attributes: role and accessible name, label, or an agreed test ID. A long CSS path through nested div elements ties a test to markup. Text also needs deliberate handling: for a localized product, explicitly set the environment's locale.

Wait for the relevant state. After saving, wait for confirmation or changed data rather than a fixed pause. A Page Object does not automatically fix synchronization: a hidden sleep is just as unstable as one directly in the test.

Why large base classes are risky

A shared BasePage with dozens of generic methods quickly becomes another framework. A method such as clickElementByAnyLocator hides useful tool capabilities and complicates errors. Making every page inherit from one giant base creates relationships between unrelated scenarios.

Extract repetition after real examples emerge. A small address-form component is more useful than a universal "all product forms" mechanism. Composition lets the same component appear on different screens without a hierarchy based on incidental technical similarities.

Data and state

Avoid storing a shared browser or user in a page object's static variable. Each new scenario needs its own context and controlled data. Otherwise, parallel tests start changing the same fields and failures become hard to explain.

Keep constructors predictable: declaring locators should not silently log in, delete data, or open several pages. Setup, navigation, and verification should remain visible at the appropriate scenario or fixture level.

Assessing architecture quality

Take a small interface change and see how many files need editing. Then read a failure report without opening the object's implementation. It should be clear which action ran, what result was expected, and which element was not found.

A useful criterion is that the test reads as a sequence of goals while the object remains small and reusable. If understanding one line requires traversing five inheritance levels, the abstraction has stopped helping.

Common mistakes and conclusions

Common mistakes include automatically creating a class for every HTML page, hiding assertions in actions, returning Boolean false instead of a useful exception, and organizing objects around CSS rather than user capabilities. The pattern also does not resolve shared-environment or poor-test-data problems.

Page Object Model is useful as a discipline for defining boundaries. It reduces repeated interactions, keeps expectations understandable, and isolates interface changes. Start with simple objects and develop them according to the suite's actual needs.

Further reading