skip to content

Page Objects vs Locator-First

You will learn the argument between the page object model and locator-first or screenplay styles, and how to pick an abstraction that clarifies rather than hides. Interviewers treat it as a design question — underneath, it is really about coupling and readability.

on this pageshow

questions

5

In an end-to-end browser test suite, what belongs inside a page object and what should stay in the test — and why do experienced teams keep assertions out of page objects?

level: middleimportance: must knowfreq 65%

answer

  1. actions in, verdicts out
  2. the test tells the story
  3. expose state, don't judge it
  4. boolean flags are the smell
  5. one owner for element knowledge

basics

~20 s

A page object owns a screen's locators and user actions and returns data or locators; the test keeps the assertions and the scenario. Assertions buried in a shared helper hide what each test actually checks and make one test's expectations everyone's.

solid answer

~50 s

A page object is a small class or module that owns one screen's element handles and the user-level actions on it — `signIn(email, password)`, `openFilters()` — plus navigation to that screen. What it should not own is the scenario or the verification. The test reads as a story: arrange, act, assert, and the assert lives in the test so a reader sees the expectation next to the scenario name. Assertions inside a page object cause three problems: the same method now means different things to different tests, so it accumulates flags and branches; a test's real expectation is invisible in the test file; and a failure is reported from shared code rather than from the case that cared. The workable rule is that a page object exposes state — a locator, a count, a string — and the test decides what that state should be. Exception: a genuine precondition, like waiting for the screen to be ready, can live in the object because every caller needs it.

code

typescript · 32 lines
typescript
import { test, expect, type Locator, type Page } from '@playwright/test';

class LoginPage {
  readonly email: Locator;
  readonly password: Locator;
  readonly submit: Locator;
  readonly error: Locator;

  constructor(private readonly page: Page) {
    this.email = page.getByLabel('Email');
    this.password = page.getByLabel('Password');
    this.submit = page.getByRole('button', { name: 'Sign in' });
    this.error = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async signIn(email: string, password: string) {
    await this.email.fill(email);
    await this.password.fill(password);
    await this.submit.click();
  }
}

test('rejects a bad password', async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  await login.signIn('[email protected]', 'wrong');
  await expect(login.error).toHaveText('Incorrect email or password');
});

go deeper

for a junior

Know what a page object is and be able to say it holds the element lookups and the user actions for a screen, so a markup change touches one file instead of many tests.

for a middle

Be ready to draw the line concretely: actions and element knowledge inside, scenario and assertions in the test, state exposed as a return value. Explain why a verifyX method grows boolean parameters over time.

for a senior

Show judgment about the exception cases — preconditions versus verdicts, locators versus resolved values — and explain how assertion placement changes what a failure report tells the on-call engineer.

for a principal

Own the convention across teams: what the layer is allowed to know, how it is reviewed, and how you stop a shared helper library from becoming a second application nobody has tests for.

## What a page object actually is A page object is an abstraction layer between a test and the DOM. Instead of the test reaching for elements directly, it talks to an object that models one screen — or one meaningful piece of a screen — in the vocabulary of the user: "sign in", "add to cart", "open the filters panel". The original motivation is a single one: **when the markup changes, exactly one file changes.** Everything else people attribute to page objects (readability, reuse, onboarding) is a secondary benefit that only materialises if the abstraction is designed well. ## The three responsibilities that belong inside 1. **Element handles.** The object knows how to find its own elements. Whether that is a role-based locator, a test id, or something else is a separate design question; what matters here is that the knowledge lives in one place with one owner. 2. **User-level actions.** Methods named after what a person does, not after what the DOM does. `signIn(email, password)` is a page-object method; `fillEmailField(value)` is a thin wrapper that has bought you nothing — the test could have written that itself, and the wrapper only adds a layer to read through. 3. **State exposure.** A getter that returns a locator, a count, or a piece of text so the caller can decide whether it is correct. ## What stays in the test The **scenario** and the **assertions**. The test file is where a reader learns what this case is proving. A test that reads ```ts await login.signIn('[email protected]', 'wrong'); await expect(login.error).toHaveText('Incorrect email or password'); ``` tells you both the action and the expectation at a glance. Compare it with `await login.signInExpectingFailure(...)` — the message being checked is now invisible, and the next case that needs a *different* message will either add a parameter or duplicate the method. ## Why assertions inside page objects go wrong **They generalise a single test's expectation.** The first caller wants the error banner; the second wants the field-level message; the third wants the submit button re-enabled. A method that asserts must serve all of them, so it grows boolean parameters (`signIn(email, pw, { expectError: true })`) until nobody can predict what a call does without reading the implementation. **They flatten the failure report.** When the assertion lives in the test, the failure is attributed to the test that cared, and the failing line is the expectation itself. When it lives three calls deep in a helper used by forty tests, the report points at shared code and the reader has to reconstruct which scenario was running. **They hide intent from review.** A reviewer reading the test file cannot tell what is verified. The coverage of the suite becomes something you can only learn by reading the helpers, which is the opposite of what the abstraction was for. ## The one legitimate exception A *precondition* is not an assertion about the thing under test — it is a guard that the object is usable at all. Waiting until the screen has rendered before returning from `goto()` is fine, because every caller needs it and no caller wants to state it. The test that a rule holds: would any caller ever want this check *not* to run, or want it to check something different? If yes, it is an assertion and belongs to the caller. ## Returning values, not verdicts The clean shape is that the object hands back something inspectable and the test judges it: ```ts // object exposes state get error() { return this.page.getByRole('alert'); } async itemCount() { return this.list.getByRole('listitem').count(); } ``` Returning a *locator* rather than resolved text is usually better in a modern e2e runner, because the assertion can then retry until it matches instead of sampling a value once and comparing a stale snapshot. ## Signs the object has taken on too much - Methods whose names contain a verdict: `verify…`, `assert…`, `shouldSee…`. - Boolean or enum parameters that switch the checks the method runs. - Methods that swallow failure — `isLoggedIn()` implemented with a try/catch around a wait — which turn a real breakage into a silently `false` result and a much later, less informative failure. - One class per URL with fifty unrelated methods, because the page is where the app happens to put things rather than a unit anyone reuses. ## The honest framing for an interview Page objects are not a testing law; they are one answer to *where does knowledge of the UI live*. The valuable part of the answer is the separation of concerns — actions and element knowledge on one side, expectations and scenario on the other — and the ability to say which side a given piece of code belongs on and why.

  • Is there any case where you would accept a check inside a page object?
    Yes — a precondition every caller needs, such as waiting for the screen to be ready before an action method returns. The test is whether any caller would ever want it to check something different; if so it is an assertion and belongs in the test. Guards that make the object usable are fine; verdicts about the behaviour under test are not.
  • Should a page-object getter return resolved text or the element handle itself?
    Prefer the handle. An assertion on a locator can retry until the expectation is met, while returning `await el.textContent()` samples once and hands the test a value that may already be stale, which is a common source of flaky comparisons. Return resolved values only when the test genuinely needs to compute with them.
  • A helper called isLoggedIn() catches the timeout and returns false. What is wrong with that?
    It converts every failure — a real regression, a broken selector, a slow deploy — into a plain `false`, and the test then fails much later with a misleading message, after burning the full timeout. Prefer asserting the expected state directly, and reserve boolean probes for genuinely optional UI where both outcomes are valid.

A page object is a remote control, not a referee: it gives the test buttons to press and a display to read, but it does not decide whether the score is correct.

saying these in an interview costs you the question

  • Page objects must assert; that is what verifyX methods are for
  • Every URL needs its own page object class
  • A method per input field is proper encapsulation
  • Returning textContent is cleaner than returning a locator
  • isLoggedIn() catching the timeout makes the test more robust

context

open as a page

In an end-to-end browser test suite, what do you gain and what do you lose by writing element lookups and steps inline in each test instead of extracting them into shared page objects or helpers?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Inline tests read top to bottom and are easy to debug, but repeat the same element knowledge in every test, so a UI change edits many files. Extraction removes that duplication and adds a layer of indirection to read through.

open as a page

An end-to-end test fails with "element not visible" reported from line 12 of a shared checkout helper, and the report gives no indication of which user step actually broke. How does test abstraction produce failures like this, and how do you structure helpers so failures stay diagnosable?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Deep helper chains report failures from shared code instead of from the step that broke. Keep the layers shallow, name methods after user intent, avoid conditionals and swallowed errors inside helpers, and let the assertion that matters live in the test.

open as a page

An end-to-end suite has one page object per URL, but the application is built from reusable components — a cart widget, a data grid, a date picker — that appear on many of those pages, and the page objects have started duplicating each other. When would you scope helpers to a component instead of to a page, and what does that change?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Scope the helper to the widget when the same widget appears on several pages: one owner, reused wherever it renders, constructed from the container element it lives in. Page-shaped objects duplicate that widget's knowledge on every page that hosts it.

open as a page

The screenplay (actor and task) style models an end-to-end test as an actor performing composable tasks rather than as calls on page objects. What does it change compared with page objects, and how would you decide whether adopting it is worth it for a growing suite?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Screenplay reuses tasks organised around user goals instead of classes organised around screens, and composes them from small interactions. It buys composability and readable narration at the cost of more indirection, more concepts and a steeper onboarding curve.

open as a page