@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/errors/ErrorFactory.js +4 -4
  3. package/lib/errors/ErrorFactory.js.map +1 -1
  4. package/lib/errors/RaiseErrors.d.ts +2 -2
  5. package/lib/errors/RaiseErrors.js +2 -2
  6. package/lib/io/index.d.ts +0 -1
  7. package/lib/io/index.d.ts.map +1 -1
  8. package/lib/io/index.js +0 -1
  9. package/lib/io/index.js.map +1 -1
  10. package/lib/io/inspectedObject.js +1 -1
  11. package/lib/io/inspectedObject.js.map +1 -1
  12. package/lib/io/reflection/ValueInspector.d.ts +56 -0
  13. package/lib/io/reflection/ValueInspector.d.ts.map +1 -0
  14. package/lib/io/reflection/ValueInspector.js +149 -0
  15. package/lib/io/reflection/ValueInspector.js.map +1 -0
  16. package/lib/io/reflection/index.d.ts +1 -2
  17. package/lib/io/reflection/index.d.ts.map +1 -1
  18. package/lib/io/reflection/index.js +1 -2
  19. package/lib/io/reflection/index.js.map +1 -1
  20. package/lib/io/stringified.js +7 -90
  21. package/lib/io/stringified.js.map +1 -1
  22. package/lib/screenplay/Activity.d.ts +5 -12
  23. package/lib/screenplay/Activity.d.ts.map +1 -1
  24. package/lib/screenplay/Activity.js +3 -14
  25. package/lib/screenplay/Activity.js.map +1 -1
  26. package/lib/screenplay/Actor.js +1 -1
  27. package/lib/screenplay/Actor.js.map +1 -1
  28. package/lib/screenplay/Interaction.d.ts +4 -3
  29. package/lib/screenplay/Interaction.d.ts.map +1 -1
  30. package/lib/screenplay/Interaction.js +2 -2
  31. package/lib/screenplay/Interaction.js.map +1 -1
  32. package/lib/screenplay/Question.d.ts +73 -21
  33. package/lib/screenplay/Question.d.ts.map +1 -1
  34. package/lib/screenplay/Question.js +237 -30
  35. package/lib/screenplay/Question.js.map +1 -1
  36. package/lib/screenplay/Task.d.ts +16 -15
  37. package/lib/screenplay/Task.d.ts.map +1 -1
  38. package/lib/screenplay/Task.js +14 -14
  39. package/lib/screenplay/Task.js.map +1 -1
  40. package/lib/screenplay/abilities/Ability.d.ts +8 -6
  41. package/lib/screenplay/abilities/Ability.d.ts.map +1 -1
  42. package/lib/screenplay/abilities/Ability.js +8 -6
  43. package/lib/screenplay/abilities/Ability.js.map +1 -1
  44. package/lib/screenplay/abilities/AbilityType.d.ts +3 -3
  45. package/lib/screenplay/abilities/AnswerQuestions.d.ts +0 -1
  46. package/lib/screenplay/abilities/AnswerQuestions.d.ts.map +1 -1
  47. package/lib/screenplay/abilities/AnswerQuestions.js +2 -4
  48. package/lib/screenplay/abilities/AnswerQuestions.js.map +1 -1
  49. package/lib/screenplay/abilities/PerformActivities.d.ts +5 -3
  50. package/lib/screenplay/abilities/PerformActivities.d.ts.map +1 -1
  51. package/lib/screenplay/abilities/PerformActivities.js +12 -10
  52. package/lib/screenplay/abilities/PerformActivities.js.map +1 -1
  53. package/lib/screenplay/artifacts/CollectsArtifacts.d.ts +2 -2
  54. package/lib/screenplay/notes/NotepadAdapter.d.ts.map +1 -1
  55. package/lib/screenplay/notes/NotepadAdapter.js +44 -4
  56. package/lib/screenplay/notes/NotepadAdapter.js.map +1 -1
  57. package/lib/screenplay/questions/Describable.d.ts +27 -0
  58. package/lib/screenplay/questions/Describable.d.ts.map +1 -0
  59. package/lib/screenplay/questions/Describable.js +40 -0
  60. package/lib/screenplay/questions/Describable.js.map +1 -0
  61. package/lib/screenplay/questions/DescriptionFormattingOptions.d.ts +14 -0
  62. package/lib/screenplay/questions/DescriptionFormattingOptions.d.ts.map +1 -0
  63. package/lib/screenplay/questions/DescriptionFormattingOptions.js +3 -0
  64. package/lib/screenplay/questions/DescriptionFormattingOptions.js.map +1 -0
  65. package/lib/screenplay/questions/Expectation.d.ts +6 -10
  66. package/lib/screenplay/questions/Expectation.d.ts.map +1 -1
  67. package/lib/screenplay/questions/Expectation.js +12 -15
  68. package/lib/screenplay/questions/Expectation.js.map +1 -1
  69. package/lib/screenplay/questions/List.d.ts +1 -3
  70. package/lib/screenplay/questions/List.d.ts.map +1 -1
  71. package/lib/screenplay/questions/List.js +6 -31
  72. package/lib/screenplay/questions/List.js.map +1 -1
  73. package/lib/screenplay/questions/Unanswered.d.ts +1 -0
  74. package/lib/screenplay/questions/Unanswered.d.ts.map +1 -1
  75. package/lib/screenplay/questions/Unanswered.js +3 -0
  76. package/lib/screenplay/questions/Unanswered.js.map +1 -1
  77. package/lib/screenplay/questions/expectations/ExpectationDetails.js +1 -1
  78. package/lib/screenplay/questions/expectations/ExpectationDetails.js.map +1 -1
  79. package/lib/screenplay/questions/index.d.ts +3 -1
  80. package/lib/screenplay/questions/index.d.ts.map +1 -1
  81. package/lib/screenplay/questions/index.js +3 -1
  82. package/lib/screenplay/questions/index.js.map +1 -1
  83. package/lib/screenplay/questions/tag-functions.d.ts +228 -0
  84. package/lib/screenplay/questions/tag-functions.d.ts.map +1 -0
  85. package/lib/screenplay/questions/tag-functions.js +115 -0
  86. package/lib/screenplay/questions/tag-functions.js.map +1 -0
  87. package/lib/screenplay/time/activities/Wait.d.ts.map +1 -1
  88. package/lib/screenplay/time/activities/Wait.js +4 -3
  89. package/lib/screenplay/time/activities/Wait.js.map +1 -1
  90. package/package.json +4 -4
  91. package/src/errors/ErrorFactory.ts +5 -5
  92. package/src/errors/RaiseErrors.ts +2 -2
  93. package/src/io/index.ts +0 -1
  94. package/src/io/inspectedObject.ts +2 -2
  95. package/src/io/reflection/ValueInspector.ts +165 -0
  96. package/src/io/reflection/index.ts +1 -2
  97. package/src/io/stringified.ts +7 -103
  98. package/src/screenplay/Activity.ts +6 -17
  99. package/src/screenplay/Actor.ts +2 -2
  100. package/src/screenplay/Interaction.ts +5 -4
  101. package/src/screenplay/Question.ts +299 -49
  102. package/src/screenplay/Task.ts +18 -17
  103. package/src/screenplay/abilities/Ability.ts +8 -6
  104. package/src/screenplay/abilities/AbilityType.ts +3 -3
  105. package/src/screenplay/abilities/AnswerQuestions.ts +2 -5
  106. package/src/screenplay/abilities/PerformActivities.ts +35 -18
  107. package/src/screenplay/artifacts/CollectsArtifacts.ts +2 -2
  108. package/src/screenplay/notes/NotepadAdapter.ts +57 -6
  109. package/src/screenplay/questions/Describable.ts +48 -0
  110. package/src/screenplay/questions/DescriptionFormattingOptions.ts +13 -0
  111. package/src/screenplay/questions/Expectation.ts +19 -19
  112. package/src/screenplay/questions/List.ts +7 -41
  113. package/src/screenplay/questions/Unanswered.ts +4 -0
  114. package/src/screenplay/questions/expectations/ExpectationDetails.ts +2 -2
  115. package/src/screenplay/questions/index.ts +3 -1
  116. package/src/screenplay/questions/tag-functions.ts +313 -0
  117. package/src/screenplay/time/activities/Wait.ts +4 -3
  118. package/lib/io/isPlainObject.d.ts +0 -7
  119. package/lib/io/isPlainObject.d.ts.map +0 -1
  120. package/lib/io/isPlainObject.js +0 -25
  121. package/lib/io/isPlainObject.js.map +0 -1
  122. package/lib/io/reflection/isPrimitive.d.ts +0 -8
  123. package/lib/io/reflection/isPrimitive.d.ts.map +0 -1
  124. package/lib/io/reflection/isPrimitive.js +0 -24
  125. package/lib/io/reflection/isPrimitive.js.map +0 -1
  126. package/lib/io/reflection/typeOf.d.ts +0 -7
  127. package/lib/io/reflection/typeOf.d.ts.map +0 -1
  128. package/lib/io/reflection/typeOf.js +0 -35
  129. package/lib/io/reflection/typeOf.js.map +0 -1
  130. package/lib/screenplay/questions/q.d.ts +0 -66
  131. package/lib/screenplay/questions/q.d.ts.map +0 -1
  132. package/lib/screenplay/questions/q.js +0 -77
  133. package/lib/screenplay/questions/q.js.map +0 -1
  134. package/src/io/isPlainObject.ts +0 -24
  135. package/src/io/reflection/isPrimitive.ts +0 -20
  136. package/src/io/reflection/typeOf.ts +0 -31
  137. package/src/screenplay/questions/q.ts +0 -82
@@ -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, d } from '@serenity-js/core'
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(d`#actor signs is as ${ username }`,
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
- * // => task marked as pending
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(`#actors finds a flight from ${ originCity } to ${ destinationCity }`, // task goal
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, ...activities: Activity[]): Task {
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, private activities: Activity[]) {
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 Test
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') // generic actor
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') // generic actor
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') // generic actor
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) && AnswerQuestions.isAPromise(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
- Outcome,
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 { Ability } from './index';
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 = this.stage.currentSceneId();
43
- const details = this.detailsOf(activity);
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(new activityStarts(sceneId, activityId, details, this.stage.currentTime()))
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
- this.stage.announce(new activityFinished(sceneId, activityId, details, new ExecutionSuccessful(), this.stage.currentTime()));
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(activity: Activity): ActivityDetails {
94
+ private detailsOf(name: string, instantiationLocation: FileSystemLocation): ActivityDetails {
78
95
  return new ActivityDetails(
79
- new Name(this.nameOf(activity)),
80
- activity.instantiationLocation(),
96
+ new Name(name),
97
+ instantiationLocation,
81
98
  )
82
99
  }
83
100
 
84
101
  protected nameOf(activity: Activity): string {
85
- const template = activity.toString() === ({}).toString()
86
- ? `#actor performs ${ activity.constructor.name }`
87
- : activity.toString();
102
+ const template = ValueInspector.hasItsOwnToString(activity)
103
+ ? activity.toString()
104
+ : `#actor performs ${ activity.constructor.name }`;
88
105
 
89
- return template.replace('#actor', this.actor.name);
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 { commaSeparated } from '../../io';
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('#actor clears their notepad', actor => {
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('number of notes', async actor => {
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(`#actor takes note of ${ commaSeparated(Object.keys(notes)) }`);
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 type { Answerable, AnswersQuestions, QuestionAdapter } from '../';
5
- import { ExpectationDetails, ExpectationMet, ExpectationNotMet, Question } from '../';
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 ? relationship.trim() + d` ${answerableArguments[0]}`
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(description, expectationDetails, expected, actual)
140
- : new ExpectationNotMet(description, expectationDetails, expected, actual);
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
- private description: string,
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.description, actor => this.predicate(actor, actual));
265
+ return Question.about(this.getDescription(), actor => this.predicate(actor, actual));
260
266
  }
261
267
 
262
268
  /**
263
269
  * @inheritDoc
264
270
  */
265
- describedAs(subject: string): this {
266
- this.description = subject;
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
  }