Abilities

An "ability" is one of the five building blocks of the Screenplay Pattern. It's what enables the actor to interact with the system under test.

In more technical terms, the Ability is an implementation of the adapter pattern. An "ability" is a thin wrapper around a lower-level, interface-specific client such as a web browser driver, a HTTP client, a database client and so on, that you'd call from an interaction or question class to access the system under test.

The Screenplay Pattern
The Screenplay Pattern

Implementing Abilities

Consider the BrowseTheWeb ability from the @serenity-js/protractor module:

export class BrowseTheWeb implements Ability {

    static using(browser: ProtractorBrowser): BrowseTheWeb {
        return new BrowseTheWeb(browser);
    }

    static as(actor: UsesAbilities): BrowseTheWeb {
        return actor.abilityTo(BrowseTheWeb);
    }

    constructor(private readonly browser: ProtractorBrowser) {
    }

    locate(locator: Locator): ElementFinder {
        return this.browser.element(locator);
    }

    // rest omitted for brevity
}

In order to enable the actor to BrowseTheWeb, we'd give it an instance of said ability, linked with the lower-level client - in this case protractor.browser:

import { Actor } from '@serenity-js/protractor';
import { BrowseTheWeb } from '@serenity-js/protractor';
import { protractor } from 'protractor';

const actor = Actor.named('Abby').whoCan(BrowseTheWeb.using(protractor.browser));

So how would our actor make use of their newly acquired ability? That's what interactions are for!

Let's take the below, much simplified, version of the original Click interaction, where BrowseTheWeb is used to locate the element of interest and perform the .click():

import { Question } from '@serenity-js/core';
import { BrowseTheWeb } from '@serenity-js/protractor';
import { Locator } from 'protractor';

const Click = {
    onElement: (locator: Locator) =>
        Interaction.where(`#actor clicks on the ${ target }`, actor => {
            return BrowseTheWeb.as(actor).locate(this.locator).click();
        }),
}

With the interaction in place, we can tell the actor to perform the click as part of their interaction flow, and therefore indirectly invoke their ability to BrowseTheWeb:

actor.attemptsTo(
    Click.onElement(by.css('button')),
)

If you're wondering why you'd want to create a wrapper around the lower-level client you could just call directly from an interaction, think about all the opportunities having a dedicated ability class presents:

PRO TIP: This is a simplified implementation of the Click interaction that doesn't account for the many quirks of the Protractor API and is intended for demonstration purposes only. Please use the original Click in your tests instead.