iTestBDD

Page Object Model vs Screenplay Pattern: Decision Guide

Most Selenium or Playwright suites older than two years share the same structural DNA: a pages/ directory full of classes, one per URL, each owning its locators and actions. Page Object Model has been the default for so long that teams rarely question whether it's still the right default. It usually is — until the suite crosses a certain complexity threshold and the model starts working against you.

The Screenplay Pattern is the alternative that gets recommended on conference slides and then quietly abandoned after the first sprint. That's not because it's bad; it's because the migration cost is real and the benefit is conditional. The pattern pays off in specific architectural contexts that most teams never articulate before choosing.

By the end of this article you'll have a concrete decision framework: which pattern fits your team's scale, test architecture, and toolchain — and what the code actually looks like in each. The comparison is grounded in Playwright 1.44, Selenium 4, and the Serenity/JS 3.x implementation of Screenplay.

API Testing using Python, Behave, VS Code & GitHub Copilot

Smarter API Test Automation — Python, Behave, VS Code, AI with GitHub Copilot & CI/CD Pipelines. Complete in a Weekend!

Learn more

Structural Anatomy: What POM and Screenplay Actually Encode

Page Object Model is a UI-centric abstraction: each class maps to a page or component, exposing methods that combine locator resolution with user-facing actions. The mental model is "what can I do on this page?" That works cleanly when your test suite is a thin acceptance layer over a monolith. Locators stay in one place, tests read like scripts, and onboarding is fast because every engineer already knows the pattern.

Screenplay is an actor-centric, task-based abstraction rooted in SOLID principles — specifically the Single Responsibility and Open/Closed principles. Instead of pages owning actions, Actors perform Tasks composed of Interactions, and assert via Questions. The unit of reuse shifts from "page method" to "user capability." This maps naturally onto BDD's persona language (As a returning customer…) and scales better when multiple actors, cross-cutting concerns (auth state, API side-effects), or parallel persona flows are in play. The cost is indirection: a three-layer call stack where POM had one.

Implementation Side-by-Side: POM in Playwright vs Screenplay in Serenity/JS

Here's a representative POM class in Playwright (TypeScript). Nothing exotic — this is what 80% of teams actually ship:

// pages/CheckoutPage.ts
import { Page, Locator } from '@playwright/test';

export class CheckoutPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly placeOrderBtn: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.placeOrderBtn = page.getByRole('button', { name: 'Place Order' });
  }

  async fillEmail(email: string) {
    await this.emailInput.fill(email);
  }

  async submitOrder() {
    await this.placeOrderBtn.click();
    await this.page.waitForURL('**/confirmation');
  }
}

Clean, readable, and fast to write. The friction appears when submitOrder() needs to handle three different post-submit states (confirmation, fraud hold, address validation error), or when the same action sequence appears across CheckoutPage, GuestCheckoutPage, and MobileCheckoutPage. Methods grow, inheritance gets tempting, and you end up with a BasePage that nobody fully trusts.

The equivalent in Serenity/JS 3.x with Screenplay looks like this:

// tasks/PlaceOrder.ts
import { Task } from '@serenity-js/core';
import { Enter, Click, Wait, isVisible } from '@serenity-js/web';
import { CheckoutPage } from '../ui/CheckoutPage';

export const PlaceOrder = {
  as: (email: string) =>
    Task.where(`#actor places an order as ${email}`,
      Enter.theValue(email).into(CheckoutPage.emailInput()),
      Click.on(CheckoutPage.placeOrderBtn()),
      Wait.until(CheckoutPage.confirmationBanner(), isVisible()),
    ),
};
// spec/checkout.spec.ts (Cucumber step)
When('the returning customer places an order', async () => {
  await actorCalled('Maya').attemptsTo(
    PlaceOrder.as('maya@example.com'),
  );
});

CheckoutPage here is a lean UI map — just PageElement definitions, no methods. All behavior lives in Tasks. When the post-submit flow branches, you compose a new Task rather than adding a conditional to a page method. A team at a mid-size e-commerce platform reported reducing duplicated step-definition logic by ~60% after migrating their checkout and returns flows to Screenplay — suite maintenance time dropped from roughly 6 hours/week to under 2. That's a real but conditional win: they had 14 overlapping persona flows. With 3 flows, the overhead wouldn't have justified the migration.

When to Choose Which

  • Use POM when: your suite has fewer than ~200 scenarios, a single actor persona, and a stable UI. Playwright's built-in fixtures and component locators already give you most of POM's benefits with minimal boilerplate.
  • Use Screenplay when: you have multiple named actors with distinct capabilities, cross-cutting Tasks (e.g., API-seeded auth + UI action sequences), or you're running Cucumber-JVM 7 / SpecFlow 4 and want your step definitions to read like the Gherkin they implement.
  • Don't migrate mid-project: a half-POM, half-Screenplay codebase is harder to maintain than either pattern applied consistently. Pick before the suite reaches 100 scenarios.

Where Both Patterns Break: Pitfalls Senior Engineers Still Hit

Anemic Page Objects with leaked waits. The most common POM failure mode isn't structural — it's behavioral. Engineers add page.waitForTimeout(2000) inside page methods when a proper wait condition is inconvenient to find. The method works locally, fails on a loaded CI runner (GitHub Actions 2-core), and the fix is another hard-coded delay. Playwright's expect(locator).toBeVisible() with a configured timeout in playwright.config.ts eliminates this; use it at the assertion layer, not inside page methods.

Screenplay Tasks that are too granular. Teams new to Screenplay often create a Task per interaction — ClickButton, FillField — which is just POM with extra ceremony. Tasks should encode user intent at the scenario level: PlaceOrder, RefundItem, SwitchToBusinessAccount. If a Task name doesn't map to a Gherkin step, it's probably at the wrong abstraction level. This mistake is a mental-model problem: engineers trained on POM default to "what does the UI need?" rather than "what is the actor trying to accomplish?"

Myths That Keep Teams Stuck on the Wrong Pattern

Myth: Screenplay is the "advanced" version of POM, so you should eventually migrate. Screenplay is a different model optimized for different constraints — not an upgrade. A 150-scenario Playwright suite with clean POM and well-scoped fixtures will outperform a Screenplay suite in readability, onboarding speed, and maintenance cost. Pattern choice is a function of your actor model and reuse surface, not seniority. Teams that migrate for prestige rather than need consistently regret it after six months.

Myth: POM doesn't work with BDD. POM integrates cleanly with Behave, Cucumber-JVM 7, and SpecFlow 4. The Gherkin-to-step-to-page-method chain is well-understood and widely tooled. The real limitation is that POM step definitions tend to describe UI mechanics ("clicks the submit button") rather than user intent ("places an order"). That's a writing discipline problem, not a POM problem. Enforcing a rule that step definitions never reference locators or page internals fixes it without requiring a pattern change.

The decision reduces to one question: does your test suite model multiple actors with overlapping, composable capabilities? If yes, Screenplay's indirection pays for itself. If no, a well-disciplined POM with Playwright 1.44's component locators and fixture scoping is the lower-cost, higher-velocity choice. Once you've settled the pattern, the next thing worth measuring is locator stability under UI refactors — track selector-change-driven test failures as a separate metric in your CI dashboard to catch POM brittleness before it compounds.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles