UNPKG

@serenity-js/core

Version:

The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure

196 lines 11 kB
import type { PerformsActivities } from './activities'; import { Activity } from './Activity'; import type { Answerable } from './Answerable'; /** * **Tasks** model **[sequences of activities](https://serenity-js.org/api/core/class/Activity/)** * and help you capture meaningful steps of an [actor](https://serenity-js.org/api/core/class/Actor/) workflow * in your domain. * * Typically, tasks correspond to higher-level, business domain-specific activities * like to `BookAPlaneTicket`, `PlaceATrade`, `TransferFunds`, and so on. * However, higher-level tasks can and should be composed of lower-level tasks. * For example, a task to `SignUp` could be composed of tasks to `ProvideUsername` and `ProvidePassword`. * * The lowest-level tasks in your abstraction hierarchy should be composed of [interactions](https://serenity-js.org/api/core/class/Interaction/). * For example, a low-level task to `ProvideUsername` could be composed of an interaction to [enter](https://serenity-js.org/api/web/class/Enter/) the value * into a form field and [press](https://serenity-js.org/api/web/class/Press/) the [`Key.Enter`](https://serenity-js.org/api/web/class/Key/#Enter). * * Tasks 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/), [abilities](https://serenity-js.org/api/core/class/Ability/), [interactions](https://serenity-js.org/api/core/class/Interaction/), and [questions](https://serenity-js.org/api/core/class/Question/). * * ![Screenplay Pattern](https://serenity-js.org/images/design/serenity-js-screenplay-pattern.png) * * Learn more about: * - [User-Centred Design: How a 50 year old technique became the key to scalable test automation](https://janmolak.com/user-centred-design-how-a-50-year-old-technique-became-the-key-to-scalable-test-automation-66a658a36555) * - [Actors](https://serenity-js.org/api/core/class/Actor/) * - [Activities](https://serenity-js.org/api/core/class/Activity/) * - [Interactions](https://serenity-js.org/api/core/class/Interaction/) * * ## Defining a task * * ```ts * import { Answerable, Task, the } from '@serenity-js/core' * import { By, Click, Enter, PageElement, Press, Key } from '@serenity-js/web' * * const SignIn = (username: Answerable<string>, password: Answerable<string>) => * Task.where(the`#actor signs is as ${ username }`, * Enter.theValue(username).into(PageElement.located(By.id('username'))), * Enter.theValue(password).into(PageElement.located(By.id('password'))), * Press.the(Key.Enter), * ); * ``` * * ## Defining a not implemented task * * Note that calling [`Task.where`](https://serenity-js.org/api/core/class/Task/#where) method without providing the sequence of [activities](https://serenity-js.org/api/core/class/Activity/) * produces a Task that's marked as "pending" in the test report. * * This feature is useful when you want to quickly write down a task that will be needed in the scenario, * but you're not yet sure what activities it will involve. * * ```ts * import { Task, the } from '@serenity-js/core' * * const SignUp = () => * Task.where(the`#actor signs up for a newsletter`) // no activities provided * // => task marked as pending * ``` * * ## Composing activities into tasks * * The purpose of **tasks** is to help you capture domain vocabulary by **associating domain meaning** with a **sequence of activities**. * From the implementation perspective, tasks help you give a **meaningful description** to such sequence * and provide a way to **easily reuse activities across your code base**. * * :::tip Remember * **Tasks** associate **domain meaning** with a sequence of **lower-level activities** and provide a mechanism for **code reuse**. * ::: * * For example, a task to _find a flight connection from London to New York_ could be modelled as a sequence of the following lower-level activities: * - specify origin city of "London" * - specify destination city of "New York" * * The easiest way to implement such task, and any custom Serenity/JS task for this matter, is to use the [`Task.where`](https://serenity-js.org/api/core/class/Task/#where) method to compose the lower-level activities: * * ```typescript * import { Task, the } from '@serenity-js/core' * * const findFlight = (originCity: string, destinationCity: string) => * Task.where(the`#actor finds a flight from ${ originCity } to ${ destinationCity }`, // task goal * specifyOriginCity(originCity), // activities * specifyDestinationCity(originCity), * ) * ``` * * Furthermore, if the actor was interacting with a web UI, a task to _specify origin city_ could again be composed of other activities: * - click on the `origin airport` widget * - enter city name of `London` * - pick the first suggested airport from the list * * Conversely, a task to _specify destination city_ could be composed of: * - click on the `destination airport` widget * - enter city name of `New York` * - pick the first suggested airport from the list * * Conveniently, [Serenity/JS modules](https://serenity-js.org/handbook/architecture/) provide low-level activities that * allow actors to interact with the various interfaces of the system under test. * For example, [Serenity/JS Web module](https://serenity-js.org/api/web) ships with activities such as [`Click`](https://serenity-js.org/api/web/class/Click/) or [`Enter`](https://serenity-js.org/api/web/class/Enter/), * which we can incorporate into our task definitions just like any other activities: * * ```typescript * import { Task, the } from '@serenity-js/core' * import { Click, Enter, Key, Press } from '@serenity-js/web' * * import { FlightFinder } from './ui/flight-finder' * * const specifyOriginCity = (originCity: string) => * Task.where(the`#actor specifies origin city of ${ originCity }`, * Click.on(FlightFinder.originAirport), * Enter.theValue(originCity).into(FlightFinder.originAirport), * Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.originAirport), * ) * * const specifyDestinationCity = (destinationCity: string) => * Task.where(the`#actor specifies destination city of ${ destinationCity }`, * Click.on(FlightFinder.destinationAirport), * Enter.theValue(destinationCity).into(FlightFinder.destinationAirport), * Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.destinationAirport), * ) * ``` * * As you can already see, tasks to _specify origin city_ and _specify destination city_ are almost identical, * save for the name of the widget and the text value the actor is supposed to enter. * Serenity/JS **task-based code reuse model** means that we can clean up such duplicated implementation * by **extracting a parameterised task**, in this case called `specifyCity`: * * ```typescript * import { Task, the } from '@serenity-js/core' * import { Click, Enter, Key, PageElement, Press } from '@serenity-js/web' * * import { FlightFinder } from './ui/flight-finder' * * const specifyOriginCity = (originCity: string) => * Task.where(the`#actor specifies origin city of ${ originCity }`, * specifyCity(originCity, FlightFinder.originAirport) * ) * * const specifyDestinationCity = (destinationCity: string) => * Task.where(the`#actor specifies destination city of ${ destinationCity }`, * specifyCity(destinationCity, FlightFinder.destinationAirport), * ) * * const specifyCity = (cityName: string, widget: PageElement) => * Task.where(the`#actor specifies city of ${ cityName } in ${ widget }`, * Click.on(widget), * Enter.theValue(cityName).into(widget), * Press.the(Key.ArrowDown, Key.Enter).into(widget), * ) * ``` * * As you work with Serenity/JS, you'll notice that the ideas of **functional decomposition**, so thinking of tasks as sequences of lower-level activities, * as well as **functional composition**, so implementing reusable activities and composing them into higher-level tasks, * are at the heart of the Screenplay Pattern. You'll also notice that the entire Serenity/JS framework does it best to help your team follow this approach. * * :::info The power of the Serenity/JS task-based code reuse model * What makes the Serenity/JS task-based code reuse model so **powerful at scale** is the observation that: * - for most software systems, a vast number of **diverse test scenarios** can be composed of a relatively **small number of high-level activities** * - all **high-level activities** can be composed of a relatively **small number of lower-level activities** * - in a reasonably consistently-designed software system, most lower-level activities tend to be similar and rather consistent across the various aspects of a given interface. * In particular, **there are only so many ways** one can interact with a UI button or send an HTTP request to a web service. * * What this means in practice is that by investing your time in properly designing **relatively few reusable tasks** * to test your system, you give your team a **significant productivity boost** and **leverage** when producing high-level test scenarios. * * On top of that, this design approach results not only in **simpler test scenarios** that reduce * the [cognitive load](https://en.wikipedia.org/wiki/Cognitive_load) on the reader as they require them to process * the scenario only one level of abstraction at the time. * It also allows for the test to **take shortcuts** in well-defined points of the workflow - use a REST API request to create * a test user account instead of going through the registration form. * ::: * * @group Screenplay Pattern */ export declare abstract class Task extends Activity { /** * A factory method that makes defining custom tasks more convenient. * * @param description * A description to be used when reporting this task * * @param activities * A sequence of lower-level activities that constitute this task */ static where(description: Answerable<string>, ...activities: Activity[]): Task; /** * Instructs the provided [`Actor`](https://serenity-js.org/api/core/class/Actor/) to perform this [`Task`](https://serenity-js.org/api/core/class/Task/). * * @param {PerformsActivities} actor * * #### Learn more * - [`Actor`](https://serenity-js.org/api/core/class/Actor/) * - [`PerformsActivities`](https://serenity-js.org/api/core/interface/PerformsActivities/) * - [`Activity`](https://serenity-js.org/api/core/class/Activity/) */ abstract performAs(actor: PerformsActivities): Promise<void>; } //# sourceMappingURL=Task.d.ts.map