@serenity-js/core
Version:
The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure
440 lines • 21.1 kB
TypeScript
import type { AbilityType } from './AbilityType';
import type { SerialisedAbility } from './SerialisedAbility';
import type { UsesAbilities } from './UsesAbilities';
/**
* **Abilities** enable [actors](https://serenity-js.org/api/core/class/Actor/)
* to perform [interactions](https://serenity-js.org/api/core/class/Interaction/) with the system under test
* and answer [questions](https://serenity-js.org/api/core/class/Question/) 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](https://serenity-js.org/api/web/class/BrowseTheWeb/) or an [HTTP client](https://serenity-js.org/api/rest/class/CallAnApi/).
* They also enable [portability](https://serenity-js.org/handbook/design/portable-test-code)
* of your test code across such integration libraries.
*
* Abilities are the core building block of the [Screenplay Pattern](https://serenity-js.org/handbook/design/screenplay-pattern),
* along with [actors](https://serenity-js.org/api/core/class/Actor/), [interactions](https://serenity-js.org/api/core/class/Interaction/),
* [questions](https://serenity-js.org/api/core/class/Question/), and [tasks](https://serenity-js.org/api/core/class/Task/).
*
* 
*
* Learn more about:
* - [Actors](https://serenity-js.org/api/core/class/Actor/)
* - [Configuring actors using Casts](https://serenity-js.org/api/core/class/Cast/)
* - [Interactions](https://serenity-js.org/api/core/class/Interaction/)
* - [Questions](https://serenity-js.org/api/core/class/Question/)
* - [Web testing](https://serenity-js.org/handbook/web-testing/)
* - [API testing](https://serenity-js.org/handbook/api-testing/)
* - [Mobile testing](https://serenity-js.org/handbook/mobile-testing/)
*
* ## 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](https://serenity-js.org/handbook/web-testing/), a [mobile app](https://serenity-js.org/handbook/mobile-testing/), a [web service](https://serenity-js.org/handbook/api-testing/),
* or [anything else](https://serenity-js.org/api/core/class/Ability/) 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](https://serenity-js.org/handbook/architecture/) of Serenity/JS.
*
* :::tip Remember
* **Actors** have **abilities** that enable them to **perform interactions** and **answer questions**.
* :::
*
* From the technical perspective, an **ability** is an [adapter](https://en.wikipedia.org/wiki/Adapter_pattern)
* 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](https://serenity-js.org/api/core/interface/Initialisable/),
* the process of [freeing up any resources](https://serenity-js.org/api/core/interface/Discardable/) 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`](https://serenity-js.org/api/core/class/Actor#whoCan) method and give them an implementation of the ability to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb),
* specific to your web integration tool of choice.
*
* Note how [`BrowseTheWebWithPlaywright`](https://serenity-js.org/api/playwright/class/BrowseTheWebWithPlaywright/), [`BrowseTheWebWithWebdriverIO`](https://serenity-js.org/api/webdriverio/class/BrowseTheWebWithWebdriverIO/), and [`BrowseTheWebWithProtractor`](https://serenity-js.org/api/protractor/class/BrowseTheWebWithProtractor/)
* all **extend** the base ability to [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/).
*
* #### Playwright
*
* ```typescript
* 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
*
* await actorCalled('Trevor') // generic actor
* .whoCan(BrowseTheWebWithPlaywright.using(browser)) // tool-specific ability
* ```
*
* #### WebdriverIO
*
* ```typescript
* import { actorCalled } from '@serenity-js/core'
* import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio' // Serenity/JS integration module
*
* await actorCalled('Trevor') // generic actor
* .whoCan(BrowseTheWebWithWebdriverIO.using(browser)) // tool-specific ability
* ```
*
* #### Protractor
*
* ```typescript
* import { actorCalled } from '@serenity-js/core'
* import { BrowseTheWebWithProtractor } from '@serenity-js/protractor' // Serenity/JS integration module
* import { protractor } from 'protractor' // integration library
*
* await actorCalled('Trevor') // generic actor
* .whoCan(BrowseTheWebWithProtractor.using(protractor.browser)) // tool-specific ability
* ```
*
* ### Retrieving an ability
*
* Use [`PerformActivities`](https://serenity-js.org/api/core/class/PerformActivities/)} to retrieve an ability in a custom [`Interaction`](https://serenity-js.org/api/core/class/Interaction/) or [`Question`](https://serenity-js.org/api/core/class/Question/) implementation.
*
* Here, `Ability` can be the integration library-specific class, for example [`BrowseTheWebWithPlaywright`](https://serenity-js.org/api/playwright/class/BrowseTheWebWithPlaywright/),
* or its library-agnostic parent class, like [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/).
*
* To make your code portable across the various integration libraries, retrieve the ability
* using the library-agnostic parent class:
*
* ```typescript
* 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](https://serenity-js.org/handbook/web-testing/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](https://serenity-js.org/handbook/design/portable-test-code),
* 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`](https://serenity-js.org/api/playwright/class/BrowseTheWebWithPlaywright/)
* to trade portability for access to library-specific low-level methods:
*
* ```typescript
* 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
* ```
*
* :::warning 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`](https://serenity-js.org/api/core/class/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:
*
* ```typescript
* 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`](https://serenity-js.org/api/web/class/BrowseTheWeb), [`TakeNotes`](https://serenity-js.org/api/core/class/TakeNotes), and [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi):
*
* ```typescript
* 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`](https://serenity-js.org/api/core/class/Ability/), as well as [interactions](https://serenity-js.org/api/core/class/Interaction/)
* and [questions](https://serenity-js.org/api/core/class/Question/) to interact with it.
*
* The best way to start with that is for you to review the examples in the [Screenplay Pattern API docs](https://serenity-js.org/api/core/class/Ability/),
* as well as the [Serenity/JS code base on GitHub](https://github.com/serenity-js/serenity-js/tree/main/packages).
* Also note that all the [Serenity/JS modules](https://serenity-js.org/handbook/architecture/)
* 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](https://serenity-js.org/community/contributing/) to the main framework.
*
* [](https://matrix.to/#/#serenity-js:gitter.im)
*
* ### Defining a custom ability to `MakePhoneCalls`
*
* ```ts
* 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
*
* ```ts
* import { Answerable, Interaction, the } from '@serenity-js/core'
*
* // A custom interaction using the actor's ability:
* const Call = (phoneNumber: Answerable<string>) =>
* Interaction.where(the`#actor calls ${ phoneNumber }`, async actor => {
* await MakePhoneCalls.as(actor).dial(phoneNumber)
* })
* ```
*
* ### Using the custom ability and interaction in a test scenario
*
* ```ts
* 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`](https://serenity-js.org/api/core/interface/Initialisable/)
* or [`Discardable`](https://serenity-js.org/api/core/interface/Discardable/) interfaces, respectively.
*
* ### Defining a custom ability to `QueryPostgresDB`
*
* ```ts
* 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
*
* ```ts
* // 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
*
* ```ts
* // 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
* - [`AbilityType`](https://serenity-js.org/api/core/#AbilityType)
* - [`Initialisable`](https://serenity-js.org/api/core/interface/Initialisable/)
* - [`Discardable`](https://serenity-js.org/api/core/interface/Discardable/)
* - [`BrowseTheWeb`](https://serenity-js.org/api/web/class/BrowseTheWeb/)
* - [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/)
* - [`TakeNotes`](https://serenity-js.org/api/core/class/TakeNotes/)
*
* @group Screenplay Pattern
*/
export declare abstract class Ability {
/**
* Used to access an [actor's](https://serenity-js.org/api/core/class/Actor/) [ability](https://serenity-js.org/api/core/class/Ability/) of the given type
* from within the [`Interaction`](https://serenity-js.org/api/core/class/Interaction/) and [`Question`](https://serenity-js.org/api/core/class/Question/) classes.
*
* #### Retrieving an ability in an interaction definition
*
* ```ts
* 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
*
* ```ts
* 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)
* })
* }
* ```
*
* @param actor
*/
static as<A extends Ability>(this: AbilityType<A>, actor: UsesAbilities): A;
/**
* Returns a JSON representation of the ability and its current state, if available.
* The purpose of this method is to enable reporting the state of the ability in a human-readable format,
* rather than to serialise and deserialise the ability itself.
*/
toJSON(): SerialisedAbility;
/**
* Returns the most abstract type of this Ability class,
* specifically the first class in the inheritance hierarchy that directly extends the `Ability` class.
*
* ```ts
* import { Ability } from '@serenity-js/core';
*
* class MyAbility extends Ability {}
* class MySpecialisedAbility extends MyAbility {}
*
* MyAbility.abilityType(); // returns MyAbility
* MySpecialisedAbility.abilityType(); // returns MyAbility
* ```
*/
static abilityType(): AbilityType<Ability>;
/**
* Returns the most abstract type of this Ability instance,
* specifically the first class in the inheritance hierarchy that directly extends the `Ability` class.
*
* ```ts
* import { Ability } from '@serenity-js/core';
*
* class MyAbility extends Ability {}
* class MySpecialisedAbility extends MyAbility {}
*
* new MyAbility().abilityType(); // returns MyAbility
* new MySpecialisedAbility().abilityType(); // returns MyAbility
* ```
*/
abilityType(): AbilityType<Ability>;
private static abilityTypeOf;
private static ancestorTypes;
}
//# sourceMappingURL=Ability.d.ts.map