level 04 / screenplay-and-fluent

Screenplay & Fluent Patterns

Beyond POM — the Screenplay pattern's actors, tasks, and questions, plus fluent chaining. When each pattern earns its complexity.

Screenplay: the cast

Screenplay reorganises automation around actors who perform tasks composed of interactions, and ask questions about state.

Actor      — who: a user with abilities (browse the web, call APIs)
Ability    — what they can use: BrowseTheWeb(page), CallAnApi(request)
Task       — business action: Login.withCredentials(user, pass)
Interaction — atomic step: Click.on(loginButton), Enter.text(...)
Question   — state query: TheCartTotal.value(), IsVisible.of(banner)
// Screenplay-style test — reads as a narrative
const james = Actor.named('James').whoCan(BrowseTheWeb.using(page));

await james.attemptsTo(
  Login.withCredentials('standard_user', 'secret_sauce'),
  AddToCart.theProduct('Sauce Labs Backpack'),
);

await james.asks(TheCartCount.value()).then(count => expect(count).toBe(1));
// A Task composes Interactions — and other Tasks
export const Login = {
  withCredentials: (user: string, pass: string): Task => ({
    async performAs(actor: Actor): Promise<void> {
      const page = BrowseTheWeb.as(actor).page;
      await page.getByPlaceholder('Username').fill(user);
      await page.getByPlaceholder('Password').fill(pass);
      await page.getByRole('button', { name: 'Login' }).click();
    },
  }),
};

What Screenplay buys you

  • Multi-actor scenarios are first-class. Two actors with different abilities (one browses, one calls APIs) model real workflows naturally.
  • Tasks compose. CompletePurchase = Login + AddToCart + Checkout — reuse without inheritance.
  • Single Responsibility everywhere. Each task is one small class/object; no god page objects.

What it costs

  • Indirection: five concepts before the first test runs.
  • Team onboarding: every new hire learns the pattern, not just Playwright.
  • Ecosystem mismatch: Playwright’s fixtures, auto-wait, and locators already solve many problems Screenplay was invented for (in Selenium-era Java).
Key insight: Honest guidance: most Playwright teams should run well-factored POM with fixtures and flow functions. Reach for Screenplay when you have multi-actor workflows, BDD reporting requirements, or an organisation already fluent in it (Serenity/JS shops).

Fluent pattern: chainable APIs

Fluent design makes each method return an object to continue from — this for same-page actions, the next page object for navigation:

export class CheckoutPage {
  async fillFirstName(name: string): Promise<this> {
    await this.firstNameInput.fill(name);
    return this;
  }

  async fillLastName(name: string): Promise<this> {
    await this.lastNameInput.fill(name);
    return this;
  }

  async fillPostcode(code: string): Promise<this> {
    await this.postcodeInput.fill(code);
    return this;
  }

  async continueToOverview(): Promise<OverviewPage> {
    await this.continueButton.click();
    return new OverviewPage(this.page);
  }
}
// Await once at the end of each chain segment
const overview = await (await (await checkout
  .fillFirstName('Ada'))
  .fillLastName('Lovelace'))
  .continueToOverview();

The nested awaits are the honest downside of async fluent chains in TypeScript. Two cleaner alternatives:

// Alternative 1: a single method taking a data object (usually best)
await checkout.fillShippingInfo({ firstName: 'Ada', lastName: 'Lovelace', postcode: 'SW1' });

// Alternative 2: collect steps, execute once
await checkout
  .with({ firstName: 'Ada', lastName: 'Lovelace' })
  .submit();

Pattern selection

Well-factored POM + fixtures
Default for Playwright teams — lowest concept count
Flow functions over POM
Multi-page journeys (purchaseFlow) — add when journeys repeat
Component objects
Shared UI (header, grid, modal) — add when pages share parts
Fluent chaining
Long form-filling sequences — prefer data-object methods first
Screenplay
Multi-actor workflows, BDD orgs, Serenity/JS ecosystems

Interview questions