Skip to main content

abstractAbility

Abilities enable actors to perform interactions with the system under test and answer questions about its state.

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 a HTTP client. They also enable portability of your test code across such integration libraries.

Abilities are the core building block of the Screenplay Pattern, along with Actors, Interactions, Questions, and Tasks.

Learn more about:

Giving actors the abilities to interact

Serenity/JS actors are capable of interacting with any interface of the system under test, be it a web UI, a mobile app, a web service, or anything else that a Node.js program can talk to. This flexibility is enabled by a mechanism called abilities and achieved without introducing any unnecessary dependencies to your code base thanks to the modular architecture of Serenity/JS.

Remember

Actors have abilities that enable them to perform interactions and answer questions.

From the technical perspective, an ability is an adapter around an interface-specific integration library, such as a web browser driver, an HTTP client, a database client, and so on. You give an actor an ability, and it’s the ability’s responsibility to provide a consistent API around the integration library and deal with any of its quirks. Abilities encapsulate integration libraries and handle their configuration and initialisation, the process of freeing up any resources they hold, as well as managing any state associated with the library.

Portable interactions with web interfaces

To make your Serenity/JS actors interact with web interfaces, you call the Actor.whoCan method and give them an implementation of the ability to BrowseTheWeb, specific to your web integration tool of choice.

Note how BrowseTheWebWithPlaywright, BrowseTheWebWithWebdriverIO, and BrowseTheWebWithProtractor all extend the base ability to BrowseTheWeb.

Playwright Test

import { actorCalled } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright' // Serenity/JS integration module
import { chromium } from 'playwright'

const browser = await chromium.launch({ headless: true }) // integration library

actorCalled('Trevor') // generic actor
.whoCan(BrowseTheWebWithPlaywright.using(browser)) // tool-specific ability

WebdriverIO

import { actorCalled } from '@serenity-js/core'
import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio' // Serenity/JS integration module

actorCalled('Trevor') // generic actor
.whoCan(BrowseTheWebWithWebdriverIO.using(browser)) // tool-specific ability

Protractor

import { actorCalled } from '@serenity-js/core'
import { BrowseTheWebWithProtractor } from '@serenity-js/protractor' // Serenity/JS integration module
import { protractor } from 'protractor' // integration library

actorCalled('Trevor') // generic actor
.whoCan(BrowseTheWebWithProtractor.using(protractor.browser)) // tool-specific ability

Retrieving an ability

Use Ability.as to retrieve an ability in a custom Interaction or Question implementation.

Here, Ability can be the integration library-specific class, for example BrowseTheWebWithPlaywright, or its library-agnostic parent class, like BrowseTheWeb.

To make your code portable across the various integration libraries, retrieve the ability using the library-agnostic parent class:

import { actorCalled } from '@serenity-js/core'
import { BrowseTheWeb } from '@serenity-js/web' // Serenity/JS web module

const actor = actorCalled('Trevor')
const ability = await BrowseTheWeb.as(actor) // retrieve implementation of BrowseTheWeb

As you can already see, providing encapsulation and a cleaner API around the integration libraries are not the only reasons why you’d want to use the abilities.

Another reason is that the Serenity/JS implementation of the Screenplay Pattern lets you completely decouple the actor from the integration libraries and make the abilities of the same type interchangeable. For example, Serenity/JS web modules offer an abstraction that lets you switch between web integration libraries as vastly different as Selenium, WebdriverIO, or Playwright without having to change anything whatsoever in your test scenarios.

What this means is that your test code can become portable and reusable across projects and teams, even if they don’t use the same low-level integration tools. It also helps you to avoid vendor lock-in, as you can wrap any third-party integration library into an ability and swap it out for another implementation if you need to.

However, Serenity/JS doesn’t prevent you from using the integration libraries directly. When you need to, you can use a library-specific ability like BrowseTheWebWithPlaywright to trade portability for access to library-specific low-level methods:

import { actorCalled } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright, PlaywrightPage } from '@serenity-js/playwright'

const actor = actorCalled('Trevor')
const ability = await BrowseTheWebWithPlaywright.as(actor)
const page = (await ability.currentPage()) as PlaywrightPage;
const playwrightPage = await page.nativePage();
// use any Playwright-specific APIs on playwrightPage
Using integration library-specific APIs reduces portability

While Serenity/JS provides you with escape hatches and ways to access the low-level APIs of your integration libraries, doing so can reduce the portability of your code. Only do it when you have a good reason to trade portability for low-level access.

Associating actors with data

One more reason to use abilities is that abilities can also help you to associate actors with data they need to perform their activities. For example, a commonly used ability is one to TakeNotes, which allows your actors to start the test scenario equipped with some data set, or record information about what they see in the test scenario so that they can act upon it later:

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

interface MyNotes {
firstName: string;
lastName: string;
emailAddress: string;
}

await actorCalled('Trevor')
.whoCan(
TakeNotes.using(Notepad.with<MyNotes>({
firstName: 'Trevor',
lastName: 'Traveller',
emailAddress: 'Trevor.Traveller@example.org',
}))
)

Actors with multiple abilities

Of course, an actor can have any number of abilities they need to play their role. For example, it is quite common for an actor to be able to BrowseTheWeb, TakeNotes, and CallAnApi:

import { actorCalled, Notepad, TakeNotes } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import { CallAnApi } from '@serenity-js/rest'
import { chromium } from 'playwright'

const browser = await chromium.launch({ headless: true })
const baseURL = 'https://example.org'

interface MyNotes {
firstName: string;
lastName: string;
emailAddress: string;
}

await actorCalled('Trevor')
.whoCan(
BrowseTheWebWithPlaywright.using(browser, { baseURL }),
CallAnApi.at(`${ baseURL }/api`),
TakeNotes.using(Notepad.with<MyNotes>({
firstName: 'Trevor',
lastName: 'Traveller',
emailAddress: 'Trevor.Traveller@example.org',
}))
)

Writing custom abilities

If your system under test provides a type of interface that Serenity/JS doesn’t support yet, you might want to implement a custom Ability, as well as interactions and questions to interact with it.

The best way to start with that is for you to review the examples in the Screenplay Pattern API docs, as well as the Serenity/JS code base on GitHub. Also note that all the Serenity/JS modules have their automated tests written in such a way to not only provide an extremely high test coverage for the framework itself, but to be accessible and act as a reference implementation for you to create your own integrations.

If you believe that the custom integration you’ve developed could benefit the wider Serenity/JS community, please consider open-sourcing it yourself, or contributing it to the main framework.

Defining a custom ability to MakePhoneCalls

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

class MakePhoneCalls extends Ability {

// A static method is typically used to inject a client of a given interface
// and instantiate the ability, for example:
// actorCalled('Phil').whoCan(MakePhoneCalls.using(phone))
static using(phone: Phone) {
return new MakePhoneCalls(phone);
}

// Abilities can hold state, for example: the client of a given interface,
// additional configuration, or the result of the last interaction with a given interface.
protected constructor(private readonly phone: Phone) {
}

// Abilities expose methods that enable Interactions to call the system under test,
// and Questions to retrieve information about its state.
dial(phoneNumber: string): Promise<void> {
// ...
}
}

Defining a custom interaction using the custom ability

// A custom interaction using the actor's ability:
const Call = (phoneNumber: string) =>
Interaction.where(`#actor calls ${ phoneNumber }`, async actor => {
await MakePhoneCalls.as(actor).dial(phoneNumber)
})

Using the custom ability and interaction in a test scenario

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

await actorCalled('Connie')
.whoCan(MakePhoneCalls.using(phone))
.attemptsTo(
Call(phoneNumber)
)

Using auto-initialisable and auto-discardable abilities

Abilities that rely on resources that need to be initialised before they can be used, or discarded before the actor is dismissed can implement the Initialisable or Discardable interfaces, respectively.

Defining a custom ability to QueryPostgresDB

import {
Ability, actorCalled, Discardable, Initialisable, Question, UsesAbilities,
} from '@serenity-js/core'

// Some low-level interface-specific client we want the Actor to use,
// for example a PostgreSQL database client:
const { Client } = require('pg');

// A custom Ability to give an Actor access to the low-level client:
class QueryPostgresDB
extends Ability
implements Initialisable, Discardable
{
constructor(private readonly client) {
}

// Invoked by Serenity/JS upon the first invocation of `actor.attemptsTo`
initialise(): Promise<void> | void {
return this.client.connect();
}

// Used to ensure that the Ability is not initialised more than once
isInitialised(): boolean {
return this.client._connected;
}

// Discards any resources the Ability uses when the Actor is dismissed,
// so when the Stage receives a SceneFinishes event for scenario-scoped actor,
// or TestRunFinishes for cross-scenario-scoped actors
discard(): Promise<void> | void {
return this.client.end();
}

// Any custom integration APIs the Ability, should make available to the Actor.
// Here, we want the ability to enable the actor to query the database.
query(query: string) {
return this.client.query(query);
}

// ... other custom integration APIs
}

Defining a custom question using the custom ability

// A custom Question to allow the Actor query the database
const CurrentDBUser = () =>
Question.about('current db user', actor =>
QueryPostgresDB.as(actor)
.query('SELECT current_user')
.then(result => result.rows[0].current_user)
);

Using the custom ability and question in a test scenario

// Example test scenario where the Actor uses the Ability to QueryPostgresDB
// to assert on the username the connection has been established with

import { describe, it } from 'mocha'
import { actorCalled } from '@serenity-js/core'
import { Ensure, equals } from '@serenity-js/assertions'

describe('Serenity/JS', () => {
it('can initialise and discard abilities automatically', () =>
actorCalled('Debbie')
.whoCan(new QueryPostgresDB(new Client()))
.attemptsTo(
Ensure.that(CurrentDBUser(), equals('jan'))
))
})

Learn more

Hierarchy

Index

Constructors

Methods

Constructors

constructor

Methods

staticas

  • Used to access an actor’s ability of the given type from within the Interaction and Question classes.

    Retrieving an ability in an interaction definition

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

    export const ClearLocalStorage = () =>
    Interaction.where(`#actor clears local storage`, async (actor: Actor) => {
    const browseTheWeb: BrowseTheWeb = BrowseTheWeb.as(actor) // retrieve an ability
    const page: Page = await browseTheWeb.currentPage()
    await page.executeScript(() => window.localStorage.clear())
    })

    Retrieving an ability in a question definition

    import { Actor, Question } from '@serenity-js/core'
    import { BrowseTheWeb, Page } from '@serenity-js/web'
    import { CallAnApi } from '@serenity-js/rest'

    const LocalStorage = {
    numberOfItems: () =>
    Question.about<number>(`number of items in local storage`, async (actor: Actor) => {
    const browseTheWeb: BrowseTheWeb = BrowseTheWeb.as(actor) // retrieve an ability
    const page: Page = await browseTheWeb.currentPage()
    return page.executeScript(() => window.localStorage.length)
    })
    }

    Type parameters

    Parameters

    Returns A