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