@serenity-js/core 3.23.2 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/lib/errors/ErrorFactory.js +4 -4
- package/lib/errors/ErrorFactory.js.map +1 -1
- package/lib/errors/RaiseErrors.d.ts +2 -2
- package/lib/errors/RaiseErrors.js +2 -2
- package/lib/io/index.d.ts +0 -1
- package/lib/io/index.d.ts.map +1 -1
- package/lib/io/index.js +0 -1
- package/lib/io/index.js.map +1 -1
- package/lib/io/inspectedObject.js +1 -1
- package/lib/io/inspectedObject.js.map +1 -1
- package/lib/io/reflection/ValueInspector.d.ts +56 -0
- package/lib/io/reflection/ValueInspector.d.ts.map +1 -0
- package/lib/io/reflection/ValueInspector.js +149 -0
- package/lib/io/reflection/ValueInspector.js.map +1 -0
- package/lib/io/reflection/index.d.ts +1 -2
- package/lib/io/reflection/index.d.ts.map +1 -1
- package/lib/io/reflection/index.js +1 -2
- package/lib/io/reflection/index.js.map +1 -1
- package/lib/io/stringified.js +7 -90
- package/lib/io/stringified.js.map +1 -1
- package/lib/screenplay/Activity.d.ts +5 -12
- package/lib/screenplay/Activity.d.ts.map +1 -1
- package/lib/screenplay/Activity.js +3 -14
- package/lib/screenplay/Activity.js.map +1 -1
- package/lib/screenplay/Actor.js +1 -1
- package/lib/screenplay/Actor.js.map +1 -1
- package/lib/screenplay/Interaction.d.ts +4 -3
- package/lib/screenplay/Interaction.d.ts.map +1 -1
- package/lib/screenplay/Interaction.js +2 -2
- package/lib/screenplay/Interaction.js.map +1 -1
- package/lib/screenplay/Question.d.ts +73 -21
- package/lib/screenplay/Question.d.ts.map +1 -1
- package/lib/screenplay/Question.js +237 -30
- package/lib/screenplay/Question.js.map +1 -1
- package/lib/screenplay/Task.d.ts +16 -15
- package/lib/screenplay/Task.d.ts.map +1 -1
- package/lib/screenplay/Task.js +14 -14
- package/lib/screenplay/Task.js.map +1 -1
- package/lib/screenplay/abilities/Ability.d.ts +8 -6
- package/lib/screenplay/abilities/Ability.d.ts.map +1 -1
- package/lib/screenplay/abilities/Ability.js +8 -6
- package/lib/screenplay/abilities/Ability.js.map +1 -1
- package/lib/screenplay/abilities/AbilityType.d.ts +3 -3
- package/lib/screenplay/abilities/AnswerQuestions.d.ts +0 -1
- package/lib/screenplay/abilities/AnswerQuestions.d.ts.map +1 -1
- package/lib/screenplay/abilities/AnswerQuestions.js +2 -4
- package/lib/screenplay/abilities/AnswerQuestions.js.map +1 -1
- package/lib/screenplay/abilities/PerformActivities.d.ts +5 -3
- package/lib/screenplay/abilities/PerformActivities.d.ts.map +1 -1
- package/lib/screenplay/abilities/PerformActivities.js +12 -10
- package/lib/screenplay/abilities/PerformActivities.js.map +1 -1
- package/lib/screenplay/artifacts/CollectsArtifacts.d.ts +2 -2
- package/lib/screenplay/notes/NotepadAdapter.d.ts.map +1 -1
- package/lib/screenplay/notes/NotepadAdapter.js +44 -4
- package/lib/screenplay/notes/NotepadAdapter.js.map +1 -1
- package/lib/screenplay/questions/Describable.d.ts +27 -0
- package/lib/screenplay/questions/Describable.d.ts.map +1 -0
- package/lib/screenplay/questions/Describable.js +40 -0
- package/lib/screenplay/questions/Describable.js.map +1 -0
- package/lib/screenplay/questions/DescriptionFormattingOptions.d.ts +14 -0
- package/lib/screenplay/questions/DescriptionFormattingOptions.d.ts.map +1 -0
- package/lib/screenplay/questions/DescriptionFormattingOptions.js +3 -0
- package/lib/screenplay/questions/DescriptionFormattingOptions.js.map +1 -0
- package/lib/screenplay/questions/Expectation.d.ts +6 -10
- package/lib/screenplay/questions/Expectation.d.ts.map +1 -1
- package/lib/screenplay/questions/Expectation.js +12 -15
- package/lib/screenplay/questions/Expectation.js.map +1 -1
- package/lib/screenplay/questions/List.d.ts +1 -3
- package/lib/screenplay/questions/List.d.ts.map +1 -1
- package/lib/screenplay/questions/List.js +6 -31
- package/lib/screenplay/questions/List.js.map +1 -1
- package/lib/screenplay/questions/Unanswered.d.ts +1 -0
- package/lib/screenplay/questions/Unanswered.d.ts.map +1 -1
- package/lib/screenplay/questions/Unanswered.js +3 -0
- package/lib/screenplay/questions/Unanswered.js.map +1 -1
- package/lib/screenplay/questions/expectations/ExpectationDetails.js +1 -1
- package/lib/screenplay/questions/expectations/ExpectationDetails.js.map +1 -1
- package/lib/screenplay/questions/index.d.ts +3 -1
- package/lib/screenplay/questions/index.d.ts.map +1 -1
- package/lib/screenplay/questions/index.js +3 -1
- package/lib/screenplay/questions/index.js.map +1 -1
- package/lib/screenplay/questions/tag-functions.d.ts +228 -0
- package/lib/screenplay/questions/tag-functions.d.ts.map +1 -0
- package/lib/screenplay/questions/tag-functions.js +115 -0
- package/lib/screenplay/questions/tag-functions.js.map +1 -0
- package/lib/screenplay/time/activities/Wait.d.ts.map +1 -1
- package/lib/screenplay/time/activities/Wait.js +4 -3
- package/lib/screenplay/time/activities/Wait.js.map +1 -1
- package/package.json +4 -4
- package/src/errors/ErrorFactory.ts +5 -5
- package/src/errors/RaiseErrors.ts +2 -2
- package/src/io/index.ts +0 -1
- package/src/io/inspectedObject.ts +2 -2
- package/src/io/reflection/ValueInspector.ts +165 -0
- package/src/io/reflection/index.ts +1 -2
- package/src/io/stringified.ts +7 -103
- package/src/screenplay/Activity.ts +6 -17
- package/src/screenplay/Actor.ts +2 -2
- package/src/screenplay/Interaction.ts +5 -4
- package/src/screenplay/Question.ts +299 -49
- package/src/screenplay/Task.ts +18 -17
- package/src/screenplay/abilities/Ability.ts +8 -6
- package/src/screenplay/abilities/AbilityType.ts +3 -3
- package/src/screenplay/abilities/AnswerQuestions.ts +2 -5
- package/src/screenplay/abilities/PerformActivities.ts +35 -18
- package/src/screenplay/artifacts/CollectsArtifacts.ts +2 -2
- package/src/screenplay/notes/NotepadAdapter.ts +57 -6
- package/src/screenplay/questions/Describable.ts +48 -0
- package/src/screenplay/questions/DescriptionFormattingOptions.ts +13 -0
- package/src/screenplay/questions/Expectation.ts +19 -19
- package/src/screenplay/questions/List.ts +7 -41
- package/src/screenplay/questions/Unanswered.ts +4 -0
- package/src/screenplay/questions/expectations/ExpectationDetails.ts +2 -2
- package/src/screenplay/questions/index.ts +3 -1
- package/src/screenplay/questions/tag-functions.ts +313 -0
- package/src/screenplay/time/activities/Wait.ts +4 -3
- package/lib/io/isPlainObject.d.ts +0 -7
- package/lib/io/isPlainObject.d.ts.map +0 -1
- package/lib/io/isPlainObject.js +0 -25
- package/lib/io/isPlainObject.js.map +0 -1
- package/lib/io/reflection/isPrimitive.d.ts +0 -8
- package/lib/io/reflection/isPrimitive.d.ts.map +0 -1
- package/lib/io/reflection/isPrimitive.js +0 -24
- package/lib/io/reflection/isPrimitive.js.map +0 -1
- package/lib/io/reflection/typeOf.d.ts +0 -7
- package/lib/io/reflection/typeOf.d.ts.map +0 -1
- package/lib/io/reflection/typeOf.js +0 -35
- package/lib/io/reflection/typeOf.js.map +0 -1
- package/lib/screenplay/questions/q.d.ts +0 -66
- package/lib/screenplay/questions/q.d.ts.map +0 -1
- package/lib/screenplay/questions/q.js +0 -77
- package/lib/screenplay/questions/q.js.map +0 -1
- package/src/io/isPlainObject.ts +0 -24
- package/src/io/reflection/isPrimitive.ts +0 -20
- package/src/io/reflection/typeOf.ts +0 -31
- package/src/screenplay/questions/q.ts +0 -82
|
@@ -4,7 +4,9 @@ import { ErrorStackParser } from '../errors';
|
|
|
4
4
|
import { FileSystemLocation, Path } from '../io';
|
|
5
5
|
import type { UsesAbilities } from './abilities';
|
|
6
6
|
import type { PerformsActivities } from './activities';
|
|
7
|
-
import type {
|
|
7
|
+
import type { Answerable } from './Answerable';
|
|
8
|
+
import type { AnswersQuestions } from './questions/AnswersQuestions';
|
|
9
|
+
import { Describable } from './questions/Describable';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* **Activities** represents {@apilink Task|tasks} and {@apilink Interaction|interactions} to be performed by an {@apilink Actor|actor}.
|
|
@@ -17,17 +19,16 @@ import type { AnswersQuestions } from './questions';
|
|
|
17
19
|
*
|
|
18
20
|
* @group Screenplay Pattern
|
|
19
21
|
*/
|
|
20
|
-
export abstract class Activity {
|
|
22
|
+
export abstract class Activity extends Describable {
|
|
21
23
|
|
|
22
24
|
private static errorStackParser = new ErrorStackParser();
|
|
23
|
-
readonly #description: string;
|
|
24
25
|
readonly #location: FileSystemLocation;
|
|
25
26
|
|
|
26
27
|
constructor(
|
|
27
|
-
description: string
|
|
28
|
+
description: Answerable<string>,
|
|
28
29
|
location: FileSystemLocation = Activity.callerLocation(5)
|
|
29
30
|
) {
|
|
30
|
-
|
|
31
|
+
super(description);
|
|
31
32
|
this.#location = location;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -51,18 +52,6 @@ export abstract class Activity {
|
|
|
51
52
|
*/
|
|
52
53
|
abstract performAs(actor: PerformsActivities | UsesAbilities | AnswersQuestions): Promise<any>;
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
-
* Generates a human-friendly description to be used when reporting this Activity.
|
|
56
|
-
*
|
|
57
|
-
* **Note**: When this activity is reported, token `#actor` in the description
|
|
58
|
-
* will be replaced with the name of the actor performing this Activity.
|
|
59
|
-
*
|
|
60
|
-
* For example, `#actor clicks on a button` becomes `Wendy clicks on a button`.
|
|
61
|
-
*/
|
|
62
|
-
toString(): string {
|
|
63
|
-
return this.#description;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
55
|
protected static callerLocation(frameOffset: number): FileSystemLocation {
|
|
67
56
|
|
|
68
57
|
const originalStackTraceLimit = Error.stackTraceLimit;
|
package/src/screenplay/Actor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigurationError, TestCompromisedError } from '../errors';
|
|
2
2
|
import { ActivityRelatedArtifactGenerated } from '../events';
|
|
3
|
-
import {
|
|
3
|
+
import { ValueInspector } from '../io';
|
|
4
4
|
import type { Artifact} from '../model';
|
|
5
5
|
import { Name, } from '../model';
|
|
6
6
|
import type { Stage } from '../stage';
|
|
@@ -250,7 +250,7 @@ export class Actor implements PerformsActivities,
|
|
|
250
250
|
|
|
251
251
|
private acquireAbility(ability: Ability): void {
|
|
252
252
|
if (!(ability instanceof Ability)) {
|
|
253
|
-
throw new ConfigurationError(`Custom abilities must extend Ability from '@serenity-js/core'. Received ${ typeOf(ability) }`);
|
|
253
|
+
throw new ConfigurationError(`Custom abilities must extend Ability from '@serenity-js/core'. Received ${ ValueInspector.typeOf(ability) }`);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
const abilityType = this.mostGenericTypeOf(ability.constructor as AbilityType<Ability>);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UsesAbilities } from './abilities';
|
|
2
2
|
import { Activity } from './Activity';
|
|
3
|
+
import type { Answerable } from './Answerable';
|
|
3
4
|
import type { CollectsArtifacts } from './artifacts';
|
|
4
5
|
import type { AnswersQuestions } from './questions';
|
|
5
6
|
|
|
@@ -34,11 +35,11 @@ import type { AnswersQuestions } from './questions';
|
|
|
34
35
|
* you can easily create your own implementations using the {@apilink Interaction.where} factory method.
|
|
35
36
|
*
|
|
36
37
|
* ```ts
|
|
37
|
-
* import { Actor, Interaction } from '@serenity-js/core'
|
|
38
|
+
* import { Actor, Interaction, the } from '@serenity-js/core'
|
|
38
39
|
* import { BrowseTheWeb, Page } from '@serenity-js/web'
|
|
39
40
|
*
|
|
40
41
|
* export const ClearLocalStorage = () =>
|
|
41
|
-
* Interaction.where(`#actor clears local storage`, async (actor: Actor) => {
|
|
42
|
+
* Interaction.where(the`#actor clears local storage`, async (actor: Actor) => {
|
|
42
43
|
* // Interaction to ClearLocalStorage directly uses Actor's ability to BrowseTheWeb
|
|
43
44
|
* const page: Page = await BrowseTheWeb.as(actor).currentPage()
|
|
44
45
|
* await page.executeScript(() => window.localStorage.clear())
|
|
@@ -76,7 +77,7 @@ export abstract class Interaction extends Activity {
|
|
|
76
77
|
* @param interaction
|
|
77
78
|
*/
|
|
78
79
|
static where(
|
|
79
|
-
description: string
|
|
80
|
+
description: Answerable<string>,
|
|
80
81
|
interaction: (actor: UsesAbilities & AnswersQuestions & CollectsArtifacts) => Promise<void> | void,
|
|
81
82
|
): Interaction {
|
|
82
83
|
return new DynamicallyGeneratedInteraction(description, interaction);
|
|
@@ -101,7 +102,7 @@ export abstract class Interaction extends Activity {
|
|
|
101
102
|
*/
|
|
102
103
|
class DynamicallyGeneratedInteraction extends Interaction {
|
|
103
104
|
constructor(
|
|
104
|
-
description: string
|
|
105
|
+
description: Answerable<string>,
|
|
105
106
|
private readonly interaction: (actor: UsesAbilities & AnswersQuestions & CollectsArtifacts) => Promise<void> | void,
|
|
106
107
|
) {
|
|
107
108
|
super(description, Interaction.callerLocation(4));
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { isRecord } from 'tiny-types/lib/objects';
|
|
1
|
+
import { isRecord, significantFieldsOf } from 'tiny-types/lib/objects';
|
|
2
2
|
import * as util from 'util'; // eslint-disable-line unicorn/import-style
|
|
3
3
|
|
|
4
4
|
import { LogicError } from '../errors';
|
|
5
5
|
import type { FileSystemLocation } from '../io';
|
|
6
|
-
import { asyncMap,
|
|
6
|
+
import { asyncMap, f, inspectedObject, ValueInspector } from '../io';
|
|
7
7
|
import type { UsesAbilities } from './abilities';
|
|
8
8
|
import type { Answerable } from './Answerable';
|
|
9
9
|
import { Interaction } from './Interaction';
|
|
10
10
|
import type { Optional } from './Optional';
|
|
11
11
|
import type { AnswersQuestions } from './questions/AnswersQuestions';
|
|
12
|
+
import { Describable } from './questions/Describable';
|
|
13
|
+
import type { DescriptionFormattingOptions } from './questions/DescriptionFormattingOptions';
|
|
12
14
|
import type { MetaQuestion } from './questions/MetaQuestion';
|
|
15
|
+
import { the } from './questions/tag-functions';
|
|
13
16
|
import { Unanswered } from './questions/Unanswered';
|
|
14
17
|
import type { RecursivelyAnswered } from './RecursivelyAnswered';
|
|
15
18
|
import type { WithAnswerableProperties } from './WithAnswerableProperties';
|
|
@@ -79,16 +82,18 @@ import type { WithAnswerableProperties } from './WithAnswerableProperties';
|
|
|
79
82
|
* import { Ensure, equals } from '@serenity-js/assertions'
|
|
80
83
|
*
|
|
81
84
|
* const RequestWasSuccessful = () =>
|
|
82
|
-
* Question.about<number>(`the text of the last response status`, actor => {
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* Question.about<number>(`the text of the last response status`, async actor => {
|
|
86
|
+
* const status = await actor.answer(LastResponse.status());
|
|
87
|
+
*
|
|
88
|
+
* return status === 200;
|
|
89
|
+
* })
|
|
85
90
|
*
|
|
86
91
|
* await actorCalled('Quentin')
|
|
87
92
|
* .whoCan(CallAnApi.at('https://api.example.org/'));
|
|
88
93
|
* .attemptsTo(
|
|
89
94
|
* Send.a(GetRequest.to('/books/0-688-00230-7')),
|
|
90
95
|
* Ensure.that(RequestWasSuccessful(), isTrue()),
|
|
91
|
-
* )
|
|
96
|
+
* )
|
|
92
97
|
* ```
|
|
93
98
|
*
|
|
94
99
|
* Note that the above example is for demonstration purposes only, Serenity/JS provides an easier way to
|
|
@@ -104,12 +109,12 @@ import type { WithAnswerableProperties } from './WithAnswerableProperties';
|
|
|
104
109
|
* .attemptsTo(
|
|
105
110
|
* Send.a(GetRequest.to('/books/0-688-00230-7')),
|
|
106
111
|
* Ensure.that(LastResponse.status(), equals(200)),
|
|
107
|
-
* )
|
|
112
|
+
* )
|
|
108
113
|
* ```
|
|
109
114
|
*
|
|
110
115
|
* @group Screenplay Pattern
|
|
111
116
|
*/
|
|
112
|
-
export abstract class Question<T> {
|
|
117
|
+
export abstract class Question<T> extends Describable {
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
120
|
* Factory method that simplifies the process of defining custom questions.
|
|
@@ -128,18 +133,18 @@ export abstract class Question<T> {
|
|
|
128
133
|
* @param [metaQuestionBody]
|
|
129
134
|
*/
|
|
130
135
|
static about<Answer_Type, Supported_Context_Type>(
|
|
131
|
-
description: string
|
|
136
|
+
description: Answerable<string>,
|
|
132
137
|
body: (actor: AnswersQuestions & UsesAbilities) => Promise<Answer_Type> | Answer_Type,
|
|
133
138
|
metaQuestionBody: (answerable: Answerable<Supported_Context_Type>) => Question<Promise<Answer_Type>> | Question<Answer_Type>,
|
|
134
139
|
): MetaQuestionAdapter<Supported_Context_Type, Awaited<Answer_Type>>
|
|
135
140
|
|
|
136
141
|
static about<Answer_Type>(
|
|
137
|
-
description: string
|
|
142
|
+
description: Answerable<string>,
|
|
138
143
|
body: (actor: AnswersQuestions & UsesAbilities) => Promise<Answer_Type> | Answer_Type
|
|
139
144
|
): QuestionAdapter<Awaited<Answer_Type>>
|
|
140
145
|
|
|
141
146
|
static about<Answer_Type, Supported_Context_Type extends Answerable<any>>(
|
|
142
|
-
description: string
|
|
147
|
+
description: Answerable<string>,
|
|
143
148
|
body: (actor: AnswersQuestions & UsesAbilities) => Promise<Answer_Type> | Answer_Type,
|
|
144
149
|
metaQuestionBody?: (answerable: Supported_Context_Type) => QuestionAdapter<Answer_Type>,
|
|
145
150
|
): any
|
|
@@ -239,9 +244,23 @@ export abstract class Question<T> {
|
|
|
239
244
|
* Generates a {@apilink QuestionAdapter} that resolves
|
|
240
245
|
* any {@apilink Answerable} elements of the provided array.
|
|
241
246
|
*/
|
|
242
|
-
static fromArray<Source_Type>(source: Array<Answerable<Source_Type
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
static fromArray<Source_Type>(source: Array<Answerable<Source_Type>>, options?: DescriptionFormattingOptions): QuestionAdapter<Source_Type[]> {
|
|
248
|
+
const formatter = new ValueFormatter(ValueFormatter.defaultOptions);
|
|
249
|
+
|
|
250
|
+
const description = source.length === 0
|
|
251
|
+
? '[ ]'
|
|
252
|
+
: Question.about(formatter.format(source), async (actor: AnswersQuestions & UsesAbilities & { name: string }) => {
|
|
253
|
+
const descriptions = await asyncMap(source, item =>
|
|
254
|
+
item instanceof Describable
|
|
255
|
+
? item.describedBy(actor)
|
|
256
|
+
: Question.formattedValue(options).of(item).answeredBy(actor)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return `[ ${ descriptions.join(', ') } ]`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return Question.about<Source_Type[]>(description, async actor => {
|
|
263
|
+
return await asyncMap<Answerable<Source_Type>, Source_Type>(source, item => actor.answer(item));
|
|
245
264
|
});
|
|
246
265
|
}
|
|
247
266
|
|
|
@@ -268,6 +287,62 @@ export abstract class Question<T> {
|
|
|
268
287
|
&& maybeMetaQuestion['of'].length === 1; // arity of 1
|
|
269
288
|
}
|
|
270
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Creates a {@apilink MetaQuestion} that can be composed with any {@apilink Answerable}
|
|
292
|
+
* to produce a single-line description of its value.
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* import { actorCalled, Question } from '@serenity-js/core'
|
|
296
|
+
* import { Ensure, equals } from '@serenity-js/assertions'
|
|
297
|
+
*
|
|
298
|
+
* const accountDetails = () =>
|
|
299
|
+
* Question.about('account details', actor => ({ name: 'Alice', age: 28 }))
|
|
300
|
+
*
|
|
301
|
+
* await actorCalled('Alice').attemptsTo(
|
|
302
|
+
* Ensure.that(
|
|
303
|
+
* Question.formattedValue().of(accountDetails()),
|
|
304
|
+
* equals('{ name: "Alice", age: 28 }'),
|
|
305
|
+
* ),
|
|
306
|
+
* )
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* @param options
|
|
310
|
+
*/
|
|
311
|
+
static formattedValue(options?: DescriptionFormattingOptions): MetaQuestion<any, Question<Promise<string>>> {
|
|
312
|
+
return MetaQuestionAboutFormattedValue.using(options);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Creates a {@apilink MetaQuestion} that can be composed with any {@apilink Answerable}
|
|
317
|
+
* to return its value when the answerable is a {@apilink Question},
|
|
318
|
+
* or the answerable itself otherwise.
|
|
319
|
+
*
|
|
320
|
+
* The description of the resulting question is produced by calling {@apilink Question.description} on the
|
|
321
|
+
* provided answerable.
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* import { actorCalled, Question } from '@serenity-js/core'
|
|
325
|
+
* import { Ensure, equals } from '@serenity-js/assertions'
|
|
326
|
+
*
|
|
327
|
+
* const accountDetails = () =>
|
|
328
|
+
* Question.about('account details', actor => ({ name: 'Alice', age: 28 }))
|
|
329
|
+
*
|
|
330
|
+
* await actorCalled('Alice').attemptsTo(
|
|
331
|
+
* Ensure.that(
|
|
332
|
+
* Question.description().of(accountDetails()),
|
|
333
|
+
* equals('account details'),
|
|
334
|
+
* ),
|
|
335
|
+
* Ensure.that(
|
|
336
|
+
* Question.value().of(accountDetails()),
|
|
337
|
+
* equals({ name: 'Alice', age: 28 }),
|
|
338
|
+
* ),
|
|
339
|
+
* )
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
static value<Answer_Type>(): MetaQuestion<Answer_Type, Question<Promise<Answer_Type>>> {
|
|
343
|
+
return new MetaQuestionAboutValue<Answer_Type>();
|
|
344
|
+
}
|
|
345
|
+
|
|
271
346
|
protected static createAdapter<AT>(statement: Question<AT>): QuestionAdapter<Awaited<AT>> {
|
|
272
347
|
function getStatement() {
|
|
273
348
|
return statement;
|
|
@@ -321,7 +396,7 @@ export abstract class Question<T> {
|
|
|
321
396
|
return;
|
|
322
397
|
}
|
|
323
398
|
|
|
324
|
-
return Question.about(Question.
|
|
399
|
+
return Question.about(Question.staticFieldDescription(target, key), async (actor: AnswersQuestions & UsesAbilities) => {
|
|
325
400
|
const answer = await actor.answer(target as Answerable<AT>);
|
|
326
401
|
|
|
327
402
|
if (!isDefined(answer)) {
|
|
@@ -333,7 +408,7 @@ export abstract class Question<T> {
|
|
|
333
408
|
return typeof field === 'function'
|
|
334
409
|
? field.bind(answer)
|
|
335
410
|
: field;
|
|
336
|
-
});
|
|
411
|
+
}).describedAs(Question.formattedValue());
|
|
337
412
|
},
|
|
338
413
|
|
|
339
414
|
set(currentStatement: () => Question<AT>, key: string | symbol, value: any, receiver: any): boolean {
|
|
@@ -366,7 +441,7 @@ export abstract class Question<T> {
|
|
|
366
441
|
}) as any;
|
|
367
442
|
}
|
|
368
443
|
|
|
369
|
-
private static
|
|
444
|
+
private static staticFieldDescription<AT>(target: Question<AT>, key: string | symbol): string {
|
|
370
445
|
|
|
371
446
|
// "of" is characteristic of Serenity/JS MetaQuestion
|
|
372
447
|
if (key === 'of') {
|
|
@@ -399,22 +474,29 @@ export abstract class Question<T> {
|
|
|
399
474
|
}
|
|
400
475
|
|
|
401
476
|
/**
|
|
402
|
-
*
|
|
477
|
+
* Instructs the provided {@apilink Actor} to use their {@apilink Ability|abilities}
|
|
478
|
+
* to answer this question.
|
|
403
479
|
*/
|
|
404
|
-
abstract
|
|
480
|
+
abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T;
|
|
405
481
|
|
|
406
482
|
/**
|
|
407
|
-
* Changes the description of this
|
|
483
|
+
* Changes the description of this object, as returned by {@apilink Describable.describedBy}
|
|
484
|
+
* and {@apilink Describable.toString}.
|
|
408
485
|
*
|
|
409
|
-
* @param
|
|
486
|
+
* @param description
|
|
487
|
+
* Replaces the current description according to the following rules:
|
|
488
|
+
* - If `description` is an {@apilink Answerable}, it replaces the current description
|
|
489
|
+
* - If `description` is a {@apilink MetaQuestion}, the current description is passed as `context` to `description.of(context)`, and the result replaces the current description
|
|
410
490
|
*/
|
|
411
|
-
|
|
491
|
+
describedAs(description: Answerable<string> | MetaQuestion<Awaited<T>, Question<Promise<string>>>): this {
|
|
492
|
+
super.setDescription(
|
|
493
|
+
Question.isAMetaQuestion(description)
|
|
494
|
+
? description.of(this as Answerable<Awaited<T>>)
|
|
495
|
+
: description
|
|
496
|
+
);
|
|
412
497
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
* to answer this question.
|
|
416
|
-
*/
|
|
417
|
-
abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T;
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
418
500
|
|
|
419
501
|
/**
|
|
420
502
|
* Maps this question to one of a different type.
|
|
@@ -497,7 +579,7 @@ class QuestionStatement<Answer_Type> extends Interaction implements Question<Pro
|
|
|
497
579
|
private answer: Answer_Type | Unanswered = new Unanswered();
|
|
498
580
|
|
|
499
581
|
constructor(
|
|
500
|
-
|
|
582
|
+
subject: Answerable<string>,
|
|
501
583
|
private readonly body: (actor: AnswersQuestions & UsesAbilities, ...Parameters) => Promise<Answer_Type> | Answer_Type,
|
|
502
584
|
location: FileSystemLocation = QuestionStatement.callerLocation(4),
|
|
503
585
|
) {
|
|
@@ -525,13 +607,14 @@ class QuestionStatement<Answer_Type> extends Interaction implements Question<Pro
|
|
|
525
607
|
return inspectedObject(this.answer)(depth, options, inspect);
|
|
526
608
|
}
|
|
527
609
|
|
|
528
|
-
describedAs(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
610
|
+
describedAs(description: Answerable<string> | MetaQuestion<Answer_Type, Question<Promise<string>>>): this {
|
|
611
|
+
super.setDescription(
|
|
612
|
+
Question.isAMetaQuestion(description)
|
|
613
|
+
? description.of(this)
|
|
614
|
+
: description
|
|
615
|
+
);
|
|
532
616
|
|
|
533
|
-
|
|
534
|
-
return this.subject;
|
|
617
|
+
return this;
|
|
535
618
|
}
|
|
536
619
|
|
|
537
620
|
as<O>(mapping: (answer: Awaited<Answer_Type>) => (Promise<O> | O)): QuestionAdapter<O> {
|
|
@@ -555,7 +638,7 @@ class MetaQuestionStatement<Answer_Type, Supported_Context_Type extends Answerab
|
|
|
555
638
|
implements MetaQuestion<Supported_Context_Type, QuestionAdapter<Answer_Type>>
|
|
556
639
|
{
|
|
557
640
|
constructor(
|
|
558
|
-
subject: string
|
|
641
|
+
subject: Answerable<string>,
|
|
559
642
|
body: (actor: AnswersQuestions & UsesAbilities, ...Parameters) => Promise<Answer_Type> | Answer_Type,
|
|
560
643
|
private readonly metaQuestionBody: (answerable: Answerable<Supported_Context_Type>) => QuestionAdapter<Answer_Type>,
|
|
561
644
|
) {
|
|
@@ -564,7 +647,7 @@ class MetaQuestionStatement<Answer_Type, Supported_Context_Type extends Answerab
|
|
|
564
647
|
|
|
565
648
|
of(answerable: Answerable<Supported_Context_Type>): QuestionAdapter<Answer_Type> {
|
|
566
649
|
return Question.about(
|
|
567
|
-
this
|
|
650
|
+
the`${ this } of ${ answerable }`,
|
|
568
651
|
actor => actor.answer(this.metaQuestionBody(answerable))
|
|
569
652
|
);
|
|
570
653
|
}
|
|
@@ -575,11 +658,8 @@ class MetaQuestionStatement<Answer_Type, Supported_Context_Type extends Answerab
|
|
|
575
658
|
*/
|
|
576
659
|
class IsPresent<T> extends Question<Promise<boolean>> {
|
|
577
660
|
|
|
578
|
-
private subject: string;
|
|
579
|
-
|
|
580
661
|
constructor(private readonly question: Question<T>) {
|
|
581
|
-
super();
|
|
582
|
-
this.subject = f`${question}.isPresent()`;
|
|
662
|
+
super(f`${question}.isPresent()`);
|
|
583
663
|
}
|
|
584
664
|
|
|
585
665
|
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<boolean> {
|
|
@@ -604,15 +684,6 @@ class IsPresent<T> extends Question<Promise<boolean>> {
|
|
|
604
684
|
return typeof maybeOptional === 'object'
|
|
605
685
|
&& Reflect.has(maybeOptional, 'isPresent');
|
|
606
686
|
}
|
|
607
|
-
|
|
608
|
-
describedAs(subject: string): this {
|
|
609
|
-
this.subject = subject;
|
|
610
|
-
return this;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
toString(): string {
|
|
614
|
-
return this.subject;
|
|
615
|
-
}
|
|
616
687
|
}
|
|
617
688
|
|
|
618
689
|
/**
|
|
@@ -666,3 +737,182 @@ async function recursivelyAnswer<K extends number | string | symbol, V> (
|
|
|
666
737
|
|
|
667
738
|
return answer as Record<K, V>;
|
|
668
739
|
}
|
|
740
|
+
|
|
741
|
+
class MetaQuestionAboutValue<Answer_Type> implements MetaQuestion<Answer_Type, Question<Promise<Answer_Type>>> {
|
|
742
|
+
of(answerable: Answerable<Answer_Type>): Question<Promise<Answer_Type>> {
|
|
743
|
+
return new QuestionAboutValue<Answer_Type>(answerable);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
toString(): string {
|
|
747
|
+
return 'value';
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
class QuestionAboutValue<Answer_Type>
|
|
752
|
+
extends Question<Promise<Answer_Type>>
|
|
753
|
+
{
|
|
754
|
+
constructor(private readonly context: Answerable<Answer_Type>) {
|
|
755
|
+
super(QuestionAboutFormattedValue.of(context).toString());
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Answer_Type> {
|
|
759
|
+
return await actor.answer(this.context);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
class MetaQuestionAboutFormattedValue<Supported_Context_Type> implements MetaQuestion<Supported_Context_Type, Question<Promise<string>>> {
|
|
764
|
+
static using<SCT>(options?: DescriptionFormattingOptions): MetaQuestion<SCT, Question<Promise<string>>> {
|
|
765
|
+
return new MetaQuestionAboutFormattedValue(new ValueFormatter({
|
|
766
|
+
...ValueFormatter.defaultOptions,
|
|
767
|
+
...options,
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
constructor(private readonly formatter: ValueFormatter) {
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
of(context: Answerable<Supported_Context_Type>): Question<Promise<string>> & MetaQuestion<any, Question<Promise<string>>> {
|
|
775
|
+
return new QuestionAboutFormattedValue(
|
|
776
|
+
this.formatter,
|
|
777
|
+
context,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
toString(): string {
|
|
782
|
+
return 'formatted value';
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
class QuestionAboutFormattedValue<Supported_Context_Type>
|
|
787
|
+
extends Question<Promise<string>>
|
|
788
|
+
implements MetaQuestion<any, Question<Promise<string>>>
|
|
789
|
+
{
|
|
790
|
+
static of(context: Answerable<unknown>): Question<Promise<string>> & MetaQuestion<any, Question<Promise<string>>> {
|
|
791
|
+
return new QuestionAboutFormattedValue(new ValueFormatter(ValueFormatter.defaultOptions), context);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
constructor(
|
|
795
|
+
private readonly formatter: ValueFormatter,
|
|
796
|
+
private context?: Answerable<Supported_Context_Type>
|
|
797
|
+
) {
|
|
798
|
+
const description = context === undefined
|
|
799
|
+
? 'formatted value'
|
|
800
|
+
: formatter.format(context);
|
|
801
|
+
|
|
802
|
+
super(description);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
806
|
+
const answer = await actor.answer(this.context);
|
|
807
|
+
|
|
808
|
+
return this.formatter.format(answer);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
override async describedBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
812
|
+
const unanswered = ! this.context
|
|
813
|
+
|| ! this.context['answer']
|
|
814
|
+
|| Unanswered.isUnanswered((this.context as any).answer);
|
|
815
|
+
|
|
816
|
+
const answer = unanswered
|
|
817
|
+
? await actor.answer(this.context)
|
|
818
|
+
: (this.context as any).answer;
|
|
819
|
+
|
|
820
|
+
return this.formatter.format(answer);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
of(context: Answerable<unknown>): Question<Promise<string>> & MetaQuestion<any, Question<Promise<string>>> {
|
|
824
|
+
return new QuestionAboutFormattedValue(
|
|
825
|
+
this.formatter,
|
|
826
|
+
Question.isAMetaQuestion(this.context)
|
|
827
|
+
? this.context.of(context)
|
|
828
|
+
: context,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
class ValueFormatter {
|
|
834
|
+
public static readonly defaultOptions = { maxLength: Number.POSITIVE_INFINITY };
|
|
835
|
+
|
|
836
|
+
constructor(private readonly options: DescriptionFormattingOptions) {
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
format(value: unknown): string {
|
|
840
|
+
if (value === null) {
|
|
841
|
+
return 'null';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (value === undefined) {
|
|
845
|
+
return 'undefined';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (typeof value === 'string') {
|
|
849
|
+
return `"${ this.trim(value) }"`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (typeof value === 'symbol') {
|
|
853
|
+
return `Symbol(${ this.trim(value.description) })`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (typeof value === 'bigint') {
|
|
857
|
+
return `${ this.trim(value.toString()) }`;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (ValueInspector.isPromise(value)) {
|
|
861
|
+
return 'Promise';
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (Array.isArray(value)) {
|
|
865
|
+
return value.length === 0
|
|
866
|
+
? '[ ]'
|
|
867
|
+
: `[ ${ this.trim(value.map(item => this.format(item)).join(', ')) } ]`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (value instanceof Map) {
|
|
871
|
+
return `Map(${ this.format(Object.fromEntries(value.entries())) })`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (value instanceof Set) {
|
|
875
|
+
return `Set(${ this.format(Array.from(value.values())) })`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (ValueInspector.isDate(value)) {
|
|
879
|
+
return `Date(${ value.toISOString() })`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (value instanceof RegExp) {
|
|
883
|
+
return `${ value }`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (ValueInspector.hasItsOwnToString(value)) {
|
|
887
|
+
return `${ this.trim(value.toString()) }`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (ValueInspector.isPlainObject(value)) {
|
|
891
|
+
const stringifiedEntries = Object
|
|
892
|
+
.entries(value)
|
|
893
|
+
.reduce((acc, [ key, value ]) => acc.concat(`${ key }: ${ this.format(value) }`), [])
|
|
894
|
+
.join(', ');
|
|
895
|
+
|
|
896
|
+
return `{ ${ this.trim(stringifiedEntries) } }`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (typeof value === 'object') {
|
|
900
|
+
const entries = significantFieldsOf(value)
|
|
901
|
+
.map(field => [ field, (value as any)[field] ]);
|
|
902
|
+
return `${ value.constructor.name }(${ this.format(Object.fromEntries(entries)) })`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return String(value);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private trim(value: string): string {
|
|
909
|
+
const ellipsis = '...';
|
|
910
|
+
const oneLiner = value.replaceAll(/\n+/g, ' ');
|
|
911
|
+
|
|
912
|
+
const maxLength = Math.max(ellipsis.length + 1, this.options.maxLength);
|
|
913
|
+
|
|
914
|
+
return oneLiner.length > maxLength
|
|
915
|
+
? `${ oneLiner.slice(0, Math.max(0, maxLength) - ellipsis.length) }${ ellipsis }`
|
|
916
|
+
: oneLiner;
|
|
917
|
+
}
|
|
918
|
+
}
|