@serenity-js/core 3.23.2 → 3.24.1

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 +32 -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 +6 -6
  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
@@ -16,7 +16,7 @@ import { ExpectationMet } from './expectations';
16
16
  *
17
17
  * @group Questions
18
18
  */
19
- export abstract class List<Item_Type> extends Question<Promise<Item_Type[]>> {
19
+ export abstract class List<Item_Type> extends Question<Promise<Array<Item_Type>>> {
20
20
  protected subject?: string;
21
21
 
22
22
  static of<IT, CT, RQT extends (Question<Promise<Array<IT>>> | Question<Array<IT>>)>(collection: Answerable<Array<IT>> & ChainableMetaQuestion<CT, RQT>): MetaList<CT, IT>;
@@ -30,7 +30,7 @@ export abstract class List<Item_Type> extends Question<Promise<Item_Type[]>> {
30
30
  }
31
31
 
32
32
  constructor(protected readonly collection: Answerable<Array<Item_Type>>) {
33
- super();
33
+ super(d`${ collection }`);
34
34
  }
35
35
 
36
36
  forEach(callback: (current: CurrentItem<Item_Type>, index: number, items: Array<Item_Type>) => Promise<void> | void): Task {
@@ -64,15 +64,6 @@ export abstract class List<Item_Type> extends Question<Promise<Item_Type[]>> {
64
64
  return collection;
65
65
  }
66
66
 
67
- describedAs(subject: string): this {
68
- this.subject = subject;
69
- return this;
70
- }
71
-
72
- toString(): string {
73
- return this.subject ?? d`${ this.collection }`;
74
- }
75
-
76
67
  /**
77
68
  * @param {number} index
78
69
  */
@@ -287,21 +278,17 @@ export class MetaList<Supported_Context_Type, Item_Type>
287
278
  class Where<Item_Type, Answer_Type>
288
279
  extends Question<Promise<Array<Item_Type>>>
289
280
  {
290
- private subject: string;
291
-
292
281
  constructor(
293
282
  protected readonly collection: Answerable<Array<Item_Type>>,
294
283
  protected readonly question: MetaQuestion<Item_Type, Question<Promise<Answer_Type> | Answer_Type>>,
295
284
  protected readonly expectation: Expectation<Answer_Type>,
296
285
  originalSubject: string,
297
286
  ) {
298
- super();
299
-
300
- const prefix = this.collection instanceof Where
287
+ const prefix = collection instanceof Where
301
288
  ? ' and'
302
289
  : ' where';
303
290
 
304
- this.subject = originalSubject + prefix + d` ${ question } does ${ expectation }`;
291
+ super(originalSubject + prefix + d` ${ question } does ${ expectation }`);
305
292
  }
306
293
 
307
294
  async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Array<Item_Type>> {
@@ -319,19 +306,11 @@ class Where<Item_Type, Answer_Type>
319
306
  }
320
307
 
321
308
  return results;
322
- } catch (error) {
309
+ }
310
+ catch (error) {
323
311
  throw new LogicError(d`Couldn't check if ${ this.question } of an item of ${ this.collection } does ${ this.expectation }: ` + error.message, error);
324
312
  }
325
313
  }
326
-
327
- describedAs(subject: string): this {
328
- this.subject = subject;
329
- return this;
330
- }
331
-
332
- toString(): string {
333
- return this.subject;
334
- }
335
314
  }
336
315
 
337
316
  /**
@@ -356,16 +335,12 @@ class MetaWhere<Supported_Context_Type, Item_Type, Answer_Type>
356
335
  */
357
336
  class EachMappedTo<Item_Type, Mapped_Item_Type> extends Question<Promise<Array<Mapped_Item_Type>>> {
358
337
 
359
- private subject: string;
360
-
361
338
  constructor(
362
339
  protected readonly collection: Answerable<Array<Item_Type>>,
363
340
  protected readonly mapping: MetaQuestion<Item_Type, Question<Promise<Mapped_Item_Type> | Mapped_Item_Type>>,
364
341
  originalSubject: string,
365
342
  ) {
366
- super();
367
-
368
- this.subject = originalSubject + d` mapped to ${ this.mapping }`;
343
+ super(originalSubject + d` mapped to ${ mapping }`);
369
344
  }
370
345
 
371
346
  async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Array<Mapped_Item_Type>> {
@@ -379,15 +354,6 @@ class EachMappedTo<Item_Type, Mapped_Item_Type> extends Question<Promise<Array<M
379
354
 
380
355
  return mapped;
381
356
  }
382
-
383
- describedAs(subject: string): this {
384
- this.subject = subject;
385
- return this;
386
- }
387
-
388
- toString(): string {
389
- return this.subject;
390
- }
391
357
  }
392
358
 
393
359
  /**
@@ -14,6 +14,10 @@ import * as util from 'util'; // eslint-disable-line unicorn/import-style
14
14
  * @group Questions
15
15
  */
16
16
  export class Unanswered extends TinyType {
17
+ static isUnanswered(value: unknown): value is Unanswered {
18
+ return value instanceof Unanswered;
19
+ }
20
+
17
21
  [util.inspect.custom](): string {
18
22
  return `<<unanswered>>`;
19
23
  }
@@ -1,7 +1,7 @@
1
1
  import type { JSONObject, JSONValue} from 'tiny-types';
2
2
  import { ensure, isArray, isDefined, TinyType } from 'tiny-types';
3
3
 
4
- import { inspected, typeOf } from '../../../io';
4
+ import { inspected, ValueInspector } from '../../../io';
5
5
  import { Name } from '../../../model';
6
6
  import { Unanswered } from '../Unanswered';
7
7
 
@@ -55,7 +55,7 @@ export class ExpectationDetails extends TinyType {
55
55
  return {
56
56
  name: this.name.value,
57
57
  args: this.args.map(arg => ({
58
- type: typeOf(arg),
58
+ type: ValueInspector.typeOf(arg),
59
59
  value: arg['toJSON']
60
60
  ? (arg as any).toJSON()
61
61
  : arg,
@@ -1,10 +1,12 @@
1
1
  export * from './AnswersQuestions';
2
2
  export * from './ChainableMetaQuestion';
3
3
  export * from './Check';
4
+ export * from './Describable';
5
+ export * from './DescriptionFormattingOptions';
4
6
  export * from './Expectation';
5
7
  export * from './expectations';
6
8
  export * from './List';
7
9
  export * from './Masked';
8
10
  export * from './MetaQuestion';
9
- export * from './q';
11
+ export * from './tag-functions';
10
12
  export * from './Unanswered';
@@ -0,0 +1,313 @@
1
+ import { asyncMap, ValueInspector } from '../../io';
2
+ import type { UsesAbilities } from '../abilities';
3
+ import type { Answerable } from '../Answerable';
4
+ import type { MetaQuestionAdapter, QuestionAdapter } from '../Question';
5
+ import { Question } from '../Question';
6
+ import type { AnswersQuestions } from './AnswersQuestions';
7
+ import { Describable } from './Describable';
8
+ import type { DescriptionFormattingOptions } from './DescriptionFormattingOptions';
9
+ import type { MetaQuestion } from './MetaQuestion';
10
+
11
+ /**
12
+ * Creates a single-line description of an {@apilink Activity} by transforming
13
+ * a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates),
14
+ * parameterised with [primitive data types](https://developer.mozilla.org/en-US/docs/Glossary/Primitive),
15
+ * [complex data structures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#objects),
16
+ * or any other {@apilink Answerable|Answerables}, into a {@link QuestionAdapter|`QuestionAdapter<string>`}
17
+ * that can be used with {@apilink Task.where} and {@apilink Interaction.where} methods.
18
+ *
19
+ * ```ts
20
+ * const dial = (phoneNumber: Answerable<string>) =>
21
+ * Task.where(the `#actor dials ${ phoneNumber }`, /* *\/)
22
+ *
23
+ * await actorCalled('Alice').attemptsTo(
24
+ * dial('(555) 123-4567'),
25
+ * // reported as: Alice dials "(555) 123-4567"
26
+ * )
27
+ * ```
28
+ *
29
+ * ## Trimming the output
30
+ *
31
+ * Use {@apilink DescriptionFormattingOptions} to trim the descriptions of template parameters.
32
+ * By default, the output is displayed in full.
33
+ *
34
+ * ```ts
35
+ * import { actorCalled, Task, the } from '@serenity-js/core'
36
+ *
37
+ * const dial = (phoneNumber: Answerable<string>) =>
38
+ * Task.where(dial({ maxLength: 10 }) `#actor dials ${ phoneNumber }`, /* *\/)
39
+ *
40
+ * await actorCalled('Alice').attemptsTo(
41
+ * dial('(555) 123-4567'),
42
+ * // reported as: Alice dials "(555) 123-...'
43
+ * )
44
+ * ```
45
+ *
46
+ * ## Using with Questions
47
+ *
48
+ * When `the` is parameterised with {@apilink Question|Questions},
49
+ * it retrieves their description by calling {@apilink Question.describedBy}
50
+ * in the context of the {@apilink Actor} performing the {@apilink Activity}.
51
+ *
52
+ * ```ts
53
+ * import { actorCalled, Question, Task, the } from '@serenity-js/core'
54
+ *
55
+ * const phoneNumber = (areaCode: string, centralOfficeCode: string, lineNumber: string) =>
56
+ * Question.about('phone number', actor => {
57
+ * return `(${ this.areaCode }) ${ this.centralOfficeCode }-${ this.lineNumber }`
58
+ * })
59
+ *
60
+ * const dial = (phoneNumber: Answerable<string>) =>
61
+ * Task.where(dial({ maxLength: 10 }) `#actor dials ${ phoneNumber }`, /* *\/)
62
+ *
63
+ * await actorCalled('Alice').attemptsTo(
64
+ * dial(phoneNumber('555', '123', '4567'),
65
+ * // reported as: Alice dials phone number
66
+ * )
67
+ * ```
68
+ *
69
+ * If you'd like the question to be described using its formatted value instead of its description, use {@apilink Question.formattedValue}.
70
+ *
71
+ * ```ts
72
+ * import { actorCalled, Question, Task, the } from '@serenity-js/core'
73
+ *
74
+ * const phoneNumber = (areaCode: string, centralOfficeCode: string, lineNumber: string) =>
75
+ * Question.about('phone number', actor => {
76
+ * return `(${ this.areaCode }) ${ this.centralOfficeCode }-${ this.lineNumber }`
77
+ * }).describedAs(Question.formattedValue())
78
+ *
79
+ * const dial = (phoneNumber: Answerable<string>) =>
80
+ * Task.where(dial({ maxLength: 10 }) `#actor dials ${ phoneNumber }`, /* *\/)
81
+ *
82
+ * await actorCalled('Alice').attemptsTo(
83
+ * dial(phoneNumber('555', '123', '4567'),
84
+ * // reported as: Alice dials "(555) 123-4567"
85
+ * )
86
+ * ```
87
+ *
88
+ * ## Using with objects with a custom `toString` method
89
+ *
90
+ * When `the` is parameterised with objects that have
91
+ * a custom [`toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) method,
92
+ * or {@link Answerable|Answerables} resolving to such objects, the `toString()` method is called to produce the resulting description.
93
+ *
94
+ * ```ts
95
+ * import { actorCalled, description, Task } from '@serenity-js/core'
96
+ *
97
+ * class PhoneNumber {
98
+ * constructor(
99
+ * private readonly areaCode: string,
100
+ * private readonly centralOfficeCode: string,
101
+ * private readonly lineNumber: string,
102
+ * ) {}
103
+ *
104
+ * toString() {
105
+ * return `(${ this.areaCode }) ${ this.centralOfficeCode }-${ this.lineNumber }`
106
+ * }
107
+ * }
108
+ *
109
+ * const dial = (phoneNumber: Answerable<PhoneNumber>) =>
110
+ * Task.where(description `#actor dials ${ phoneNumber }`, /* *\/)
111
+ *
112
+ * await actorCalled('Alice').attemptsTo(
113
+ * dial(new PhoneNumber('555', '123', '4567')),
114
+ * // reported as: Alice dials (555) 123-4567
115
+ * )
116
+ * ```
117
+ *
118
+ * ## Using with objects without a custom `toString` method
119
+ *
120
+ * When `the` is parameterised with complex objects that don't have a custom `toString()` method,
121
+ * or {@link Answerable}s resolving to such objects,
122
+ * the resulting description will contain a JSON-like string representation of the object.
123
+ *
124
+ * ```ts
125
+ * import { actorCalled, description, Task } from '@serenity-js/core'
126
+ *
127
+ * interface PhoneNumber {
128
+ * areaCode: string;
129
+ * centralOfficeCode: string;
130
+ * lineNumber: string;
131
+ * }
132
+ *
133
+ * const dial = (phoneNumber: Answerable<PhoneNumber>) =>
134
+ * Task.where(the `#actor dials ${ phoneNumber }`, /* *\/)
135
+ *
136
+ * await actorCalled('Alice').attemptsTo(
137
+ * dial({ areaCode: '555', centralOfficeCode: '123', lineNumber: '4567' }),
138
+ * // reported as: Alice dials { areaCode: "555", centralOfficeCode: "123", lineNumber: "4567" }
139
+ * )
140
+ * ```
141
+ *
142
+ * ## Using with masked values
143
+ *
144
+ * When `the` is parameterised with {@apilink Masked} values,
145
+ * the resulting description will contain a masked representation of the values.
146
+ *
147
+ * ```ts
148
+ * import { actorCalled, description, Task } from '@serenity-js/core'
149
+ *
150
+ * const dial = (phoneNumber: Answerable<string>) =>
151
+ * Task.where(description `#actor dials ${ phoneNumber }`, /* *\/)
152
+ *
153
+ * await actorCalled('Alice').attemptsTo(
154
+ * dial(Masked.valueOf('(555) 123-4567')),
155
+ * // reported as: Alice dials [a masked value]
156
+ * )
157
+ * ```
158
+ *
159
+ * ## Learn more
160
+ *
161
+ * - {@apilink Answerable}
162
+ * - {@apilink Question}
163
+ * - {@apilink Question.describedAs}
164
+ * - {@apilink QuestionAdapter}
165
+ * - {@apilink Masked}
166
+ *
167
+ * @group Questions
168
+ */
169
+ export function the(options: DescriptionFormattingOptions): <Supported_Context_Type>(templates: TemplateStringsArray, ...placeholders: Array<MetaQuestion<Supported_Context_Type, any> | any>) => MetaQuestionAdapter<Supported_Context_Type, string>
170
+ export function the<Supported_Context_Type>(templates: TemplateStringsArray, ...parameters: Array<MetaQuestion<Supported_Context_Type, any> | any>): MetaQuestionAdapter<Supported_Context_Type, string>
171
+ export function the(...args: any[]): any {
172
+ if (ValueInspector.isPlainObject(args[0])) {
173
+ const descriptionFormattingOptions = args[0] as DescriptionFormattingOptions;
174
+
175
+ return (templates: TemplateStringsArray, ...parameters: Array<any>) =>
176
+ templateToQuestion(templates, parameters, createParameterToDescriptionMapper(descriptionFormattingOptions), createParameterValueToDescriptionMapper(descriptionFormattingOptions));
177
+ }
178
+
179
+ return templateToQuestion(args[0], args.slice(1), createParameterToDescriptionMapper(), createParameterValueToDescriptionMapper());
180
+ }
181
+
182
+ /**
183
+ * A Serenity/JS Screenplay Pattern-flavour
184
+ * of a [tagged template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates),
185
+ * `q` is a tag function capable of resolving any `Answerable<string>` or `Answerable<number>`
186
+ * you parametrise it with, and returning a `QuestionAdapter<string>`.
187
+ *
188
+ * Use `q` to concatenate `string` and `number` values returned from synchronous an asynchronous sources.
189
+ *
190
+ * ## Interpolating questions
191
+ *
192
+ * ```ts
193
+ * import { q, actorCalled } from '@serenity-js/core'
194
+ * import { Send, DeleteRequest } from '@serenity-js/rest'
195
+ * import { Text } from '@serenity-js/web'
196
+ *
197
+ * await actorCalled('Alice').attemptsTo(
198
+ * Send.a(DeleteRequest.to(
199
+ * q `/articles/${ Text.of(Article.id()) }`
200
+ * ))
201
+ * )
202
+ * ```
203
+ *
204
+ * ## Using a custom description
205
+ *
206
+ * ```ts
207
+ * import { q, actorCalled } from '@serenity-js/core'
208
+ * import { Send, DeleteRequest } from '@serenity-js/rest'
209
+ *
210
+ * await actorCalled('Alice').attemptsTo(
211
+ * Send.a(DeleteRequest.to(
212
+ * q `/articles/${ Text.of(Article.id()) }`.describedAs('/articles/:id')
213
+ * ))
214
+ * )
215
+ * ```
216
+ *
217
+ * ## Transforming the interpolated string
218
+ *
219
+ * The mechanism presented below relies on {@apilink QuestionAdapter}.
220
+ *
221
+ * ```ts
222
+ * import { q, actorCalled } from '@serenity-js/core'
223
+ * import { Send, DeleteRequest } from '@serenity-js/rest'
224
+ *
225
+ * await actorCalled('Alice').attemptsTo(
226
+ * Send.a(DeleteRequest.to(
227
+ * q `/articles/${ Text.of(Article.id()) }`.toLocaleLowerCase()
228
+ * ))
229
+ * )
230
+ * ```
231
+ *
232
+ * ## Learn more
233
+ *
234
+ * - {@apilink Answerable}
235
+ * - {@apilink Question}
236
+ * - {@apilink Question.describedAs}
237
+ * - {@apilink QuestionAdapter}
238
+ *
239
+ * @group Questions
240
+ *
241
+ * @param templates
242
+ * @param parameters
243
+ */
244
+ export function q(templates: TemplateStringsArray, ...parameters: Array<Answerable<string | number>>): QuestionAdapter<string> {
245
+ return templateToQuestion(
246
+ templates,
247
+ parameters,
248
+ (parameter: Answerable<string | number>) => {
249
+ // return static string and number parameter values as is
250
+ if (typeof parameter === 'string' || typeof parameter === 'number') {
251
+ return String(parameter);
252
+ }
253
+ // for Questions, Promises and other Answerables, return their description
254
+ return `{${ createParameterToDescriptionMapper()(parameter) }}`
255
+ },
256
+ createParameterValueMapper()
257
+ );
258
+ }
259
+
260
+ function createParameterToDescriptionMapper(options?: DescriptionFormattingOptions) {
261
+ return (parameter: any) =>
262
+ parameter === undefined
263
+ ? 'undefined'
264
+ : Question.formattedValue(options).of(parameter).toString();
265
+ }
266
+
267
+ function createParameterValueToDescriptionMapper(options?: DescriptionFormattingOptions) {
268
+ return async (actor: AnswersQuestions & UsesAbilities & { name: string }, parameter: any) =>
269
+ parameter instanceof Describable
270
+ ? parameter.describedBy(actor)
271
+ : actor.answer(Question.formattedValue(options).of(parameter))
272
+ }
273
+
274
+ function createParameterValueMapper() {
275
+ return async (actor: AnswersQuestions & UsesAbilities & { name: string }, parameter: Answerable<string | number>) =>
276
+ actor.answer(parameter)
277
+ }
278
+
279
+ function templateToQuestion(
280
+ templates: TemplateStringsArray,
281
+ parameters: Array<any>,
282
+ descriptionMapper: (parameter: any) => string,
283
+ valueMapper: (actor: AnswersQuestions & UsesAbilities & { name: string }, parameter: any) => Promise<any> | any,
284
+ ) {
285
+ const description = interpolate(templates, parameters.map(parameter => descriptionMapper(parameter)));
286
+
287
+ return Question.about<string, any>(description,
288
+ async (actor: AnswersQuestions & UsesAbilities & { name: string }) => {
289
+ const descriptions = await asyncMap(parameters, parameter => valueMapper(actor, parameter));
290
+
291
+ return interpolate(templates, descriptions);
292
+ },
293
+ (context: any) =>
294
+ templateToQuestion(
295
+ templates,
296
+ parameters.map(parameter =>
297
+ Question.isAMetaQuestion(parameter)
298
+ ? parameter.of(context)
299
+ : parameter
300
+ ),
301
+ descriptionMapper,
302
+ valueMapper,
303
+ )
304
+ );
305
+ }
306
+
307
+ function interpolate(templates: TemplateStringsArray, parameters: Array<any>): string {
308
+ return templates.flatMap((template, i) =>
309
+ i < parameters.length
310
+ ? [ template, parameters[i] ]
311
+ : [ template ],
312
+ ).join('');
313
+ }
@@ -5,7 +5,8 @@ import { d } from '../../../io';
5
5
  import type { UsesAbilities } from '../../abilities';
6
6
  import type { Answerable } from '../../Answerable';
7
7
  import { Interaction } from '../../Interaction';
8
- import type { AnswersQuestions, Expectation, ExpectationOutcome } from '../../questions';
8
+ import type { AnswersQuestions, Expectation, ExpectationOutcome} from '../../questions';
9
+ import { the } from '../../questions';
9
10
  import { ExpectationMet } from '../../questions';
10
11
  import { ScheduleWork } from '../abilities';
11
12
  import { Duration } from '../models';
@@ -202,7 +203,7 @@ export class Wait {
202
203
  */
203
204
  class WaitFor extends Interaction {
204
205
  constructor(private readonly duration: Answerable<Duration>) {
205
- super(d`#actor waits for ${ duration }`);
206
+ super(the`#actor waits for ${ duration }`);
206
207
  }
207
208
 
208
209
  async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
@@ -231,7 +232,7 @@ export class WaitUntil<Actual> extends Interaction {
231
232
  private readonly pollingInterval: Duration,
232
233
  private readonly timeout?: Duration,
233
234
  ) {
234
- super(d`#actor waits until ${ actual } does ${ expectation }`);
235
+ super(the`#actor waits until ${ actual } does ${ expectation }`);
235
236
 
236
237
  if (timeout) {
237
238
  ensure('Timeout', timeout.inMilliseconds(), isGreaterThanOrEqualTo(Wait.minimumTimeout.inMilliseconds()));
@@ -1,7 +0,0 @@
1
- /**
2
- * Checks if the value has a good chance of being a plain JavaScript object
3
- *
4
- * @param v
5
- */
6
- export declare function isPlainObject(v: unknown): v is object;
7
- //# sourceMappingURL=isPlainObject.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isPlainObject.d.ts","sourceRoot":"","sources":["../../src/io/isPlainObject.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAkBrD"}
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPlainObject = void 0;
4
- /**
5
- * Checks if the value has a good chance of being a plain JavaScript object
6
- *
7
- * @param v
8
- */
9
- function isPlainObject(v) {
10
- // Basic check for Type object that's not null
11
- if (typeof v === 'object' && v !== null) {
12
- // If Object.getPrototypeOf supported, use it
13
- if (typeof Object.getPrototypeOf === 'function') {
14
- const proto = Object.getPrototypeOf(v);
15
- return proto === Object.prototype || proto === null;
16
- }
17
- // Otherwise, use internal class
18
- // This should be reliable as if getPrototypeOf not supported, is pre-ES5
19
- return Object.prototype.toString.call(v) === '[object Object]';
20
- }
21
- // Not an object
22
- return false;
23
- }
24
- exports.isPlainObject = isPlainObject;
25
- //# sourceMappingURL=isPlainObject.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isPlainObject.js","sourceRoot":"","sources":["../../src/io/isPlainObject.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,SAAgB,aAAa,CAAC,CAAU;IAEpC,8CAA8C;IAC9C,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,EAAE;QAErC,6CAA6C;QAC7C,IAAI,OAAO,MAAM,CAAC,cAAc,KAAK,UAAU,EAAE;YAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;YACvC,OAAO,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC;SACvD;QAED,gCAAgC;QAChC,yEAAyE;QACzE,OAAO,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,iBAAiB,CAAC;KAClE;IAED,gBAAgB;IAChB,OAAO,KAAK,CAAC;AACjB,CAAC;AAlBD,sCAkBC"}
@@ -1,8 +0,0 @@
1
- /**
2
- * Returns true if `value` is a [JavaScript primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive),
3
- * false otherwise.
4
- *
5
- * @param value
6
- */
7
- export declare function isPrimitive(value: unknown): boolean;
8
- //# sourceMappingURL=isPrimitive.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isPrimitive.d.ts","sourceRoot":"","sources":["../../../src/io/reflection/isPrimitive.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAanD"}
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPrimitive = void 0;
4
- /**
5
- * Returns true if `value` is a [JavaScript primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive),
6
- * false otherwise.
7
- *
8
- * @param value
9
- */
10
- function isPrimitive(value) {
11
- if (value === null) {
12
- return true;
13
- }
14
- return [
15
- 'string',
16
- 'number',
17
- 'bigint',
18
- 'boolean',
19
- 'undefined',
20
- 'symbol'
21
- ].includes(typeof value);
22
- }
23
- exports.isPrimitive = isPrimitive;
24
- //# sourceMappingURL=isPrimitive.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isPrimitive.js","sourceRoot":"","sources":["../../../src/io/reflection/isPrimitive.ts"],"names":[],"mappings":";;;AAAA;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,IAAI,EAAE;QAChB,OAAO,IAAI,CAAC;KACf;IAED,OAAO;QACH,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,SAAS;QACT,WAAW;QACX,QAAQ;KACX,CAAC,QAAQ,CAAC,OAAO,KAAK,CAAC,CAAC;AAC7B,CAAC;AAbD,kCAaC"}
@@ -1,7 +0,0 @@
1
- /**
2
- * Describes the type of the provided value.
3
- *
4
- * @param value
5
- */
6
- export declare function typeOf(value: unknown): string;
7
- //# sourceMappingURL=typeOf.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"typeOf.d.ts","sourceRoot":"","sources":["../../../src/io/reflection/typeOf.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAuB7C"}
@@ -1,35 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.typeOf = void 0;
4
- const util_1 = require("util");
5
- /**
6
- * Describes the type of the provided value.
7
- *
8
- * @param value
9
- */
10
- function typeOf(value) {
11
- switch (true) {
12
- case value === null:
13
- return 'null';
14
- case util_1.types.isProxy(value):
15
- return `Proxy<${Reflect.getPrototypeOf(value).constructor.name}>`;
16
- case typeof value !== 'object':
17
- return typeof value;
18
- case value instanceof Date:
19
- return `Date`;
20
- case Array.isArray(value):
21
- return `Array`;
22
- case value instanceof RegExp:
23
- return `RegExp`;
24
- case value instanceof Set:
25
- return 'Set';
26
- case value instanceof Map:
27
- return 'Map';
28
- case !!value.constructor && value.constructor !== Object:
29
- return value.constructor.name;
30
- default:
31
- return 'object';
32
- }
33
- }
34
- exports.typeOf = typeOf;
35
- //# sourceMappingURL=typeOf.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"typeOf.js","sourceRoot":"","sources":["../../../src/io/reflection/typeOf.ts"],"names":[],"mappings":";;;AAAA,+BAA6B;AAE7B;;;;GAIG;AACH,SAAgB,MAAM,CAAC,KAAc;IACjC,QAAQ,IAAI,EAAE;QACV,KAAK,KAAK,KAAK,IAAI;YACf,OAAO,MAAM,CAAC;QAClB,KAAK,YAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACrB,OAAO,SAAU,OAAO,CAAC,cAAc,CAAC,KAAe,CAAC,CAAC,WAAW,CAAC,IAAK,GAAG,CAAC;QAClF,KAAK,OAAO,KAAK,KAAK,QAAQ;YAC1B,OAAO,OAAO,KAAK,CAAC;QACxB,KAAK,KAAK,YAAY,IAAI;YACtB,OAAO,MAAM,CAAC;QAClB,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACrB,OAAO,OAAO,CAAC;QACnB,KAAK,KAAK,YAAY,MAAM;YACxB,OAAO,QAAQ,CAAA;QACnB,KAAK,KAAK,YAAY,GAAG;YACrB,OAAO,KAAK,CAAC;QACjB,KAAK,KAAK,YAAY,GAAG;YACrB,OAAO,KAAK,CAAC;QACjB,KAAK,CAAC,CAAE,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,KAAK,MAAM;YACrD,OAAO,KAAK,CAAC,WAAW,CAAC,IAAI,CAAA;QACjC;YACI,OAAO,QAAQ,CAAC;KACvB;AACL,CAAC;AAvBD,wBAuBC"}