Skip to content
End To End Tester

The Screenplay Pattern

Actors, abilities, tasks and questions — a compositional alternative to page objects for suites with many user types and deep flows.

2 min read · updated 19 September 2026

The page object model organises automation around screens. The screenplay pattern organises it around actors performing tasks, which composes better when there are several kinds of user and the flows are deep.

java
// Java, Serenity Screenplay
alice.attemptsTo(
    SignIn.withCredentials(alice.usernameAndPassword()),
    AddToBasket.theProduct("Field Notes").times(2),
    CheckOut.payingWith(VISA_TEST_CARD)
);

alice.should(seeThat(TheOrder.status(), is("confirmed")));

Read it aloud and it is a user story. That is the design goal.

#The four pieces

Actor — who is doing it. Carries identity and state, which is what makes multi-user scenarios natural.

Ability — what the actor can do: browse the web, call an API, query the database. Abilities are how an actor gets a browser, and how the same task can be performed through different interfaces.

Task — a thing the user wants, composed of other tasks and interactions. CheckOut is a task; Click.on(PAY_BUTTON) is an interaction.

Question — something the actor can observe. TheOrder.status() returns a value the test asserts on.

typescript
// TypeScript, hand-rolled. The whole pattern is four small interfaces.
interface Ability { }

class BrowseTheWeb implements Ability {
  constructor(readonly page: Page) {}
  static as(actor: Actor): BrowseTheWeb { return actor.abilityTo(BrowseTheWeb); }
}

interface Task { performAs(actor: Actor): Promise<void>; }
interface Question<T> { answeredBy(actor: Actor): Promise<T>; }

class Actor {
  private abilities = new Map<Function, Ability>();

  constructor(readonly name: string) {}

  whoCan(...abilities: Ability[]): this {
    for (const ability of abilities) this.abilities.set(ability.constructor, ability);
    return this;
  }

  abilityTo<A extends Ability>(type: new (...args: never[]) => A): A {
    const ability = this.abilities.get(type);
    if (!ability) throw new Error(`${this.name} cannot ${type.name}`);
    return ability as A;
  }

  async attemptsTo(...tasks: Task[]) {
    for (const task of tasks) await task.performAs(this);
  }

  async asks<T>(question: Question<T>): Promise<T> {
    return question.answeredBy(this);
  }
}
typescript
// A task: composed, reusable, and named after the user's intent.
class AddToBasket implements Task {
  private constructor(private readonly product: string, private readonly quantity: number) {}

  static theProduct(product: string) {
    return { times: (quantity: number) => new AddToBasket(product, quantity) };
  }

  async performAs(actor: Actor) {
    const { page } = BrowseTheWeb.as(actor);
    await page.goto(`/products/${slugify(this.product)}`);
    await page.getByLabel('Quantity').fill(String(this.quantity));
    await page.getByRole('button', { name: 'Add to basket' }).click();
  }
}

// A question: observation, not assertion. The test decides what is expected.
const BasketCount: Question<number> = {
  async answeredBy(actor) {
    const { page } = BrowseTheWeb.as(actor);
    return Number(await page.getByTestId('basket-count').innerText());
  }
};

#What it buys over page objects

Multiple actors in one test, naturally. This is the biggest single advantage, and it is the reason to reach for the pattern at all.

typescript
const alice = new Actor('Alice').whoCan(new BrowseTheWeb(alicePage));
const bob = new Actor('Bob').whoCan(new BrowseTheWeb(bobPage));

await alice.attemptsTo(ShareDocument.with(bob).asCommenter());
await bob.attemptsTo(OpenTheDocument.sharedByAlice());

expect(await bob.asks(CanEdit.theDocument())).toBe(false);   // authorization, tested

Expressing that with page objects means two driver instances threaded through helper functions by hand. Here it falls out of the model. See authorization testing — this is the cheapest way to write those tests.

Tasks compose. CheckOut is used by twenty tests and built from EnterCard, ConfirmAddress, Submit. Change checkout once.

The same task, a different interface. An actor whoCan(CallAnApi) rather than BrowseTheWeb can perform AddToBasket through HTTP. Setup becomes fast without a second vocabulary — the point made in end-to-end testing about arranging via the API.

Reports that read like prose. Serenity generates documentation from the task names, which is the strongest form of living documentation going.

#What it costs

More indirection. A one-line click becomes an interaction class. For a small suite this is pure overhead, and page objects are the right answer.

More concepts for a new joiner. Actor, ability, task, interaction and question are five ideas before anyone writes a test.

Tooling support is thinner outside the JVM. Serenity is excellent; @serenity-js covers Node well; elsewhere you are hand-rolling, which is fine but is a decision to make deliberately.

#When to choose it

Reach for screenplay when at least two of these are true:

  • More than two distinct user roles whose permissions differ
  • Flows that reuse the same steps across many tests
  • Tests that need two users interacting
  • A UI that is not a set of pages
  • A suite large enough that the indirection cost is amortised

Otherwise use page objects, which are less code and perfectly adequate for most suites.

Common questions

Is the screenplay pattern worth the extra machinery?
For a suite of thirty tests with one user type, no — page objects are less code and just as clear. It starts paying somewhere around several user roles, tasks reused across many flows, or a UI that is not organised into pages.
Do I need Serenity BDD to use screenplay?
No. Serenity is the best-known implementation and gives you reporting for free, but the pattern is four small interfaces and can be hand-rolled in any language in an afternoon.
How does screenplay relate to the page object model?
It replaces the page as the unit of abstraction with the task. Page objects still exist underneath, usually as thin locator holders; what changes is that tests compose tasks performed by actors rather than calling methods on screens.

Runnable samples for this page

last test results ↗
  • TypeScripttypescript/src/practices/screenplay-pattern

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?