@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
package/src/screenplay/Task.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ImplementationPendingError } from '../errors';
|
|
2
2
|
import type { PerformsActivities } from './activities';
|
|
3
3
|
import { Activity } from './Activity';
|
|
4
|
+
import type { Answerable } from './Answerable';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* **Tasks** model **{@apilink Activity|sequences of activities}**
|
|
@@ -30,11 +31,11 @@ import { Activity } from './Activity';
|
|
|
30
31
|
* ## Defining a task
|
|
31
32
|
*
|
|
32
33
|
* ```ts
|
|
33
|
-
* import { Answerable, Task,
|
|
34
|
+
* import { Answerable, Task, the } from '@serenity-js/core'
|
|
34
35
|
* import { By, Click, Enter, PageElement, Press, Key } from '@serenity-js/web'
|
|
35
36
|
*
|
|
36
37
|
* const SignIn = (username: Answerable<string>, password: Answerable<string>) =>
|
|
37
|
-
* Task.where(
|
|
38
|
+
* Task.where(the`#actor signs is as ${ username }`,
|
|
38
39
|
* Enter.theValue(username).into(PageElement.located(By.id('username'))),
|
|
39
40
|
* Enter.theValue(password).into(PageElement.located(By.id('password'))),
|
|
40
41
|
* Press.the(Key.Enter),
|
|
@@ -50,11 +51,11 @@ import { Activity } from './Activity';
|
|
|
50
51
|
* but you're not yet sure what activities it will involve.
|
|
51
52
|
*
|
|
52
53
|
* ```ts
|
|
53
|
-
* import { Task } from '@serenity-js/core'
|
|
54
|
+
* import { Task, the } from '@serenity-js/core'
|
|
54
55
|
*
|
|
55
56
|
* const SignUp = () =>
|
|
56
|
-
* Task.where(`#actor signs up for a newsletter`) // no activities provided
|
|
57
|
-
*
|
|
57
|
+
* Task.where(the`#actor signs up for a newsletter`) // no activities provided
|
|
58
|
+
* // => task marked as pending
|
|
58
59
|
* ```
|
|
59
60
|
*
|
|
60
61
|
* ## Composing activities into tasks
|
|
@@ -74,10 +75,10 @@ import { Activity } from './Activity';
|
|
|
74
75
|
* The easiest way to implement such task, and any custom Serenity/JS task for this matter, is to use the {@apilink Task.where} method to compose the lower-level activities:
|
|
75
76
|
*
|
|
76
77
|
* ```typescript
|
|
77
|
-
* import { Task } from '@serenity-js/core'
|
|
78
|
+
* import { Task, the } from '@serenity-js/core'
|
|
78
79
|
*
|
|
79
80
|
* const findFlight = (originCity: string, destinationCity: string) =>
|
|
80
|
-
* Task.where(`#
|
|
81
|
+
* Task.where(the`#actor finds a flight from ${ originCity } to ${ destinationCity }`, // task goal
|
|
81
82
|
* specifyOriginCity(originCity), // activities
|
|
82
83
|
* specifyDestinationCity(originCity),
|
|
83
84
|
* )
|
|
@@ -99,20 +100,20 @@ import { Activity } from './Activity';
|
|
|
99
100
|
* which we can incorporate into our task definitions just like any other activities:
|
|
100
101
|
*
|
|
101
102
|
* ```typescript
|
|
102
|
-
* import { Task } from '@serenity-js/core'
|
|
103
|
+
* import { Task, the } from '@serenity-js/core'
|
|
103
104
|
* import { Click, Enter, Key, Press } from '@serenity-js/web'
|
|
104
105
|
*
|
|
105
106
|
* import { FlightFinder } from './ui/flight-finder'
|
|
106
107
|
*
|
|
107
108
|
* const specifyOriginCity = (originCity: string) =>
|
|
108
|
-
* Task.where(`#actor specifies origin city of ${ originCity }`,
|
|
109
|
+
* Task.where(the`#actor specifies origin city of ${ originCity }`,
|
|
109
110
|
* Click.on(FlightFinder.originAirport),
|
|
110
111
|
* Enter.theValue(originCity).into(FlightFinder.originAirport),
|
|
111
112
|
* Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.originAirport),
|
|
112
113
|
* )
|
|
113
114
|
*
|
|
114
115
|
* const specifyDestinationCity = (destinationCity: string) =>
|
|
115
|
-
* Task.where(`#actor specifies destination city of ${ destinationCity }`,
|
|
116
|
+
* Task.where(the`#actor specifies destination city of ${ destinationCity }`,
|
|
116
117
|
* Click.on(FlightFinder.destinationAirport),
|
|
117
118
|
* Enter.theValue(destinationCity).into(FlightFinder.destinationAirport),
|
|
118
119
|
* Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.destinationAirport),
|
|
@@ -125,23 +126,23 @@ import { Activity } from './Activity';
|
|
|
125
126
|
* by **extracting a parameterised task**, in this case called `specifyCity`:
|
|
126
127
|
*
|
|
127
128
|
* ```typescript
|
|
128
|
-
* import { Task } from '@serenity-js/core'
|
|
129
|
+
* import { Task, the } from '@serenity-js/core'
|
|
129
130
|
* import { Click, Enter, Key, PageElement, Press } from '@serenity-js/web'
|
|
130
131
|
*
|
|
131
132
|
* import { FlightFinder } from './ui/flight-finder'
|
|
132
133
|
*
|
|
133
134
|
* const specifyOriginCity = (originCity: string) =>
|
|
134
|
-
* Task.where(`#actor specifies origin city of ${ originCity }`,
|
|
135
|
+
* Task.where(the`#actor specifies origin city of ${ originCity }`,
|
|
135
136
|
* specifyCity(originCity, FlightFinder.originAirport)
|
|
136
137
|
* )
|
|
137
138
|
*
|
|
138
139
|
* const specifyDestinationCity = (destinationCity: string) =>
|
|
139
|
-
* Task.where(`#actor specifies destination city of ${ destinationCity }`,
|
|
140
|
+
* Task.where(the`#actor specifies destination city of ${ destinationCity }`,
|
|
140
141
|
* specifyCity(destinationCity, FlightFinder.destinationAirport),
|
|
141
142
|
* )
|
|
142
143
|
*
|
|
143
144
|
* const specifyCity = (cityName: string, widget: PageElement) =>
|
|
144
|
-
* Task.where(`#actor specifies city of ${ cityName } in ${ widget }`,
|
|
145
|
+
* Task.where(the`#actor specifies city of ${ cityName } in ${ widget }`,
|
|
145
146
|
* Click.on(widget),
|
|
146
147
|
* Enter.theValue(cityName).into(widget),
|
|
147
148
|
* Press.the(Key.ArrowDown, Key.Enter).into(widget),
|
|
@@ -182,7 +183,7 @@ export abstract class Task extends Activity {
|
|
|
182
183
|
* @param activities
|
|
183
184
|
* A sequence of lower-level activities that constitute this task
|
|
184
185
|
*/
|
|
185
|
-
static where(description: string
|
|
186
|
+
static where(description: Answerable<string>, ...activities: Activity[]): Task {
|
|
186
187
|
return activities.length > 0
|
|
187
188
|
? new DynamicallyGeneratedTask(description, activities)
|
|
188
189
|
: new NotImplementedTask(description);
|
|
@@ -205,7 +206,7 @@ export abstract class Task extends Activity {
|
|
|
205
206
|
* @package
|
|
206
207
|
*/
|
|
207
208
|
class DynamicallyGeneratedTask extends Task {
|
|
208
|
-
constructor(description: string
|
|
209
|
+
constructor(description: Answerable<string>, private activities: Activity[]) {
|
|
209
210
|
super(description, Task.callerLocation(4));
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -218,7 +219,7 @@ class DynamicallyGeneratedTask extends Task {
|
|
|
218
219
|
* @package
|
|
219
220
|
*/
|
|
220
221
|
class NotImplementedTask extends Task {
|
|
221
|
-
constructor(description: string) {
|
|
222
|
+
constructor(description: Answerable<string>) {
|
|
222
223
|
super(description, Task.callerLocation(4));
|
|
223
224
|
}
|
|
224
225
|
|
|
@@ -54,7 +54,7 @@ import type { UsesAbilities } from './UsesAbilities';
|
|
|
54
54
|
* Note how {@apilink BrowseTheWebWithPlaywright}, {@apilink BrowseTheWebWithWebdriverIO}, and {@apilink BrowseTheWebWithProtractor}
|
|
55
55
|
* all **extend** the base ability to {@apilink BrowseTheWeb}.
|
|
56
56
|
*
|
|
57
|
-
* #### Playwright
|
|
57
|
+
* #### Playwright
|
|
58
58
|
*
|
|
59
59
|
* ```typescript
|
|
60
60
|
* import { actorCalled } from '@serenity-js/core'
|
|
@@ -63,7 +63,7 @@ import type { UsesAbilities } from './UsesAbilities';
|
|
|
63
63
|
*
|
|
64
64
|
* const browser = await chromium.launch({ headless: true }) // integration library
|
|
65
65
|
*
|
|
66
|
-
* actorCalled('Trevor')
|
|
66
|
+
* await actorCalled('Trevor') // generic actor
|
|
67
67
|
* .whoCan(BrowseTheWebWithPlaywright.using(browser)) // tool-specific ability
|
|
68
68
|
* ```
|
|
69
69
|
*
|
|
@@ -73,7 +73,7 @@ import type { UsesAbilities } from './UsesAbilities';
|
|
|
73
73
|
* import { actorCalled } from '@serenity-js/core'
|
|
74
74
|
* import { BrowseTheWebWithWebdriverIO } from '@serenity-js/webdriverio' // Serenity/JS integration module
|
|
75
75
|
*
|
|
76
|
-
* actorCalled('Trevor')
|
|
76
|
+
* await actorCalled('Trevor') // generic actor
|
|
77
77
|
* .whoCan(BrowseTheWebWithWebdriverIO.using(browser)) // tool-specific ability
|
|
78
78
|
* ```
|
|
79
79
|
*
|
|
@@ -84,7 +84,7 @@ import type { UsesAbilities } from './UsesAbilities';
|
|
|
84
84
|
* import { BrowseTheWebWithProtractor } from '@serenity-js/protractor' // Serenity/JS integration module
|
|
85
85
|
* import { protractor } from 'protractor' // integration library
|
|
86
86
|
*
|
|
87
|
-
* actorCalled('Trevor')
|
|
87
|
+
* await actorCalled('Trevor') // generic actor
|
|
88
88
|
* .whoCan(BrowseTheWebWithProtractor.using(protractor.browser)) // tool-specific ability
|
|
89
89
|
* ```
|
|
90
90
|
*
|
|
@@ -242,9 +242,11 @@ import type { UsesAbilities } from './UsesAbilities';
|
|
|
242
242
|
* ### Defining a custom interaction using the custom ability
|
|
243
243
|
*
|
|
244
244
|
* ```ts
|
|
245
|
+
* import { Answerable, Interaction, the } from '@serenity-js/core'
|
|
246
|
+
*
|
|
245
247
|
* // A custom interaction using the actor's ability:
|
|
246
|
-
* const Call = (phoneNumber: string) =>
|
|
247
|
-
* Interaction.where(`#actor calls ${ phoneNumber }`, async actor => {
|
|
248
|
+
* const Call = (phoneNumber: Answerable<string>) =>
|
|
249
|
+
* Interaction.where(the`#actor calls ${ phoneNumber }`, async actor => {
|
|
248
250
|
* await MakePhoneCalls.as(actor).dial(phoneNumber)
|
|
249
251
|
* })
|
|
250
252
|
* ```
|
|
@@ -7,7 +7,7 @@ import type { Ability } from './Ability';
|
|
|
7
7
|
* #### Retrieving an ability from an interaction
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* import { Ability, actorCalled, Interaction } from '@serenity-js/core';
|
|
10
|
+
* import { Ability, Answerable, actorCalled, Interaction, the } from '@serenity-js/core';
|
|
11
11
|
*
|
|
12
12
|
* class MakePhoneCalls extends Ability {
|
|
13
13
|
* static using(phone: Phone) {
|
|
@@ -23,8 +23,8 @@ import type { Ability } from './Ability';
|
|
|
23
23
|
* }
|
|
24
24
|
* }
|
|
25
25
|
*
|
|
26
|
-
* const Call = (phoneNumber: string) =>
|
|
27
|
-
* Interaction.where(`#actor calls ${ phoneNumber }`, async actor => {
|
|
26
|
+
* const Call = (phoneNumber: Answerable<string>) =>
|
|
27
|
+
* Interaction.where(the`#actor calls ${ phoneNumber }`, async actor => {
|
|
28
28
|
* await MakePhoneCalls.as(actor).dial(phoneNumber)
|
|
29
29
|
* });
|
|
30
30
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValueInspector } from '../../io';
|
|
1
2
|
import type { Answerable } from '../Answerable';
|
|
2
3
|
import { Question } from '../Question';
|
|
3
4
|
import type { AnswersQuestions } from '../questions';
|
|
@@ -20,7 +21,7 @@ export class AnswerQuestions extends Ability {
|
|
|
20
21
|
|
|
21
22
|
answer<T>(answerable: Answerable<T>): Promise<T> {
|
|
22
23
|
|
|
23
|
-
if (AnswerQuestions.isDefined(answerable) &&
|
|
24
|
+
if (AnswerQuestions.isDefined(answerable) && ValueInspector.isPromise(answerable)) {
|
|
24
25
|
return answerable;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -31,10 +32,6 @@ export class AnswerQuestions extends Ability {
|
|
|
31
32
|
return Promise.resolve(answerable as T);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
private static isAPromise<V>(v: Answerable<V>): v is Promise<V> {
|
|
35
|
-
return Object.prototype.hasOwnProperty.call(v, 'then');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
35
|
private static isDefined<V>(v: Answerable<V>) {
|
|
39
36
|
return !(v === undefined || v === null);
|
|
40
37
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { match } from 'tiny-types';
|
|
2
2
|
|
|
3
3
|
import { AssertionError, ImplementationPendingError, TestCompromisedError } from '../../errors';
|
|
4
|
-
import type { EmitsDomainEvents} from '../../events';
|
|
4
|
+
import type { EmitsDomainEvents } from '../../events';
|
|
5
5
|
import { InteractionFinished, InteractionStarts, TaskFinished, TaskStarts } from '../../events';
|
|
6
|
-
import type
|
|
7
|
-
|
|
8
|
-
ProblemIndication
|
|
9
|
-
} from '../../model';
|
|
6
|
+
import { type FileSystemLocation, ValueInspector } from '../../io';
|
|
7
|
+
import type { Outcome, ProblemIndication } from '../../model';
|
|
10
8
|
import {
|
|
11
9
|
ActivityDetails,
|
|
12
10
|
ExecutionCompromised,
|
|
@@ -19,7 +17,9 @@ import {
|
|
|
19
17
|
import type { PerformsActivities } from '../activities/PerformsActivities';
|
|
20
18
|
import type { Activity } from '../Activity';
|
|
21
19
|
import { Interaction } from '../Interaction';
|
|
22
|
-
import {
|
|
20
|
+
import type { AnswersQuestions } from '../questions';
|
|
21
|
+
import { Ability } from './Ability';
|
|
22
|
+
import type { UsesAbilities } from './UsesAbilities';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* An {@apilink Ability} that enables an {@apilink Actor} to perform a given {@apilink Activity}.
|
|
@@ -32,15 +32,15 @@ import { Ability } from './index';
|
|
|
32
32
|
*/
|
|
33
33
|
export class PerformActivities extends Ability {
|
|
34
34
|
constructor(
|
|
35
|
-
protected readonly actor: PerformsActivities & { name: string },
|
|
35
|
+
protected readonly actor: AnswersQuestions & UsesAbilities & PerformsActivities & { name: string },
|
|
36
36
|
protected readonly stage: EmitsDomainEvents,
|
|
37
37
|
) {
|
|
38
38
|
super();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async perform(activity: Activity): Promise<void> {
|
|
42
|
-
const sceneId
|
|
43
|
-
const details
|
|
42
|
+
const sceneId = this.stage.currentSceneId();
|
|
43
|
+
const details = this.detailsOf(this.nameOf(activity), activity.instantiationLocation());
|
|
44
44
|
const activityId = this.stage.assignNewActivityId(details);
|
|
45
45
|
|
|
46
46
|
const [ activityStarts, activityFinished ] = activity instanceof Interaction
|
|
@@ -48,11 +48,28 @@ export class PerformActivities extends Ability {
|
|
|
48
48
|
: [ TaskStarts, TaskFinished ];
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
-
this.stage.announce(
|
|
51
|
+
this.stage.announce(
|
|
52
|
+
new activityStarts(
|
|
53
|
+
sceneId,
|
|
54
|
+
activityId,
|
|
55
|
+
details,
|
|
56
|
+
this.stage.currentTime()
|
|
57
|
+
)
|
|
58
|
+
);
|
|
52
59
|
|
|
53
60
|
await activity.performAs(this.actor);
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
const name = await activity.describedBy(this.actor);
|
|
63
|
+
|
|
64
|
+
this.stage.announce(
|
|
65
|
+
new activityFinished(
|
|
66
|
+
sceneId,
|
|
67
|
+
activityId,
|
|
68
|
+
this.detailsOf(name, activity.instantiationLocation()),
|
|
69
|
+
new ExecutionSuccessful(),
|
|
70
|
+
this.stage.currentTime()
|
|
71
|
+
)
|
|
72
|
+
);
|
|
56
73
|
}
|
|
57
74
|
catch (error) {
|
|
58
75
|
this.stage.announce(new activityFinished(sceneId, activityId, details, this.outcomeFor(error), this.stage.currentTime()));
|
|
@@ -74,18 +91,18 @@ export class PerformActivities extends Ability {
|
|
|
74
91
|
.else(_ => new ExecutionFailedWithError(error));
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
private detailsOf(
|
|
94
|
+
private detailsOf(name: string, instantiationLocation: FileSystemLocation): ActivityDetails {
|
|
78
95
|
return new ActivityDetails(
|
|
79
|
-
new Name(
|
|
80
|
-
|
|
96
|
+
new Name(name),
|
|
97
|
+
instantiationLocation,
|
|
81
98
|
)
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
protected nameOf(activity: Activity): string {
|
|
85
|
-
const template =
|
|
86
|
-
?
|
|
87
|
-
: activity.
|
|
102
|
+
const template = ValueInspector.hasItsOwnToString(activity)
|
|
103
|
+
? activity.toString()
|
|
104
|
+
: `#actor performs ${ activity.constructor.name }`;
|
|
88
105
|
|
|
89
|
-
return template.
|
|
106
|
+
return template.replaceAll('#actor', this.actor.name);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
@@ -19,7 +19,7 @@ export interface CollectsArtifacts {
|
|
|
19
19
|
*
|
|
20
20
|
* ```ts
|
|
21
21
|
* import * as fs from 'node:fs'
|
|
22
|
-
* import { Answerable, Interaction } from '@serenity-js/core'
|
|
22
|
+
* import { Answerable, Interaction, the } from '@serenity-js/core'
|
|
23
23
|
* import { Path } from '@serenity-js/core/lib/io'
|
|
24
24
|
* import { Name, TextData } from '@serenity-js/core/lib/model'
|
|
25
25
|
*
|
|
@@ -36,7 +36,7 @@ export interface CollectsArtifacts {
|
|
|
36
36
|
* })
|
|
37
37
|
*
|
|
38
38
|
* static textData = (contents: Answerable<string>, name?: string): Interaction =>
|
|
39
|
-
* Interaction.where(`#actor attaches text data`, async actor => {
|
|
39
|
+
* Interaction.where(the`#actor attaches text data`, async actor => {
|
|
40
40
|
* const data = await actor.answer(contents);
|
|
41
41
|
*
|
|
42
42
|
* actor.collect(
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { JSONObject } from 'tiny-types';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { asyncMap } from '../../io';
|
|
4
4
|
import type { UsesAbilities } from '../abilities';
|
|
5
5
|
import type { Answerable } from '../Answerable';
|
|
6
6
|
import { Interaction } from '../Interaction';
|
|
7
7
|
import type { QuestionAdapter } from '../Question';
|
|
8
8
|
import { Question } from '../Question';
|
|
9
|
-
import type { AnswersQuestions } from '../questions';
|
|
9
|
+
import type { AnswersQuestions, DescriptionFormattingOptions} from '../questions';
|
|
10
|
+
import { the } from '../questions';
|
|
10
11
|
import type { ChainableSetter } from './ChainableSetter';
|
|
11
12
|
import { TakeNotes } from './TakeNotes';
|
|
12
13
|
|
|
@@ -57,7 +58,7 @@ export class NotepadAdapter<Notes extends Record<any, any>> implements Chainable
|
|
|
57
58
|
get<Subject extends keyof Notes>(subject: Subject): QuestionAdapter<Notes[Subject]> {
|
|
58
59
|
return Question.about(`a note of ${ String(subject) }`, actor => {
|
|
59
60
|
return TakeNotes.as(actor).notepad.get(subject);
|
|
60
|
-
});
|
|
61
|
+
}).describedAs(Question.formattedValue());
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -207,7 +208,7 @@ export class NotepadAdapter<Notes extends Record<any, any>> implements Chainable
|
|
|
207
208
|
* - {@apilink Notepad.clear}
|
|
208
209
|
*/
|
|
209
210
|
clear(): Interaction {
|
|
210
|
-
return Interaction.where(
|
|
211
|
+
return Interaction.where(the`#actor clears ${ new NumberOfNotes() } from their notepad`, actor => {
|
|
211
212
|
return TakeNotes.as(actor).notepad.clear();
|
|
212
213
|
});
|
|
213
214
|
}
|
|
@@ -236,7 +237,7 @@ export class NotepadAdapter<Notes extends Record<any, any>> implements Chainable
|
|
|
236
237
|
* - {@apilink Notepad.size}
|
|
237
238
|
*/
|
|
238
239
|
size(): QuestionAdapter<number> {
|
|
239
|
-
return Question.about(
|
|
240
|
+
return Question.about(the`${ new NumberOfNotes() }`, async actor => {
|
|
240
241
|
return TakeNotes.as(actor).notepad.size();
|
|
241
242
|
});
|
|
242
243
|
}
|
|
@@ -296,7 +297,7 @@ type NotesToSet<Notes extends Record<any, any>> = {
|
|
|
296
297
|
class ChainableNoteSetter<Notes extends Record<any, any>> extends Interaction implements ChainableSetter<Notes> {
|
|
297
298
|
|
|
298
299
|
constructor(private readonly notes: NotesToSet<Notes>) {
|
|
299
|
-
super(
|
|
300
|
+
super(new DescriptionOfNotes(notes));
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
set<K extends keyof Notes>(subject: K, value: Answerable<Notes[K]>): ChainableSetter<Notes> & Interaction {
|
|
@@ -316,3 +317,53 @@ class ChainableNoteSetter<Notes extends Record<any, any>> extends Interaction im
|
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
}
|
|
320
|
+
|
|
321
|
+
class DescriptionOfNotes<Notes extends Record<any, any>>
|
|
322
|
+
extends Question<Promise<string>>
|
|
323
|
+
{
|
|
324
|
+
constructor(
|
|
325
|
+
private readonly notes: NotesToSet<Notes>,
|
|
326
|
+
private readonly options?: DescriptionFormattingOptions,
|
|
327
|
+
) {
|
|
328
|
+
super(`#actor takes notes: ${ Object.keys(notes).join(', ') }`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
332
|
+
const noteNames = Object.keys(this.notes);
|
|
333
|
+
const maxWidth = noteNames.reduce((max, name) => Math.max(max, name.length), 0);
|
|
334
|
+
|
|
335
|
+
const list = await asyncMap(noteNames, async noteName => {
|
|
336
|
+
const label = `${ noteName }:`.padEnd(maxWidth + 1);
|
|
337
|
+
const noteDescription = await actor.answer(Question.formattedValue(this.options).of(this.notes[noteName]));
|
|
338
|
+
|
|
339
|
+
return `- ${ label } ${ noteDescription }`
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return [
|
|
343
|
+
`#actor takes notes:`,
|
|
344
|
+
...list,
|
|
345
|
+
].join('\n');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async describedBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
349
|
+
return this.answeredBy(actor);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
class NumberOfNotes extends Question<Promise<number>> {
|
|
354
|
+
constructor() {
|
|
355
|
+
super('notes');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<number> {
|
|
359
|
+
return TakeNotes.as(actor).notepad.size();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async describedBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
363
|
+
const count = await this.answeredBy(actor);
|
|
364
|
+
|
|
365
|
+
return count === 1
|
|
366
|
+
? '1 note'
|
|
367
|
+
: `${ count } notes`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ValueInspector } from '../../io/reflection/ValueInspector';
|
|
2
|
+
import type { UsesAbilities } from '../abilities/UsesAbilities';
|
|
3
|
+
import type { Answerable } from '../Answerable';
|
|
4
|
+
import type { AnswersQuestions } from './AnswersQuestions';
|
|
5
|
+
|
|
6
|
+
const descriptionField = Symbol('description');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @group Questions
|
|
10
|
+
*/
|
|
11
|
+
export abstract class Describable {
|
|
12
|
+
|
|
13
|
+
private [descriptionField]: Answerable<string>;
|
|
14
|
+
|
|
15
|
+
protected constructor(description: Answerable<string>) {
|
|
16
|
+
this[descriptionField] = description;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the description of this object in the context of the provided `actor`.
|
|
21
|
+
*
|
|
22
|
+
* @param actor
|
|
23
|
+
*/
|
|
24
|
+
async describedBy(actor: AnswersQuestions & UsesAbilities & { name: string }): Promise<string> {
|
|
25
|
+
const description = await actor.answer(this[descriptionField]);
|
|
26
|
+
|
|
27
|
+
return description.replaceAll('#actor', actor.name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected setDescription(description: Answerable<string>): void {
|
|
31
|
+
this[descriptionField] = description;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected getDescription(): Answerable<string> {
|
|
35
|
+
return this[descriptionField];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a human-readable description of this object.
|
|
40
|
+
*/
|
|
41
|
+
toString(): string {
|
|
42
|
+
if (ValueInspector.isPromise(this[descriptionField])) {
|
|
43
|
+
return 'Promise';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return String(this[descriptionField]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for {@apilink Question.formattedValue} and
|
|
3
|
+
* the [`the`](/api/core/function/the/) function.
|
|
4
|
+
*
|
|
5
|
+
* @group Questions
|
|
6
|
+
*/
|
|
7
|
+
export interface DescriptionFormattingOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The maximum length of the string representation of the value.
|
|
10
|
+
* String representations longer than this value will be truncated and appended with an ellipsis.
|
|
11
|
+
*/
|
|
12
|
+
maxLength: number;
|
|
13
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { JSONValue } from 'tiny-types';
|
|
2
2
|
|
|
3
3
|
import { asyncMap, d } from '../../io';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { ExpectationDetails, ExpectationMet, ExpectationNotMet } from '../';
|
|
5
|
+
import type { Answerable, AnswersQuestions, QuestionAdapter} from '../index';
|
|
6
|
+
import { the } from '../index';
|
|
7
|
+
import { Question } from '../Question';
|
|
8
|
+
import { Describable } from '../questions';
|
|
6
9
|
import type { ExpectationOutcome } from './expectations';
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -19,7 +22,7 @@ type AnswerableArguments<Arguments extends Array<unknown>> = { [Index in keyof A
|
|
|
19
22
|
*
|
|
20
23
|
* @group Expectations
|
|
21
24
|
*/
|
|
22
|
-
export class Expectation<Actual> {
|
|
25
|
+
export class Expectation<Actual> extends Describable {
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* A factory method to that makes defining custom {@apilink Expectation|expectations} easier
|
|
@@ -108,13 +111,13 @@ export class Expectation<Actual> {
|
|
|
108
111
|
*/
|
|
109
112
|
static define<Actual_Type, PredicateArguments extends Array<unknown>>(
|
|
110
113
|
functionName: string,
|
|
111
|
-
relationship: ((...answerableArguments: AnswerableArguments<PredicateArguments>) => string) | string
|
|
114
|
+
relationship: ((...answerableArguments: AnswerableArguments<PredicateArguments>) => Answerable<string>) | Answerable<string>,
|
|
112
115
|
predicate: (actual: Actual_Type, ...predicateArguments: PredicateArguments) => Promise<boolean> | boolean,
|
|
113
116
|
): (...answerableArguments: AnswerableArguments<PredicateArguments>) => Expectation<Actual_Type>
|
|
114
117
|
{
|
|
115
118
|
return Object.defineProperty(function(...answerableArguments: AnswerableArguments<PredicateArguments>): Expectation<Actual_Type> {
|
|
116
|
-
const description = typeof relationship === 'function' ? relationship(...answerableArguments)
|
|
117
|
-
: (answerableArguments.length === 1 ?
|
|
119
|
+
const description: Answerable<string> = typeof relationship === 'function' ? relationship(...answerableArguments)
|
|
120
|
+
: (answerableArguments.length === 1 ? the`${ { toString: () => relationship } } ${ answerableArguments[0] }`
|
|
118
121
|
: relationship);
|
|
119
122
|
|
|
120
123
|
return new Expectation<Actual_Type>(
|
|
@@ -129,6 +132,8 @@ export class Expectation<Actual> {
|
|
|
129
132
|
|
|
130
133
|
const result = await predicate(actual, ...predicateArguments as PredicateArguments);
|
|
131
134
|
|
|
135
|
+
const descriptionText = await actor.answer(description);
|
|
136
|
+
|
|
132
137
|
const expectationDetails = ExpectationDetails.of(functionName, ...predicateArguments);
|
|
133
138
|
|
|
134
139
|
const expected = predicateArguments.length > 0
|
|
@@ -136,8 +141,8 @@ export class Expectation<Actual> {
|
|
|
136
141
|
: true; // the only parameter-less expectations are boolean ones like `isPresent`, `isActive`, etc.
|
|
137
142
|
|
|
138
143
|
return result
|
|
139
|
-
? new ExpectationMet(
|
|
140
|
-
: new ExpectationNotMet(
|
|
144
|
+
? new ExpectationMet(descriptionText, expectationDetails, expected, actual)
|
|
145
|
+
: new ExpectationNotMet(descriptionText, expectationDetails, expected, actual);
|
|
141
146
|
}
|
|
142
147
|
)
|
|
143
148
|
}, 'name', {value: functionName, writable: false});
|
|
@@ -243,9 +248,10 @@ export class Expectation<Actual> {
|
|
|
243
248
|
|
|
244
249
|
protected constructor(
|
|
245
250
|
private readonly functionName: string,
|
|
246
|
-
|
|
251
|
+
description: Answerable<string>,
|
|
247
252
|
private readonly predicate: Predicate<Actual>
|
|
248
253
|
) {
|
|
254
|
+
super(description);
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
/**
|
|
@@ -256,21 +262,15 @@ export class Expectation<Actual> {
|
|
|
256
262
|
* @param actual
|
|
257
263
|
*/
|
|
258
264
|
isMetFor(actual: Answerable<Actual>): QuestionAdapter<ExpectationOutcome> {
|
|
259
|
-
return Question.about(this.
|
|
265
|
+
return Question.about(this.getDescription(), actor => this.predicate(actor, actual));
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
/**
|
|
263
269
|
* @inheritDoc
|
|
264
270
|
*/
|
|
265
|
-
describedAs(
|
|
266
|
-
|
|
267
|
-
return this;
|
|
268
|
-
}
|
|
271
|
+
describedAs(description: Answerable<string>): this {
|
|
272
|
+
super.setDescription(description);
|
|
269
273
|
|
|
270
|
-
|
|
271
|
-
* @inheritDoc
|
|
272
|
-
*/
|
|
273
|
-
toString(): string {
|
|
274
|
-
return this.description;
|
|
274
|
+
return this;
|
|
275
275
|
}
|
|
276
276
|
}
|