@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
TypeScript
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/).
*
* 
*
* 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