UNPKG

@serenity-js/core

Version:

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

349 lines 17.4 kB
import * as util from 'util'; import type { FileSystemLocation } from '../io'; import type { UsesAbilities } from './abilities'; import type { Answerable } from './Answerable'; import { Interaction } from './Interaction'; import type { Optional } from './Optional'; import type { AnswersQuestions } from './questions/AnswersQuestions'; import { Describable } from './questions/Describable'; import type { DescriptionFormattingOptions } from './questions/DescriptionFormattingOptions'; import type { MetaQuestion } from './questions/MetaQuestion'; import type { RecursivelyAnswered } from './RecursivelyAnswered'; import type { WithAnswerableProperties } from './WithAnswerableProperties'; /** * **Questions** describe how [actors](https://serenity-js.org/api/core/class/Actor/) should query the system under test or the test environment to retrieve some information. * * Questions 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 [tasks](https://serenity-js.org/api/core/class/Task/). * * ![Screenplay Pattern](https://serenity-js.org/images/design/serenity-js-screenplay-pattern.png) * * Learn more about: * - [`Actor`](https://serenity-js.org/api/core/class/Actor/) * - [`Ability`](https://serenity-js.org/api/core/class/Ability/) * - [`Interaction`](https://serenity-js.org/api/core/class/Interaction/) * - [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) * * ## Implementing a basic custom Question * * ```ts * import { actorCalled, AnswersQuestions, UsesAbilities, Question } from '@serenity-js/core' * import { Ensure, equals } from '@serenity-js/assertions' * * const LastItemOf = <T>(list: T[]): Question<T> => * Question.about('last item from the list', (actor: AnswersQuestions & UsesAbilities) => { * return list[list.length - 1] * }); * * await actorCalled('Quentin').attemptsTo( * Ensure.that(LastItemFrom([1,2,3]), equals(3)), * ) * ``` * * ## Implementing a Question that uses an Ability * * Just like the [interactions](https://serenity-js.org/api/core/class/Interaction/), a [`Question`](https://serenity-js.org/api/core/class/Question/) * also can use [actor's](https://serenity-js.org/api/core/class/Actor/) [abilities](https://serenity-js.org/api/core/class/Ability/). * * Here, we use the ability to [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/) to retrieve a property of * an HTTP response. * * ```ts * import { AnswersQuestions, UsesAbilities, Question } from '@serenity-js/core' * import { CallAnApi } from '@serenity-js/rest' * * const TextOfLastResponseStatus = () => * Question.about<number>(`the text of the last response status`, actor => { * return CallAnApi.as(actor).mapLastResponse(response => response.statusText) * }) * ``` * * #### Learn more * - [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/) * - [`LastResponse`](https://serenity-js.org/api/rest/class/LastResponse/) * * ## Mapping answers to other questions * * Apart from retrieving information, [questions](https://serenity-js.org/api/core/class/Question/) can be used to transform information retrieved by other questions. * * Here, we use the factory method [`Question.about`](https://serenity-js.org/api/core/class/Question/#about) to produce a question that makes the received [actor](https://serenity-js.org/api/core/class/Actor/) * answer [`LastResponse.status`](https://serenity-js.org/api/rest/class/LastResponse/#status) and then compare it against some expected value. * * ```ts * import { actorCalled, AnswersQuestions, UsesAbilities, Question } from '@serenity-js/core' * import { CallAnApi, LastResponse } from '@serenity-js/rest' * import { Ensure, equals } from '@serenity-js/assertions' * * const RequestWasSuccessful = () => * Question.about<number>(`the text of the last response status`, async actor => { * const status = await actor.answer(LastResponse.status()); * * return status === 200; * }) * * await actorCalled('Quentin') * .whoCan(CallAnApi.at('https://api.example.org/')); * .attemptsTo( * Send.a(GetRequest.to('/books/0-688-00230-7')), * Ensure.that(RequestWasSuccessful(), isTrue()), * ) * ``` * * Note that the above example is for demonstration purposes only, Serenity/JS provides an easier way to * verify the response status of the [`LastResponse`](https://serenity-js.org/api/rest/class/LastResponse/): * * ```ts * import { actorCalled } from '@serenity-js/core' * import { CallAnApi, LastResponse } from '@serenity-js/rest' * import { Ensure, equals } from '@serenity-js/assertions' * * await actorCalled('Quentin') * .whoCan(CallAnApi.at('https://api.example.org/')); * .attemptsTo( * Send.a(GetRequest.to('/books/0-688-00230-7')), * Ensure.that(LastResponse.status(), equals(200)), * ) * ``` * * @group Screenplay Pattern */ export declare abstract class Question<T> extends Describable { /** * Factory method that simplifies the process of defining custom questions. * * #### Defining a custom question * * ```ts * import { Question } from '@serenity-js/core' * * const EnvVariable = (name: string) => * Question.about(`the ${ name } env variable`, actor => process.env[name]) * ``` * * @param description * @param body * @param [metaQuestionBody] */ static about<Answer_Type, Supported_Context_Type>(description: Answerable<string>, body: (actor: AnswersQuestions & UsesAbilities) => Promise<Answer_Type> | Answer_Type, metaQuestionBody: (answerable: Answerable<Supported_Context_Type>) => Question<Promise<Answer_Type>> | Question<Answer_Type>): MetaQuestionAdapter<Supported_Context_Type, Awaited<Answer_Type>>; static about<Answer_Type>(description: Answerable<string>, body: (actor: AnswersQuestions & UsesAbilities) => Promise<Answer_Type> | Answer_Type): QuestionAdapter<Awaited<Answer_Type>>; /** * Generates a [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) that recursively resolves * any [`Answerable`](https://serenity-js.org/api/core/#Answerable) fields of the provided object, * including [`Answerable`](https://serenity-js.org/api/core/#Answerable) fields * of [nested objects](https://serenity-js.org/api/core/#WithAnswerableProperties). * * Optionally, the method accepts `overrides` to be shallow-merged with the fields of the original `source`, * producing a new merged object. * * Overrides are applied from left to right, with subsequent objects overwriting property assignments of the previous ones. * * #### Resolving an object recursively using `Question.fromObject` * * ```ts * import { actorCalled, Question } from '@serenity-js/core' * import { Send, PostRequest } from '@serenity-js/rest' * import { By, Text, PageElement } from '@serenity-js/web' * * await actorCalled('Daisy') * .whoCan(CallAnApi.at('https://api.example.org')) * .attemptsTo( * Send.a( * PostRequest.to('/products/2') * .with( * Question.fromObject({ * name: Text.of(PageElement.located(By.css('.name'))), * }) * ) * ) * ); * ``` * * #### Merging objects using `Question.fromObject` * * ```ts * import { actorCalled, Question } from '@serenity-js/core' * import { Send, PostRequest } from '@serenity-js/rest' * import { By, Text, PageElement } from '@serenity-js/web' * * await actorCalled('Daisy') * .whoCan(CallAnApi.at('https://api.example.org')) * .attemptsTo( * Send.a( * PostRequest.to('/products/2') * .with( * Question.fromObject({ * name: Text.of(PageElement.located(By.css('.name'))), * quantity: undefined, * }, { * quantity: 2, * }) * ) * ) * ); * ``` * * #### Learn more * - [`WithAnswerableProperties`](https://serenity-js.org/api/core/#WithAnswerableProperties) * - [`RecursivelyAnswered`](https://serenity-js.org/api/core/#RecursivelyAnswered) * - [`Answerable`](https://serenity-js.org/api/core/#Answerable) * * @param source * @param overrides */ static fromObject<Source_Type extends object>(source: Answerable<WithAnswerableProperties<Source_Type>>, ...overrides: Array<Answerable<Partial<WithAnswerableProperties<Source_Type>>>>): QuestionAdapter<RecursivelyAnswered<Source_Type>>; /** * Generates a [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) that resolves * any [`Answerable`](https://serenity-js.org/api/core/#Answerable) elements of the provided array. */ static fromArray<Source_Type>(source: Array<Answerable<Source_Type>>, options?: DescriptionFormattingOptions): QuestionAdapter<Source_Type[]>; /** * Checks if the value is a [`Question`](https://serenity-js.org/api/core/class/Question/). * * @param maybeQuestion * The value to check */ static isAQuestion<T>(maybeQuestion: unknown): maybeQuestion is Question<T>; /** * Checks if the value is a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/). * * @param maybeMetaQuestion * The value to check */ static isAMetaQuestion<CT, RQT extends Question<unknown>>(maybeMetaQuestion: unknown): maybeMetaQuestion is MetaQuestion<CT, RQT>; /** * Creates a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that can be composed with any [`Answerable`](https://serenity-js.org/api/core/#Answerable) * to produce a single-line description of its value. * * ```ts * import { actorCalled, Question } from '@serenity-js/core' * import { Ensure, equals } from '@serenity-js/assertions' * * const accountDetails = () => * Question.about('account details', actor => ({ name: 'Alice', age: 28 })) * * await actorCalled('Alice').attemptsTo( * Ensure.that( * Question.formattedValue().of(accountDetails()), * equals('{ name: "Alice", age: 28 }'), * ), * ) * ``` * * @param options */ static formattedValue(options?: DescriptionFormattingOptions): MetaQuestion<any, Question<Promise<string>>>; /** * Creates a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that can be composed with any [`Answerable`](https://serenity-js.org/api/core/#Answerable) * to return its value when the answerable is a [`Question`](https://serenity-js.org/api/core/class/Question/), * or the answerable itself otherwise. * * The description of the resulting question is produced by calling [`Question.describedBy`](https://serenity-js.org/api/core/class/Question/#describedBy) on the * provided answerable. * * ```ts * import { actorCalled, Question } from '@serenity-js/core' * import { Ensure, equals } from '@serenity-js/assertions' * * const accountDetails = () => * Question.about('account details', actor => ({ name: 'Alice', age: 28 })) * * await actorCalled('Alice').attemptsTo( * Ensure.that( * Question.description().of(accountDetails()), * equals('account details'), * ), * Ensure.that( * Question.value().of(accountDetails()), * equals({ name: 'Alice', age: 28 }), * ), * ) * ``` */ static value<Answer_Type>(): MetaQuestion<Answer_Type, Question<Promise<Answer_Type>>>; protected static createAdapter<AT>(statement: Question<AT>): QuestionAdapter<Awaited<AT>>; private static staticFieldDescription; private static methodDescription; /** * Instructs the provided [`Actor`](https://serenity-js.org/api/core/class/Actor/) to use their [abilities](https://serenity-js.org/api/core/class/Ability/) * to answer this question. */ abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T; /** * Changes the description of this object, as returned by [`Describable.describedBy`](https://serenity-js.org/api/core/class/Describable/#describedBy) * and [`Describable.toString`](https://serenity-js.org/api/core/class/Describable/#toString). * * @param description * Replaces the current description according to the following rules: * - If `description` is an [`Answerable`](https://serenity-js.org/api/core/#Answerable), it replaces the current description * - If `description` is a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/), the current description is passed as `context` to `description.of(context)`, and the result replaces the current description */ describedAs(description: Answerable<string> | MetaQuestion<Awaited<T>, Question<Promise<string>>>): this; /** * Maps this question to one of a different type. * * ```ts * Question.about('number returned as string', actor => '42') // returns: QuestionAdapter<string> * .as(Number) // returns: QuestionAdapter<number> * ``` * * @param mapping */ as<O>(mapping: (answer: Awaited<T>) => Promise<O> | O): QuestionAdapter<O>; } declare global { interface ProxyConstructor { new <Source_Type extends object, Target_Type extends object>(target: Source_Type, handler: ProxyHandler<Source_Type>): Target_Type; } } /** * Describes an object recursively wrapped in [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) proxies, so that: * - both methods and fields of the wrapped object can be used as [questions](https://serenity-js.org/api/core/class/Question/) or [interactions](https://serenity-js.org/api/core/class/Interaction/) * - method parameters of the wrapped object will accept [`Answerable<T>`](https://serenity-js.org/api/core/#Answerable) * * @group Questions */ export type QuestionAdapterFieldDecorator<Original_Type> = { [Field in keyof Omit<Original_Type, keyof QuestionStatement<Original_Type>>]: Original_Type[Field] extends (...args: infer OriginalParameters) => infer OriginalMethodResult ? Field extends 'replace' | 'replaceAll' ? (searchValue: Answerable<string | RegExp>, replaceValue: Answerable<string>) => QuestionAdapter<string> : (...args: { [P in keyof OriginalParameters]: Answerable<Awaited<OriginalParameters[P]>>; }) => QuestionAdapter<Awaited<OriginalMethodResult>> : Original_Type[Field] extends number | bigint | boolean | string | symbol | object ? QuestionAdapter<Awaited<Original_Type[Field]>> : any; }; /** * A union type representing a proxy object returned by [`Question.about`](https://serenity-js.org/api/core/class/Question/#about). * * [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter) proxies the methods and fields of the wrapped object recursively, * allowing them to be used as either a [`Question`](https://serenity-js.org/api/core/class/Question/) or an [`Interaction`](https://serenity-js.org/api/core/class/Interaction/). * * @group Questions */ export type QuestionAdapter<Answer_Type> = Question<Promise<Answer_Type>> & Interaction & { isPresent(): Question<Promise<boolean>>; } & QuestionAdapterFieldDecorator<Answer_Type>; /** * An extension of [`QuestionAdapter`](https://serenity-js.org/api/core/#QuestionAdapter), that in addition to proxying methods and fields * of the wrapped object can also act as a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/). * * @group Questions */ export type MetaQuestionAdapter<Context_Type, Answer_Type> = QuestionAdapter<Answer_Type> & MetaQuestion<Context_Type, QuestionAdapter<Answer_Type>>; /** * @package */ declare class QuestionStatement<Answer_Type> extends Interaction implements Question<Promise<Answer_Type>>, Optional { private readonly body; private answer; constructor(subject: Answerable<string>, body: (actor: AnswersQuestions & UsesAbilities, ...Parameters: any[]) => Promise<Answer_Type> | Answer_Type, location?: FileSystemLocation); /** * Returns a Question that resolves to `true` if resolving the `QuestionStatement` * returns a value other than `null` or `undefined`, and doesn't throw errors. */ isPresent(): Question<Promise<boolean>>; answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Answer_Type>; performAs(actor: UsesAbilities & AnswersQuestions): Promise<void>; [util.inspect.custom](depth: number, options: util.InspectOptionsStylized, inspect: typeof util.inspect): string; describedAs(description: Answerable<string> | MetaQuestion<Answer_Type, Question<Promise<string>>>): this; as<O>(mapping: (answer: Awaited<Answer_Type>) => (Promise<O> | O)): QuestionAdapter<O>; } export {}; //# sourceMappingURL=Question.d.ts.map