Skip to main content

Screenplay Pattern

The Screenplay Pattern is an innovative, user-centred approach to writing high-quality automated acceptance tests. It steers your team towards effectively using layers of abstraction, helps your test scenarios capture the business vocabulary of your domain, and encourages good testing and software engineering habits.

Focusing on actors and their goals and incorporating your domain language into test scenarios improves team collaboration and alignment, enabling technical and business stakeholders to understand and readily contribute to the test automation process.

Serenity/JS implementation of the Screenplay Pattern enables developers to easily introduce this design approach even into existing test automation projects. Moreover, the framework provides integration libraries to facilitate various test automation types, including end-to-end, component, mobile and API testing, making it a versatile choice for different testing needs. Serenity/JS also provides reporting tools and code reuse patterns that facilitate sharing test code across projects and teams and reducing maintenance costs.

The design principleโ€‹

The design principle behind the Screenplay Pattern is simple but might forever change the way you look at test automation:

Remember

Automated acceptance tests should use your domain language to clearly express what activities the actors interacting with your system need to perform in order to accomplish their goals.

Applying this design principle to your automated tests has a number of positive implications:

  • Expressing your test scenarios in your domain language makes them easier to understand and accessible to a wider audience
  • Focusing on actors and their goals makes it easy to correlate any test failures with the actual business impact
  • Modelling actor workflows using sequences of business-focused, reusable activities reduces code duplication, improves flexibility of your test code base, and means that your team can quickly compose new test scenarios from existing steps

To communicate effectively, the code must be based on the same language used to write the requirementsโ€”the same language that the developers speak with each other and with domain experts.

โ€• Eric Evans, "Domain-Driven Design: Tackling Complexity in the Heart of Software"

The five elements of the Screenplay Patternโ€‹

The Screenplay Pattern uses the system metaphor of a stage performance, helping you model each test scenario like a little screenplay describing how the actors should go about performing their activities while interacting with the system under test.

Following the Screenplay Pattern helps you capture:

  • Who the actors interacting with your system are
  • Why they interact with your system
  • What they need to do to accomplish their goals
  • How exactly they would go about that

The five building blocks of the Screenplay Pattern are:

  • Actors, who represent people and external systems interacting with the system under test
  • Abilities, that act as thin wrappers around any integration libraries required to interact with the system under test
  • Interactions, which represent the low-level activities an actor can perform using a given interface
  • Tasks, used to model sequences of activities as meaningful steps of a business workflow in your domain
  • Questions, used to retrieve information from the system under test and the test execution environment
Five elements of the Screenplay Pattern

Screenplay Pattern with Serenity/JSโ€‹

The best way to illustrate the Screenplay Pattern is through a practical example, so assume for a moment that we're writing a test scenario for an online shop. The shop has a REST API that lets us configure its product catalogue with some test data, and a web storefront that lets customers find the products they need and make a purchase.

We'll create a test scenario that uses two actors: one to set up the test data, and one to interact with the web UI.

Serenity/JS Project Templates

To follow along with the coding, get one of the Serenity/JS Project Templates as they come with everything you need to get started with Serenity/JS.

Actorsโ€‹

A test scenario following the Screenplay Pattern has one or multiple actors representing people and external systems interacting with the system under test and playing specific roles.

The role of an actor

Just like the five core elements of the Screenplay Pattern, the term "role" comes from the system metaphor of a stage performance. It should be interpreted as the role a given actor plays in the performance, the role they play in the system. Some good examples of roles include "a doctor", "a trader", or "a writer".

While a "role" might imply the permissions a given actor has in the system they interact with (e.g. a "writer" can write articles, but only a "publisher" publishes articles), this is not a mechanism to prevent the actor from performing activities inconsistent with their role.

In particular, Serenity/JS will not prevent you from writing scenarios where a "writer" tries to impersonate a "publisher" and publish an article. If it did, you would not be able to test if your system correctly implemented its access control mechanisms!

Our example scenario could have two actors, who we'll call:

  • Apisitt, responsible for setting up test data using the REST API
  • Wendy, representing a customer interacting with the web UI

Instantiating and retrieving actorsโ€‹

With Serenity/JS, you can instantiate new actors or retrieve the ones you've already referenced in the given scenario using the function actorCalled and providing the name of the actor:

spec/screenplay-pattern.ts
import { actorCalled } from '@serenity-js/core'

actorCalled('Apisitt') // returns: Actor(name='Apisitt')
actorCalled('Wendy') // returns: Actor(name='Wendy')

Note that every Serenity/JS actor is uniquely identified by their name. The first time you call actorCalled('Wendy'), Serenity/JS instantiates a new Actor and stores a reference to it internally under the name you gave it. This way, whenever you call actorCalled('Wendy') within the same scenario again, you'll get the same actor instance back.

spec/screenplay-pattern.ts
import assert from 'node:assert/strict'
import { actorCalled } from '@serenity-js/core'

const wendy1 = actorCalled('Wendy') // first invocation of actorCalled
const wendy2 = actorCalled('Wendy') // second invocation of actorCalled

assert.equal(wendy1, wendy2) // wendy1 === wendy 2

To avoid typos and repetition when instantiating and retrieving actors in your test scenarios, you might want to consider using string enums or constants to store actor names:

spec/screenplay-pattern.ts
import { actorCalled } from '@serenity-js/core'

enum ActorNames {
Apisitt = 'Apisitt, the test data manager',
Wendy = 'Wendy, the customer',
}

actorCalled(ActorNames.Apisitt) // returns: Actor(name='Apisitt')
actorCalled(ActorNames.Wendy) // returns: Actor(name='Wendy')

Using actors with test runnersโ€‹

While you could use Serenity/JS and the actorCalled function as part of any regular Node.js program, you'll typically use it with a test runner such as Playwright Test, Cucumber.js, Mocha, or Jasmine.

To help you ensure no state leakage between test scenarios, Serenity/JS test runner adapters will also automatically dismiss any actors instantiated within the scope of a test scenario and free up the resources they were using when the scenario finishes.

Note that since different test runners have different APIs, the way you retrieve actors might vary slightly depending on the test runner you use:

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'


describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {
actorCalled('Apisitt') // returns: Actor(name="Apisitt")
actorCalled('Wendy') // returns: Actor(name="Wendy")
})
})

Abilitiesโ€‹

Actors have abilities that enable them to interact with the various interfaces of the system under test and the test execution environment.

From the technical perspective, abilities act as wrappers around any integration libraries required to communicate with the external interfaces of system under test, such as web browser drivers or an HTTP client, or hold state to allow actors to remember retrieved information. Abilities also enable portability of your test code across various lower-level integration libraries as they expose a standardised API.

Did you know?

The word "screen" in "screenplay" has nothing to do with the computer screen. On the contrary, the Screenplay Pattern is a general method of modelling acceptance tests interacting with any external interface of your system. In fact, Serenity/JS implementation of the Screenplay Pattern can help you break free from UI-only-based testing!

To allow Apisitt to interact with a REST API, we'll give him the ability to CallAnApi, wrapping an instance of an Axios HTTP client:

spec/screenplay-pattern.ts
import axios from 'axios'
import { actorCalled } from '@serenity-js/core'
import { CallAnApi } from '@serenity-js/rest'

const axiosInstance = axios.create({ baseURL 'https://api.example.org/' })

const actor = actorCalled('Apisitt')
.whoCan(CallAnApi.using(axiosInstance))

// ...

At the same time, to allow Wendy to interact with the web UI we'll give her an instance of a Playwright browser, although she could use any other popular web browser driver supported by Serenity/JS:

spec/screenplay-pattern.ts
import { actorCalled } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'

import { chromium } from 'playwright'

async function example() {
let browser

try {
browser = await chromium.launch({ headless: false })

const actor = actorCalled('Wendy')
.whoCan(BrowseTheWebWithPlaywright.using(browser))

// ...
}
finally {
if (browser) {
await browser.close()
}
}
}

Even though the actorCalled function makes instantiating and retrieving actors straightforward, setting up browser drivers, HTTP clients, and similar libraries can be more involved. Thankfully, Serenity/JS has a mechanism to help you with that, as you'll see in the next section.

Using default test runner configurationโ€‹

Since configuring integration libraries like Axios or Playwright is a common task in test automation, Serenity/JS test runner adapters use your test runner's configuration to set up the abilities for you. And so, Serenity/JS adapters for the following test runners will automatically give every actor you retrieve via actorCalled the below abilities so that you don't have to configure them yourself:

To retrieve actor's ability to do something, you can use the Actor.abilityTo method, which returns an implementation of the ability if the actor has it, or throws a ConfigurationError if they don't. While you wouldn't retrieve the abilities directly in your test scenarios, you'd use this mechanism in your custom interactions and questions.

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'
import { BrowseTheWeb } from '@serenity-js/web'
import { CallAnApi } from '@serenity-js/rest'
import { TakeNotes } from '@serenity-js/core'
import assert from 'node:assert/strict'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {
assert.ok(actorCalled('Wendy').abilityTo(BrowseTheWeb))
assert.ok(actorCalled('Wendy').abilityTo(CallAnApi))
assert.ok(actorCalled('Wendy').abilityTo(TakeNotes))

// ...
})
})

As this simple test scenario shows, Serenity/JS provides your actors with the default abilities to BrowseTheWeb, CallAnApi and TakeNotes without you having to modify any configuration and without writing any additional code. All you need to do is to create/retrieve the Actor by calling actorCalled(<name>).

Portable test code

Playwright Test, WebdriverIO and Protractor test runners vary dramatically and have completely different and incompatible APIs. This would normally tie your test suite to a specific integration tool and prevent you from being able to run test scenarios written for one test runner using another.

To solve this problem and allow your test suite to be agnostic of the underlying integration tool, Serenity/JS test runner adapters configure the default cast of actors with an ability to BrowseTheWeb specific to the given test runner, so: BrowseTheWebWithPlaywright, BrowseTheWebWithWebdriverIO, or BrowseTheWebWithProtractor, but exposing a consistent API across all test runners.

This design allows you to write test scenarios that interact with the web UI without having to worry about the underlying integration tool and makes your test code portable across different test runners.

Using a custom cast of actorsโ€‹

Following the system metaphor of a stage performance, Serenity/JS uses a cast of actors to configure the default abilities the actors receive upon instantiation.

If you'd like to modify those defaults, or if you use Serenity/JS with Cucumber, Jasmine, or Mocha without a WebdriverIO or Protractor wrapper that set up the defaults automatically, you can instruct Serenity/JS to use a custom cast.

spec/online_shop.spec.ts
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import { describe, it, test } from '@serenity-js/playwright-test'
import { Cast, TakeNotes, Notepad } from '@serenity-js/core'

test.use({
// Replace the entire default cast:
actors: async ({ contextOptions, page }, use): Promise<void> => {
const cast = Cast.where(actor => actor.whoCan(
BrowseTheWebWithPlaywright.usingPage(page, contextOptions),
TakeNotes.usingAnEmptyNotepad(),
// ... other abilities
))

await use(cast)
},

// Alternatively, wrap actorCalled to give actors additional abilities
// or override the default ones:
actorCalled: async ({ actorCalled }, use) => {
await use((name: string) => {
return actorCalled(name).whoCan(
TakeNotes.using(Notepad.with({
firstName: name,
lastName: 'Tester',
}))
// ... other abilities
)
})
},
})

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {
actorCalled('Apisitt') // returns: Actor(name="Apisitt")
actorCalled('Wendy') // returns: Actor(name="Wendy")
})
})

If you're using Cucumber, Jasmine, or Mocha without a WebdriverIO or Protractor wrapper, you can configure the default cast of actors using the configure and engage functions, as per the test runner adapter configuration.

Learn more about:

Interactionsโ€‹

Abilities enable actors to perform interactions with the system under test. Interactions are command objects that instruct an actor how to use their abilities to perform the given activity. Most interactions you will need are already provided by Serenity/JS modules, and you can easily create new ones if you'd like to.

To instruct an actor to attempt to perform a sequence of interactions, use the Actor.attemptsTo method. Note that this method returns a Promise that resolves when the actor has completed the interactions, or rejects if any of the interactions fail, so you can use it with await in an async function.

Here, we instruct Apisitt to use the interaction to Send.a(HTTPRequest) from the Serenity/JS REST module to set up some test data for our test scenario:

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'
import { Send, PostRequest } from '@serenity-js/rest'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {

await actorCalled('Apisitt').attemptsTo( // - actors attempt to perform interactions
Send.a(PostRequest.to('/products').with([ // - interactions like `Send` are command objects,
{ name: 'Apples', price: 'ยฃ2.50' } // that instruct actors how to use their abilities
])),
)

// ...
})
})

In the same manner, we can instruct Wendy to use interactions from the Serenity/JS Web module to navigate to the web interface of our system under test:

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'
import { Send, PostRequest } from '@serenity-js/rest'
import { Navigate } from '@serenity-js/web'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {

await actorCalled('Apisitt').attemptsTo( // - actors attempt to perform interactions
Send.a(PostRequest.to('/products').with([ // - interactions like `Send` are command objects,
{ name: 'Apples', price: 'ยฃ2.50' } // that instruct actors how to use their abilities
])),
)

await actorCalled('Wendy').attemptsTo(
Navigate.to('https://example.org'), // - all Serenity/JS interactions have a consistent API,
) // no matter the interface they're interacting with
})
})

Note how the interaction to Navigate.to(url) comes from the @serenity-js/web module and works regardless of the underlying web browser driver used by the actor, making your test code portable across different web integration tools. Under the hood, the interaction requests the actor's ability to BrowseTheWeb and Serenity/JS returns the appropriate implementation of the ability.

If you wanted to implement an interaction to navigateTo(url) yourself, you might do it like this:

import { Interaction } from '@serenity-js/core'
import { BrowseTheWeb } from '@serenity-js/web'

const navigateTo = (url: string) =>
Interaction.where(`#actor navigates to ${ url }`, async actor => {
const page = await BrowseTheWeb.as(actor).currentPage()

return page.navigateTo(url)
})

Another thing to note about the code samples in this section is that even though Playwright Test, Mocha, and Cucumber have completely different APIs, the way we instruct actors to perform interactions is consistent across all test runners. This design makes it easier for you to migrate your test scenarios from one test runner to another if needed. It also enables you to make your test suite use different test runners for different types of test scenarios while reusing the same underlying test automation code.

For example, you might want to use Serenity/JS with Cucumber for business-facing scenarios, Playwright Test for UI component tests, and with Mocha for REST API tests.

Learn more about:

Questionsโ€‹

Apart from enabling interactions, abilities also enable actors to answer questions about the state of the system under test and the test execution environment. More specifically, questions instruct actors how to use their abilities to retrieve information when the activity is performed and provide a way to parameterise activities.

When Apisitt uses his ability to CallAnApi to Send a PostRequest, the result of this interaction is stored in the ability. To retrieve its value, we instruct the actor to answer a question about the LastResponse, such as LastResponse.status(). This declarative approach to information retrieval not only makes your test scenarios more readable, it provides a consistent API for you to design assertions, waiting and synchronisation statements, and perform data transformations.

For example, to instruct Apisitt to assert on the result of the HTTP request, we provide him with an interaction to Ensure.that, parameterised with a question about the LastResponse.status() and an expectation that the status code equals 201:

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'
import { Ensure, equals } from '@serenity-js/assertions'
import { Send, PostRequest, LastResponse } from '@serenity-js/rest'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {

await actorCalled('Apisitt').attemptsTo(
Send.a(PostRequest.to('/products').with([
{ name: 'Apples', price: 'ยฃ2.50' }
])),
Ensure.that(
LastResponse.status(),
equals(201)
)
)

// ...
})
})

An excellent proof of the design consistency enabled by the Serenity/JS Screenplay Pattern is that even though Wendy uses a web browser and not an API client, the way we instruct her to answer web questions and perform web assertions is identical with how you'd interact with any other interface:

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'
import { Ensure, equals, endsWith } from '@serenity-js/assertions'
import { Send, PostRequest, LastResponse } from '@serenity-js/rest'
import { Navigate, Page } from '@serenity-js/web'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {

await actorCalled('Apisitt').attemptsTo(
Send.a(PostRequest.to('/products').with([
{ name: 'Apples', price: 'ยฃ2.50' }
])),
Ensure.that(
LastResponse.status(),
equals(201)
)
)

await actorCalled('Wendy').attemptsTo(
Navigate.to('https://example.org'),
Ensure.that(
Page.current().title(),
endsWith('My Example Shop'),
)
)
})
})

Learn more about:

Tasksโ€‹

The idea that underpins the Screenplay Pattern is to capture your domain language and use your acceptance tests as an opportunity to demonstrate how actors interacting with your system accomplish their goals.

Conceptually similar to standard JavaScript functions, tasks offer an easy way to associate business meaning with sequences of activities and turn them into reusable building blocks from which your team can assemble test scenarios.

For example, we can use Serenity/JS convenient Task.where method to define custom tasks that capture how an actor would set up a product catalogue or open an online store:

spec/tasks.ts
import { Task } from '@serenity-js/core'
import { Send, PostRequest, LastResponse } from '@serenity-js/rest'
import { Navigate, Page } from '@serenity-js/web'
import { Ensure, equals, endsWith } from '@serenity-js/assertions'

export interface Product {
name: string;
price: string;
}

export const setupProductCatalogue = (products: Product[]) =>
Task.where(`#actor sets up the product catalogue`,
Send.a(PostRequest.to('/products').with(products)),
Ensure.that(
LastResponse.status(),
equals(201)
)
)

export const openOnlineStore = () =>
Task.where(`#actor opens the online store`,
Navigate.to('https://example.org'),
Ensure.that(
Page.current().title(),
endsWith('My Example Shop'),
)
)

As you can see, custom tasks like these are easy to read and understand, and can be parameterised and reused across different test scenarios, test suites, or even across different projects and teams. Tasks can help you capture the domain language, provide a consistent way to structure your test scenarios, and make your test code reusable and easier to maintain.

spec/online_shop.spec.ts
import { describe, it } from '@serenity-js/playwright-test'

import { setupProductCatalogue, openOnlineStore } from './tasks'

describe('Online shop', () => {

it('should allow customers to find products of interest', async ({ actorCalled }) => {

await actorCalled('Apisitt').attemptsTo(
setupProductCatalogue([
{ name: 'Apples', price: 'ยฃ2.50' }
])
)

await actorCalled('Wendy').attemptsTo(
openOnlineStore(),
// ... other tasks, like findProductCalled('Apples'),
)
})
})

Learn more about:

Performing activities at multiple levelsโ€‹

The role of an actor is to perform activities that demonstrate how to accomplish a given goal.

Remember

Actors represent people and external systems interacting with the system under test.

For example, we might have an acceptance test that demonstrates how the system under test enables an actor to accomplish the goal of booking a plane ticket. If we were using Serenity/JS with a spec-style test runner like Jasmine, Mocha, or Playwright Test, we could implement such scenario like this:

import { describe, it, test } from '@serenity-js/playwright-test'

test.use({ defaultActorName: 'Trevor' })

describe('Serenity Airlines flight booking', () => { // system feature

it('should allow travellers to book a plane ticket', async ({ actor }) => { // scenario goal
await actor.attemptsTo(
findFlight('London', 'New York'), // activities
chooseFlightClass(FlightClass.Economy),
providePaymentDetails(defaultCard),
receiveBookingConfirmation(),
)
})
})

If we were using Cucumber.js, the name of the feature, the goal of the scenario, as well as the high-level steps necessary to achieve the goal would already be captured in our .feature files:

Feature: Serenity Airlines flight booking                                       # system feature

Scenario: traveller books a plane ticket # scenario goal

Given Trevor finds a flight from 'London' to 'New York' # high-level steps
And he chooses the 'Economy' flight class
When he provides his payment details
Then he should receive a booking confirmation

In this case, each Cucumber step definition is mapped to a Serenity/JS actor performing one or more activities. Completing those activities helps the actor accomplish the mini-goal of the associated Cucumber step:

import { Given } from '@cucumber/cucumber'
import { Actor } from '@serenity-js/core'

Given('{actor} finds a flight from {string} to {string}', // step goal
async (actor: Actor, origin: string, destination: string) => {
await actor.attemptsTo(
findFlight(origin, destination), // activities
)
}
)

Given(`{pronoun} chooses the '{flightClass}' flight class`, // step goal
async (actor: Actor, flightClass: FlightClass) => {
await actor.attemptsTo(
chooseFlightClass(flightClass), // activities
)
}
)

From the implementation perspective, functions like findFlight or providePaymentDetails produce activities, which are command objects that encapsulate information needed for the actor to perform some defined action.

The way you instruct an actor to perform some activities is always exactly the same, no matter the kind of test you write or the type of interface the actor interacts with - you pass them to the Actor.attemptsTo method. This method call makes the actor attempt to perform the activities one by one and returns a standard JavaScript Promise that's resolved when the process is finished, or rejected in case of any errors:

import { actorCalled } from '@serenity-js/core'

await actorCalled('Alice').attemptsTo(
activity1,
activity2,
activity3,
// ...
)
// returns: Promise<void>

Looking at activities from the design philosophy perspective, however, can be much more interesting.

The role of an actor is not just to perform any activities. It is to perform activities that demonstrate how a goal can be accomplished at the given level of abstraction. The interesting bit is that the way we describe what the goal is and the vocabulary we use to describe what activities it requires vary depending on the level of abstraction we're operating at.

Remember

Actors demonstrate how to accomplish a goal by performing activities at multiple levels of abstraction.

At the high levels of abstraction, e.g. in business-focused acceptance test scenarios, the vocabulary we use is rooted in the business domain, and so are the names we choose for the activities.

For example, an acceptance test scenario might state that for the system to enable the actor to accomplish the goal of booking a plane ticket, an actor should be able to successfully perform the following high-level activities:

  • find an appropriate flight connection,
  • choose flight class,
  • provide payment details,
  • receive booking confirmation.

The names we give functions that produce those activities, such as findFlight or chooseFlightClass, represent those steps in the business process and are agnostic of the interface through which actors interact with the system under test.

Model the expected process, not the existing implementation

When describing an acceptance test at a high level of abstraction, the way we name the activities is focused on representing the steps of the expected business process and not tied to the implementation of any specific interface of the system under test. "Find an appropriate flight connection", "choose flight class", or "provide payment details" are all good examples of such high-level activity names.

This design approach helps to produce test scenarios that are easier to read and understand and to a much wider audience than the traditional test scripts. It also results in two other major advantages:

  • once the business process is clearly described in our test scenario, we can often use our acceptance tests to identify obstacles in user journeys, or even highlight errors and hidden assumptions in the business process itself
  • since we're not tying the implementation to any particular interface, we leave ourselves more integration options when it comes to automation.

After all, most business process steps could be accomplished in different ways. An actor could findFlight by interacting with a web UI, a mobile app, by sending requests to a web service, or even by actually going to the ticket office at the airport!

At the low level of abstraction, e.g. in UI component tests, the vocabulary we use to describe actor's activities is focused on the interface the actor needs to interact with. Here the goal might be to use the web UI to specify the origin airport. To accomplish it, the actor would need to:

  • click on the origin airport widget
  • enter the name of the origin city, like London
  • pick the first suggested airport from the list
Remember

The core idea behind the Screenplay Pattern is to express the acceptance tests from the perspective of actors playing a certain role and attempting to accomplish their goals by performing activities at multiple levels of abstraction.

Of course, most activities fall somewhere in between the high and low levels of abstraction. Furthermore, turns out that higher-level activities can be composed of lower-level activities, which themselves could be composed of even lower-level activities!

If you're familiar with User Experience Design, you might recognise this style of functional decomposition from Hierarchical Task Analysis.

The fascinating aspect of looking at your test scenarios as sequences of activities made up of activities, made up of activities, is that this mental model lends itself perfectly to functional composition and making activities the primary component of code reuse in Serenity/JS.

Start with Serenity/JS Screenplay Pattern ๐Ÿš€โ€‹

The easiest way to experience working with Serenity/JS and the Screenplay Pattern is to follow the tutorial and write your first web scenario!

When you're ready to start your own test automation project, use one of the available Serenity/JS Project Templates as they include a handful of Screenplay scenarios and combine some of the most popular configurations of Serenity/JS modules and test automation tools:

To dive even deeper, check out the Serenity/JS repository and explore the examples.

Try Serenity/JS in your browser

Thanks to Gitpod.io, you can follow the web testing tutorial and use any of the Serenity/JS Project Templates right here in your browser, no local installation required! ๐Ÿš€

History of the Screenplay Patternโ€‹

Serenity/JS introduced the Screenplay Pattern to JavaScript back in 2016, but the ideas behind the pattern have been around since 2007 in various forms.

This list is a chronological order of significant events, implementations, and writings related to the evolution of the Screenplay Pattern.

Credits