@serenity-js/core 2.33.1 → 3.0.0-rc.11

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 (210) hide show
  1. package/CHANGELOG.md +476 -0
  2. package/lib/index.d.ts +2 -1
  3. package/lib/index.js +6 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/io/ErrorSerialiser.js +4 -1
  6. package/lib/io/ErrorSerialiser.js.map +1 -1
  7. package/lib/io/ErrorStackParser.d.ts +2 -2
  8. package/lib/io/ErrorStackParser.js.map +1 -1
  9. package/lib/io/asyncMap.d.ts +8 -0
  10. package/lib/io/asyncMap.js +18 -0
  11. package/lib/io/asyncMap.js.map +1 -0
  12. package/lib/io/format.d.ts +39 -0
  13. package/lib/io/format.js +51 -0
  14. package/lib/io/format.js.map +1 -0
  15. package/lib/io/formatted.d.ts +5 -1
  16. package/lib/io/formatted.js +6 -13
  17. package/lib/io/formatted.js.map +1 -1
  18. package/lib/io/index.d.ts +2 -1
  19. package/lib/io/index.js +2 -1
  20. package/lib/io/index.js.map +1 -1
  21. package/lib/io/inspected.d.ts +9 -1
  22. package/lib/io/inspected.js +52 -15
  23. package/lib/io/inspected.js.map +1 -1
  24. package/lib/model/Timestamp.d.ts +4 -2
  25. package/lib/model/Timestamp.js +8 -2
  26. package/lib/model/Timestamp.js.map +1 -1
  27. package/lib/screenplay/Optional.d.ts +29 -0
  28. package/lib/{io/collections/reducible.js → screenplay/Optional.js} +1 -1
  29. package/lib/screenplay/Optional.js.map +1 -0
  30. package/lib/screenplay/Question.d.ts +41 -82
  31. package/lib/screenplay/Question.js +132 -100
  32. package/lib/screenplay/Question.js.map +1 -1
  33. package/lib/screenplay/actor/Actor.js +2 -2
  34. package/lib/screenplay/actor/Actor.js.map +1 -1
  35. package/lib/screenplay/index.d.ts +1 -1
  36. package/lib/screenplay/index.js +1 -1
  37. package/lib/screenplay/index.js.map +1 -1
  38. package/lib/screenplay/interactions/index.d.ts +0 -1
  39. package/lib/screenplay/interactions/index.js +0 -1
  40. package/lib/screenplay/interactions/index.js.map +1 -1
  41. package/lib/screenplay/questions/Check.d.ts +3 -3
  42. package/lib/screenplay/questions/Check.js +5 -7
  43. package/lib/screenplay/questions/Check.js.map +1 -1
  44. package/lib/screenplay/questions/Expectation.d.ts +15 -10
  45. package/lib/screenplay/questions/Expectation.js +28 -37
  46. package/lib/screenplay/questions/Expectation.js.map +1 -1
  47. package/lib/screenplay/questions/List.d.ts +22 -192
  48. package/lib/screenplay/questions/List.js +160 -208
  49. package/lib/screenplay/questions/List.js.map +1 -1
  50. package/lib/screenplay/questions/Note.d.ts +10 -0
  51. package/lib/screenplay/questions/Note.js +17 -1
  52. package/lib/screenplay/questions/Note.js.map +1 -1
  53. package/lib/screenplay/questions/index.d.ts +0 -3
  54. package/lib/screenplay/questions/index.js +0 -5
  55. package/lib/screenplay/questions/index.js.map +1 -1
  56. package/lib/stage/index.d.ts +0 -2
  57. package/lib/stage/index.js +0 -2
  58. package/lib/stage/index.js.map +1 -1
  59. package/package.json +7 -6
  60. package/src/index.ts +2 -1
  61. package/src/io/ErrorSerialiser.ts +5 -1
  62. package/src/io/ErrorStackParser.ts +2 -1
  63. package/src/io/asyncMap.ts +18 -0
  64. package/src/io/format.ts +49 -0
  65. package/src/io/formatted.ts +7 -15
  66. package/src/io/index.ts +2 -1
  67. package/src/io/inspected.ts +68 -15
  68. package/src/model/Timestamp.ts +10 -2
  69. package/src/screenplay/Optional.ts +30 -0
  70. package/src/screenplay/Question.ts +206 -124
  71. package/src/screenplay/actor/Actor.ts +2 -2
  72. package/src/screenplay/index.ts +1 -1
  73. package/src/screenplay/interactions/index.ts +0 -1
  74. package/src/screenplay/questions/Check.ts +10 -15
  75. package/src/screenplay/questions/Expectation.ts +47 -55
  76. package/src/screenplay/questions/List.ts +224 -233
  77. package/src/screenplay/questions/Note.ts +21 -1
  78. package/src/screenplay/questions/index.ts +0 -3
  79. package/src/stage/index.ts +0 -2
  80. package/lib/io/collections/index.d.ts +0 -2
  81. package/lib/io/collections/index.js +0 -15
  82. package/lib/io/collections/index.js.map +0 -1
  83. package/lib/io/collections/mappable.d.ts +0 -52
  84. package/lib/io/collections/mappable.js +0 -28
  85. package/lib/io/collections/mappable.js.map +0 -1
  86. package/lib/io/collections/reducible.d.ts +0 -16
  87. package/lib/io/collections/reducible.js.map +0 -1
  88. package/lib/screenplay/interactions/See.d.ts +0 -31
  89. package/lib/screenplay/interactions/See.js +0 -43
  90. package/lib/screenplay/interactions/See.js.map +0 -1
  91. package/lib/screenplay/questions/Property.d.ts +0 -91
  92. package/lib/screenplay/questions/Property.js +0 -99
  93. package/lib/screenplay/questions/Property.js.map +0 -1
  94. package/lib/screenplay/questions/Transform.d.ts +0 -31
  95. package/lib/screenplay/questions/Transform.js +0 -46
  96. package/lib/screenplay/questions/Transform.js.map +0 -1
  97. package/lib/screenplay/questions/lists/ArrayListAdapter.d.ts +0 -88
  98. package/lib/screenplay/questions/lists/ArrayListAdapter.js +0 -152
  99. package/lib/screenplay/questions/lists/ArrayListAdapter.js.map +0 -1
  100. package/lib/screenplay/questions/lists/ListAdapter.d.ts +0 -20
  101. package/lib/screenplay/questions/lists/ListAdapter.js +0 -3
  102. package/lib/screenplay/questions/lists/ListAdapter.js.map +0 -1
  103. package/lib/screenplay/questions/lists/index.d.ts +0 -2
  104. package/lib/screenplay/questions/lists/index.js +0 -15
  105. package/lib/screenplay/questions/lists/index.js.map +0 -1
  106. package/lib/screenplay/questions/mappings/AnswerMappingFunction.d.ts +0 -11
  107. package/lib/screenplay/questions/mappings/AnswerMappingFunction.js +0 -3
  108. package/lib/screenplay/questions/mappings/AnswerMappingFunction.js.map +0 -1
  109. package/lib/screenplay/questions/mappings/index.d.ts +0 -2
  110. package/lib/screenplay/questions/mappings/index.js +0 -15
  111. package/lib/screenplay/questions/mappings/index.js.map +0 -1
  112. package/lib/screenplay/questions/mappings/string/append.d.ts +0 -14
  113. package/lib/screenplay/questions/mappings/string/append.js +0 -25
  114. package/lib/screenplay/questions/mappings/string/append.js.map +0 -1
  115. package/lib/screenplay/questions/mappings/string/index.d.ts +0 -11
  116. package/lib/screenplay/questions/mappings/string/index.js +0 -24
  117. package/lib/screenplay/questions/mappings/string/index.js.map +0 -1
  118. package/lib/screenplay/questions/mappings/string/normalize.d.ts +0 -20
  119. package/lib/screenplay/questions/mappings/string/normalize.js +0 -30
  120. package/lib/screenplay/questions/mappings/string/normalize.js.map +0 -1
  121. package/lib/screenplay/questions/mappings/string/replace.d.ts +0 -17
  122. package/lib/screenplay/questions/mappings/string/replace.js +0 -30
  123. package/lib/screenplay/questions/mappings/string/replace.js.map +0 -1
  124. package/lib/screenplay/questions/mappings/string/slice.d.ts +0 -28
  125. package/lib/screenplay/questions/mappings/string/slice.js +0 -47
  126. package/lib/screenplay/questions/mappings/string/slice.js.map +0 -1
  127. package/lib/screenplay/questions/mappings/string/split.d.ts +0 -19
  128. package/lib/screenplay/questions/mappings/string/split.js +0 -36
  129. package/lib/screenplay/questions/mappings/string/split.js.map +0 -1
  130. package/lib/screenplay/questions/mappings/string/toLocaleLowerCase.d.ts +0 -17
  131. package/lib/screenplay/questions/mappings/string/toLocaleLowerCase.js +0 -28
  132. package/lib/screenplay/questions/mappings/string/toLocaleLowerCase.js.map +0 -1
  133. package/lib/screenplay/questions/mappings/string/toLocaleUpperCase.d.ts +0 -17
  134. package/lib/screenplay/questions/mappings/string/toLocaleUpperCase.js +0 -29
  135. package/lib/screenplay/questions/mappings/string/toLocaleUpperCase.js.map +0 -1
  136. package/lib/screenplay/questions/mappings/string/toLowerCase.d.ts +0 -10
  137. package/lib/screenplay/questions/mappings/string/toLowerCase.js +0 -19
  138. package/lib/screenplay/questions/mappings/string/toLowerCase.js.map +0 -1
  139. package/lib/screenplay/questions/mappings/string/toNumber.d.ts +0 -10
  140. package/lib/screenplay/questions/mappings/string/toNumber.js +0 -18
  141. package/lib/screenplay/questions/mappings/string/toNumber.js.map +0 -1
  142. package/lib/screenplay/questions/mappings/string/toUpperCase.d.ts +0 -10
  143. package/lib/screenplay/questions/mappings/string/toUpperCase.js +0 -19
  144. package/lib/screenplay/questions/mappings/string/toUpperCase.js.map +0 -1
  145. package/lib/screenplay/questions/mappings/string/trim.d.ts +0 -12
  146. package/lib/screenplay/questions/mappings/string/trim.js +0 -21
  147. package/lib/screenplay/questions/mappings/string/trim.js.map +0 -1
  148. package/lib/screenplay/questions/proxies/PropertyPathKey.d.ts +0 -4
  149. package/lib/screenplay/questions/proxies/PropertyPathKey.js +0 -3
  150. package/lib/screenplay/questions/proxies/PropertyPathKey.js.map +0 -1
  151. package/lib/screenplay/questions/proxies/createMetaQuestionProxy.d.ts +0 -14
  152. package/lib/screenplay/questions/proxies/createMetaQuestionProxy.js +0 -35
  153. package/lib/screenplay/questions/proxies/createMetaQuestionProxy.js.map +0 -1
  154. package/lib/screenplay/questions/proxies/createQuestionProxy.d.ts +0 -13
  155. package/lib/screenplay/questions/proxies/createQuestionProxy.js +0 -34
  156. package/lib/screenplay/questions/proxies/createQuestionProxy.js.map +0 -1
  157. package/lib/screenplay/questions/proxies/describePath.d.ts +0 -5
  158. package/lib/screenplay/questions/proxies/describePath.js +0 -19
  159. package/lib/screenplay/questions/proxies/describePath.js.map +0 -1
  160. package/lib/screenplay/questions/proxies/index.d.ts +0 -2
  161. package/lib/screenplay/questions/proxies/index.js +0 -15
  162. package/lib/screenplay/questions/proxies/index.js.map +0 -1
  163. package/lib/screenplay/questions/proxies/key.d.ts +0 -8
  164. package/lib/screenplay/questions/proxies/key.js +0 -16
  165. package/lib/screenplay/questions/proxies/key.js.map +0 -1
  166. package/lib/screenplay/tasks/Loop.d.ts +0 -198
  167. package/lib/screenplay/tasks/Loop.js +0 -222
  168. package/lib/screenplay/tasks/Loop.js.map +0 -1
  169. package/lib/screenplay/tasks/index.d.ts +0 -1
  170. package/lib/screenplay/tasks/index.js +0 -14
  171. package/lib/screenplay/tasks/index.js.map +0 -1
  172. package/lib/stage/DressingRoom.d.ts +0 -37
  173. package/lib/stage/DressingRoom.js +0 -53
  174. package/lib/stage/DressingRoom.js.map +0 -1
  175. package/lib/stage/WithStage.d.ts +0 -51
  176. package/lib/stage/WithStage.js +0 -3
  177. package/lib/stage/WithStage.js.map +0 -1
  178. package/src/io/collections/index.ts +0 -2
  179. package/src/io/collections/mappable.ts +0 -60
  180. package/src/io/collections/reducible.ts +0 -16
  181. package/src/screenplay/interactions/See.ts +0 -45
  182. package/src/screenplay/questions/Property.ts +0 -98
  183. package/src/screenplay/questions/Transform.ts +0 -51
  184. package/src/screenplay/questions/lists/ArrayListAdapter.ts +0 -186
  185. package/src/screenplay/questions/lists/ListAdapter.ts +0 -33
  186. package/src/screenplay/questions/lists/index.ts +0 -2
  187. package/src/screenplay/questions/mappings/AnswerMappingFunction.ts +0 -13
  188. package/src/screenplay/questions/mappings/index.ts +0 -2
  189. package/src/screenplay/questions/mappings/string/append.ts +0 -28
  190. package/src/screenplay/questions/mappings/string/index.ts +0 -11
  191. package/src/screenplay/questions/mappings/string/normalize.ts +0 -33
  192. package/src/screenplay/questions/mappings/string/replace.ts +0 -34
  193. package/src/screenplay/questions/mappings/string/slice.ts +0 -53
  194. package/src/screenplay/questions/mappings/string/split.ts +0 -38
  195. package/src/screenplay/questions/mappings/string/toLocaleLowerCase.ts +0 -31
  196. package/src/screenplay/questions/mappings/string/toLocaleUpperCase.ts +0 -30
  197. package/src/screenplay/questions/mappings/string/toLowerCase.ts +0 -20
  198. package/src/screenplay/questions/mappings/string/toNumber.ts +0 -19
  199. package/src/screenplay/questions/mappings/string/toUpperCase.ts +0 -20
  200. package/src/screenplay/questions/mappings/string/trim.ts +0 -22
  201. package/src/screenplay/questions/proxies/PropertyPathKey.ts +0 -4
  202. package/src/screenplay/questions/proxies/createMetaQuestionProxy.ts +0 -51
  203. package/src/screenplay/questions/proxies/createQuestionProxy.ts +0 -49
  204. package/src/screenplay/questions/proxies/describePath.ts +0 -23
  205. package/src/screenplay/questions/proxies/index.ts +0 -2
  206. package/src/screenplay/questions/proxies/key.ts +0 -14
  207. package/src/screenplay/tasks/Loop.ts +0 -240
  208. package/src/screenplay/tasks/index.ts +0 -1
  209. package/src/stage/DressingRoom.ts +0 -53
  210. package/src/stage/WithStage.ts +0 -52
@@ -1,6 +1,8 @@
1
- import { isMappable, Mappable } from '../io/collections';
1
+ import { f } from '../io';
2
2
  import { AnswersQuestions, UsesAbilities } from './actor';
3
- import { AnswerMappingFunction } from './questions/mappings';
3
+ import { Answerable } from './Answerable';
4
+ import { Interaction } from './Interaction';
5
+ import { Optional } from './Optional';
4
6
 
5
7
  /**
6
8
  * @desc
@@ -52,16 +54,6 @@ import { AnswerMappingFunction } from './questions/mappings';
52
54
  * @abstract
53
55
  */
54
56
  export abstract class Question<T> {
55
-
56
- /**
57
- * @param {string} subject
58
- * The subject of this question
59
- *
60
- * @protected
61
- */
62
- protected constructor(protected subject: string) {
63
- }
64
-
65
57
  /**
66
58
  * @desc
67
59
  * Factory method that simplifies the process of defining custom questions.
@@ -77,8 +69,8 @@ export abstract class Question<T> {
77
69
  *
78
70
  * @returns {Question<R>}
79
71
  */
80
- static about<R>(description: string, body: (actor: AnswersQuestions & UsesAbilities) => R): Question<R> {
81
- return new AnonymousQuestion<R>(description, body);
72
+ static about<R>(description: string, body: (actor: AnswersQuestions & UsesAbilities) => Promise<R> | R): QuestionAdapter<Awaited<R>> {
73
+ return Question.createAdapter(new QuestionStatement(description, body));
82
74
  }
83
75
 
84
76
  /**
@@ -96,15 +88,91 @@ export abstract class Question<T> {
96
88
  return !! maybeQuestion && !! (maybeQuestion as any).answeredBy;
97
89
  }
98
90
 
91
+ protected static createAdapter<AT>(statement: Question<AT>): QuestionAdapter<Awaited<AT>> {
92
+ return new Proxy<() => Question<AT>, QuestionStatement<AT>>(() => statement, {
93
+
94
+ get(currentStatement: () => Question<AT>, key: string | symbol, receiver: any) {
95
+ const target = currentStatement();
96
+
97
+ if (key === Symbol.toPrimitive) {
98
+ return (_hint: 'number' | 'string' | 'default') => {
99
+ return target.toString();
100
+ }
101
+ }
102
+
103
+ if (key in target) {
104
+ return Reflect.get(target, key)
105
+ }
106
+
107
+ if (key === 'then') {
108
+ return;
109
+ }
110
+
111
+ const originalSubject = f`${ target }`;
112
+
113
+ const fieldDescription = (typeof key === 'number' || /^\d+$/.test(String(key)))
114
+ ? `[${ String(key) }]` // array index
115
+ : `.${ String(key) }`; // field/method name
116
+
117
+ return Question.about(`${ originalSubject }${ fieldDescription }`, async (actor: AnswersQuestions & UsesAbilities) => {
118
+ const answer = await actor.answer(target as Answerable<AT>);
119
+
120
+ if (! isDefined(answer)) {
121
+ return undefined; // eslint-disable-line unicorn/no-useless-undefined
122
+ }
123
+
124
+ const field = answer[key];
125
+
126
+ return typeof field === 'function'
127
+ ? field.bind(answer)
128
+ : field;
129
+ })
130
+ },
131
+
132
+ set(currentStatement: () => Question<AT>, key: string | symbol, value: any, receiver: any): boolean {
133
+ const target = currentStatement();
134
+
135
+ return Reflect.set(target, key, value);
136
+ },
137
+
138
+ apply(currentStatement: () => Question<AT>, _thisArgument: any, parameters: unknown[]): QuestionAdapter<AT> {
139
+
140
+ const target = currentStatement();
141
+
142
+ const parameterDescriptions = [
143
+ '(',
144
+ parameters.map(p => f`${ p }`).join(', '),
145
+ ')'
146
+ ].join('');
147
+
148
+ return Question.about(target.toString() + parameterDescriptions, async actor => {
149
+ const params = [] as any;
150
+ for (const parameter of parameters) {
151
+ const answered = await actor.answer(parameter);
152
+ params.push(answered);
153
+ }
154
+
155
+ const field = await actor.answer(target);
156
+
157
+ return typeof field === 'function'
158
+ ? field(...params)
159
+ : field;
160
+ });
161
+ },
162
+
163
+ getPrototypeOf(currentStatement: () => Question<AT>): object | null {
164
+ return Reflect.getPrototypeOf(currentStatement());
165
+ },
166
+ }) as any;
167
+ }
168
+
99
169
  /**
100
170
  * @desc
101
171
  * Describes the subject of this {@link Question}.
102
172
  *
103
173
  * @returns {string}
104
174
  */
105
- toString(): string {
106
- return this.subject;
107
- }
175
+ abstract toString(): string;
108
176
 
109
177
  /**
110
178
  * @desc
@@ -113,135 +181,149 @@ export abstract class Question<T> {
113
181
  * @param {string} subject
114
182
  * @returns {Question<T>}
115
183
  */
116
- describedAs(subject: string): this {
117
- this.subject = subject;
184
+ abstract describedAs(subject: string): this;
118
185
 
119
- return this;
186
+ /**
187
+ * @abstract
188
+ */
189
+ abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T;
190
+
191
+ public as<O>(mapping: (answer: Awaited<T>) => Promise<O> | O): QuestionAdapter<O> {
192
+ return Question.about<O>(f`${ this }.as(${ mapping })`, async actor => {
193
+ const answer = (await actor.answer(this)) as Awaited<T>;
194
+ return mapping(answer);
195
+ });
196
+ }
197
+ }
198
+
199
+ declare global {
200
+ interface ProxyConstructor {
201
+ new <Source_Type extends object, Target_Type extends object>(target: Source_Type, handler: ProxyHandler<Source_Type>): Target_Type;
202
+ }
203
+ }
204
+
205
+ /* eslint-disable @typescript-eslint/indent */
206
+ export type ProxiedAnswer<Original_Type> = {
207
+ [Field in keyof Omit<Original_Type, keyof QuestionStatement<Original_Type>>]:
208
+ // is it a method?
209
+ Original_Type[Field] extends (...args: infer OriginalParameters) => infer OriginalMethodResult
210
+ // make the method signature asynchronous, accepting Answerables and returning a Promise
211
+ ? (...args: { [P in keyof OriginalParameters]: Answerable<OriginalParameters[P]> }) =>
212
+ { isPresent(): Question<Promise<boolean>> } & QuestionAdapter<Awaited<OriginalMethodResult>>
213
+ // is it an object? wrap each field
214
+ : { isPresent(): Question<Promise<boolean>> } & QuestionAdapter<Awaited<Original_Type[Field]>>
215
+ };
216
+ /* eslint-enable @typescript-eslint/indent */
217
+
218
+ export type QuestionAdapter<Original_Type> =
219
+ Question<Promise<Original_Type>> &
220
+ Interaction &
221
+ Optional &
222
+ ProxiedAnswer<Original_Type>;
223
+
224
+ class QuestionStatement<Answer_Type> extends Interaction implements Question<Promise<Answer_Type>>, Optional {
225
+
226
+ constructor(
227
+ private subject: string,
228
+ private readonly body: (actor: AnswersQuestions & UsesAbilities, ...Parameters) => Promise<Answer_Type> | Answer_Type,
229
+ ) {
230
+ super();
120
231
  }
121
232
 
122
233
  /**
123
234
  * @desc
124
- * Creates a new {@link Question}, which value is a result of applying the `mapping`
125
- * function to the value of this {@link Question}.
126
- *
127
- * @example <caption>Mapping a Question<Promise<string>> to Question<Promise<number>></caption>
128
- * import { Question, replace, toNumber } from '@serenity-js/core';
129
- *
130
- * Question.about('the price of some item', actor => '$3.99')
131
- * .map(replace('$', ''))
132
- * .map(toNumber)
133
- *
134
- * // => Question<Promise<number>>
135
- * // 3.99
136
- *
137
- * @example <caption>Mapping all items of Question<string[]> to Question<Promise<number>></caption>
138
- * import { Question, trim } from '@serenity-js/core';
139
- *
140
- * Question.about('things to do', actor => [ ' walk the dog ', ' read a book ' ])
141
- * .map(trim())
142
- *
143
- * // => Question<Promise<string[]>>
144
- * // [ 'walk the dog', 'read a book' ]
145
- *
146
- * @example <caption>Using a custom mapping function</caption>
147
- * import { Question } from '@serenity-js/core';
235
+ * Returns a Question that resolves to `true` if resolving the {@link QuestionStatement}
236
+ * returns a value other than `null` or `undefined` and doesn't throw errors.
148
237
  *
149
- * Question.about('normalised percentages', actor => [ 0.1, 0.3, 0.6 ])
150
- * .map((actor: AnswersQuestions) => (value: number) => value * 100)
151
- *
152
- * // => Question<Promise<number[]>>
153
- * // [ 10, 30, 60 ]
154
- *
155
- * @example <caption>Extracting values from LastResponse.body()</caption>
156
- * import { Question } from '@serenity-js/core';
157
- * import { LastResponse } from '@serenity-js/rest';
158
- *
159
- * interface UserDetails {
160
- * id: number;
161
- * name: string;
162
- * }
163
- *
164
- * LastResponse.body<UserDetails>().map(actor => details => details.id)
165
- *
166
- * // => Question<number>
167
- *
168
- * @param {function(value: A, index?: number): Promise<O> | O} mapping
169
- * A mapping function that receives a value of type `<A>`, which is either:
170
- * - an answer to the original question, if the question is defined as `Question<Promise<A>>` or `Question<A>`
171
- * - or, if the question is defined as `Question<Promise<Mappable<A>>`, `Question<Mappable<A>>` - each item of the {@link Mappable} collection,
172
- *
173
- * @returns {Question<Promise<Mapped>>}
174
- * A new Question which value is a result of applying the `mapping` function
175
- * to the value of the current question, so that:
176
- * - if the answer to the current question is a `Mappable<A>`, the result becomes `Question<Promise<O[]>>`
177
- * - if the answer is a value `<A>` or `Promise<A>`, the result becomes `Question<Promise<O>>`
178
- *
179
- * @see {@link AnswerMappingFunction}
180
- * @see {@link Mappable}
238
+ * @returns {Question<Promise<boolean>>}
181
239
  */
182
- map<O>(mapping: AnswerMappingFunction<AnswerOrItemOfMappableCollection<T>, O>): Question<Promise<Mapped<T, O>>> {
183
- return Question.about(this.subject, actor =>
184
- actor.answer(this).then(value =>
185
- (isMappable(value)
186
- ? Promise.all(((value).map(mapping(actor)) as Array<PromiseLike<O> | O>))
187
- : mapping(actor)(value as AnswerOrItemOfMappableCollection<T>)
188
- ) as Promise<Mapped<T, O>>
189
- )
190
- ) as Question<Promise<Mapped<T, O>>>;
240
+ isPresent(): Question<Promise<boolean>> {
241
+ return new IsPresent(f`${ this }.isPresent()`, this.body);
191
242
  }
192
243
 
193
- /**
194
- * @abstract
195
- */
196
- abstract answeredBy(actor: AnswersQuestions & UsesAbilities): T;
244
+ async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<Answer_Type> {
245
+ const result = await this.body(actor);
246
+
247
+ return isDefined(result)
248
+ ? result
249
+ : undefined;
250
+ }
251
+
252
+ async performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
253
+ await this.body(actor);
254
+ }
255
+
256
+ describedAs(subject: string): this {
257
+ this.subject = subject;
258
+ return this;
259
+ }
260
+
261
+ toString(): string {
262
+ return this.subject;
263
+ }
264
+
265
+ as<O>(mapping: (answer: Awaited<Answer_Type>) => (Promise<O> | O)): QuestionAdapter<O>{
266
+ return Question.about<O>(f`${ this }.as(${ mapping })`, async actor => {
267
+ const answer = await actor.answer(this);
268
+
269
+ if (! isDefined(answer)) {
270
+ return undefined; // eslint-disable-line unicorn/no-useless-undefined
271
+ }
272
+
273
+ return mapping(answer);
274
+ });
275
+ }
197
276
  }
198
277
 
199
278
  /**
200
279
  * @package
201
280
  */
202
- type AnswerOrItemOfMappableCollection<V> =
203
- V extends PromiseLike<infer PromisedValue>
204
- ? PromisedValue extends Mappable<infer Item>
205
- ? Item
206
- : PromisedValue
207
- : V extends Mappable<infer Item>
208
- ? Item
209
- : V;
281
+ class IsPresent<T> extends Question<Promise<boolean>> {
210
282
 
211
- /**
212
- * @package
213
- */
214
- type Mapped<T, O> =
215
- T extends PromiseLike<infer PromisedValue>
216
- ? PromisedValue extends Mappable<infer Item> // eslint-disable-line @typescript-eslint/no-unused-vars
217
- ? O[]
218
- : O
219
- : T extends Mappable<infer Item> // eslint-disable-line @typescript-eslint/no-unused-vars
220
- ? O[]
221
- : O
283
+ constructor(
284
+ private subject: string,
285
+ private readonly body: (actor: AnswersQuestions & UsesAbilities) => T,
286
+ ) {
287
+ super();
288
+ }
222
289
 
223
- /**
224
- * @package
225
- */
226
- class AnonymousQuestion<T> extends Question<T> {
227
- constructor(private description: string, private body: (actor: AnswersQuestions & UsesAbilities) => T) {
228
- super(description);
290
+ async answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<boolean> {
291
+ try {
292
+ const answer = await this.body(actor);
293
+
294
+ if (answer === undefined || answer === null) {
295
+ return false;
296
+ }
297
+
298
+ if (this.isOptional(answer)) {
299
+ return await actor.answer(answer.isPresent());
300
+ }
301
+
302
+ return true;
303
+ } catch {
304
+ return false;
305
+ }
229
306
  }
230
307
 
231
- answeredBy(actor: AnswersQuestions & UsesAbilities) {
232
- return this.body(actor);
308
+ private isOptional(maybeOptional: any): maybeOptional is Optional {
309
+ return typeof maybeOptional === 'object'
310
+ && Reflect.has(maybeOptional, 'isPresent');
233
311
  }
234
312
 
235
- /**
236
- * Changes the description of this question's subject
237
- * and produces a new instance without mutating the original one.
238
- *
239
- * @param {string} subject
240
- * @returns {Question<T>}
241
- */
242
313
  describedAs(subject: string): this {
243
314
  this.subject = subject;
244
-
245
315
  return this;
246
316
  }
317
+
318
+ toString(): string {
319
+ return this.subject;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * @package
325
+ */
326
+ function isDefined<T>(value: T): boolean {
327
+ return value !== undefined
328
+ && value !== null;
247
329
  }
@@ -114,11 +114,11 @@ export class Actor implements
114
114
  */
115
115
  answer<T>(answerable: Answerable<T>): Promise<T> {
116
116
  function isAPromise<V>(v: Answerable<V>): v is Promise<V> {
117
- return !!(v as any).then;
117
+ return Object.prototype.hasOwnProperty.call(v, 'then');
118
118
  }
119
119
 
120
120
  function isDefined<V>(v: Answerable<V>) {
121
- return ! (answerable === undefined || answerable === null);
121
+ return ! (v === undefined || v === null);
122
122
  }
123
123
 
124
124
  if (isDefined(answerable) && isAPromise(answerable)) {
@@ -6,7 +6,7 @@ export * from './actor';
6
6
  export * from './Answerable';
7
7
  export * from './Interaction';
8
8
  export * from './interactions';
9
+ export * from './Optional';
9
10
  export * from './Question';
10
11
  export * from './questions';
11
12
  export * from './Task';
12
- export * from './tasks';
@@ -1,3 +1,2 @@
1
1
  export * from './Log';
2
- export * from './See';
3
2
  export * from './TakeNote';
@@ -1,4 +1,4 @@
1
- import { formatted } from '../../io';
1
+ import { d } from '../../io';
2
2
  import { Activity } from '../Activity';
3
3
  import { AnswersQuestions, PerformsActivities } from '../actor';
4
4
  import { Answerable } from '../Answerable';
@@ -44,7 +44,7 @@ import { ExpectationMet } from './expectations';
44
44
  */
45
45
  export class Check<Actual> extends Task {
46
46
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
47
- static whether<A>(actual: Answerable<A>, expectation: Expectation<any, A>) {
47
+ static whether<A>(actual: Answerable<A>, expectation: Expectation<A>) {
48
48
  return {
49
49
  andIfSo: (...activities: Activity[]) => new Check(actual, expectation, activities),
50
50
  };
@@ -59,7 +59,7 @@ export class Check<Actual> extends Task {
59
59
  */
60
60
  constructor(
61
61
  private readonly actual: Answerable<Actual>,
62
- private readonly expectation: Expectation<any, Actual>,
62
+ private readonly expectation: Expectation<Actual>,
63
63
  private readonly activities: Activity[],
64
64
  private readonly alternativeActivities: Activity[] = [],
65
65
  ) {
@@ -86,17 +86,12 @@ export class Check<Actual> extends Task {
86
86
  * @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
87
87
  * @see {@link @serenity-js/core/lib/screenplay/actor~PerformsActivities}
88
88
  */
89
- performAs(actor: AnswersQuestions & PerformsActivities): PromiseLike<void> {
90
- return Promise.all([
91
- actor.answer(this.actual),
92
- actor.answer(this.expectation),
93
- ]).then(([actual, expectation]) =>
94
- expectation(actual).then(outcome =>
95
- outcome instanceof ExpectationMet
96
- ? actor.attemptsTo(...this.activities)
97
- : actor.attemptsTo(...this.alternativeActivities),
98
- ),
99
- );
89
+ async performAs(actor: AnswersQuestions & PerformsActivities): Promise<void> {
90
+ const outcome = await actor.answer(this.expectation.isMetFor(this.actual));
91
+
92
+ return outcome instanceof ExpectationMet
93
+ ? actor.attemptsTo(...this.activities)
94
+ : actor.attemptsTo(...this.alternativeActivities);
100
95
  }
101
96
 
102
97
  /**
@@ -106,6 +101,6 @@ export class Check<Actual> extends Task {
106
101
  * @returns {string}
107
102
  */
108
103
  toString(): string {
109
- return formatted `#actor checks whether ${ this.actual } does ${ this.expectation }`;
104
+ return d`#actor checks whether ${ this.actual } does ${ this.expectation }`;
110
105
  }
111
106
  }
@@ -1,13 +1,14 @@
1
- import { formatted } from '../../io';
2
- import { Answerable, AnswersQuestions, Question } from '../';
3
- import { ExpectationMet, ExpectationNotMet, ExpectationOutcome } from './expectations';
1
+ import { d } from '../../io';
2
+ import { Answerable, AnswersQuestions, ExpectationMet, ExpectationNotMet, Question, QuestionAdapter } from '../';
3
+ import { ExpectationOutcome } from './expectations';
4
4
 
5
5
  /**
6
6
  * @public
7
7
  *
8
- * @typedef {function(actual: A, expected: E) => boolean} Predicate<A,E>
8
+ * @typedef {function(actual: Answerable<Actual>) => Promise<ExpectationOutcome<Expected, Actual>> | ExpectationOutcome<unknown, Actual>} Predicate<Actual>
9
9
  */
10
- export type Predicate<A, E> = (actual: A, expected: E) => boolean;
10
+ export type Predicate<Actual> = (actor: AnswersQuestions, actual: Answerable<Actual>) =>
11
+ Promise<ExpectationOutcome<unknown, Actual>> | ExpectationOutcome<unknown, Actual>; // eslint-disable-line @typescript-eslint/indent
11
12
 
12
13
  /**
13
14
  * @desc
@@ -16,9 +17,7 @@ export type Predicate<A, E> = (actual: A, expected: E) => boolean;
16
17
  *
17
18
  * @extends {Question}
18
19
  */
19
- export abstract class Expectation<Expected, Actual = Expected>
20
- extends Question<(actual: Actual) => Promise<ExpectationOutcome<Expected, Actual>>>
21
- {
20
+ export class Expectation<Actual> {
22
21
 
23
22
  /**
24
23
  * @desc
@@ -40,14 +39,28 @@ export abstract class Expectation<Expected, Actual = Expected>
40
39
  * @param {string} relationshipName
41
40
  * @param {@serenity-js/core/lib/screenplay~Answerable<E>} expectedValue
42
41
  *
43
- * @returns {"soThat": function(predicate: Predicate<A,E>): Expectation<E, A>}
42
+ * @returns {"soThat": function(predicate: Predicate<Expected, Actual>): Expectation<Expected, Actual>}
44
43
  */
45
- static thatActualShould<E, A>(relationshipName: string, expectedValue: Answerable<E>): {
46
- soThat: (predicate: Predicate<A, E>) => Expectation<E, A>,
44
+ static thatActualShould<E, A>(relationshipName: string, expectedValue?: Answerable<E>): {
45
+ soThat: (simplifiedPredicate: (actualValue: A, expectedValue: E) => Promise<boolean> | boolean) => Expectation<A>,
47
46
  } {
48
47
  return ({
49
- soThat: (predicate: Predicate<A, E>): Expectation<E, A> => {
50
- return new DynamicallyGeneratedExpectation<E, A>(relationshipName, predicate, expectedValue);
48
+ soThat: (simplifiedPredicate: (actualValue: A, expectedValue: E) => Promise<boolean> | boolean): Expectation<A> => {
49
+ const subject = relationshipName + ' ' + d`${expectedValue}`;
50
+
51
+ return new Expectation<A>(
52
+ subject,
53
+ async (actor: AnswersQuestions, actualValue: Answerable<A>): Promise<ExpectationOutcome<E, A>> => {
54
+ const expected = await actor.answer(expectedValue);
55
+ const actual = await actor.answer(actualValue);
56
+
57
+ const result = await simplifiedPredicate(actual, expected);
58
+
59
+ return result
60
+ ? new ExpectationMet(subject, expected, actual)
61
+ : new ExpectationNotMet(subject, expected, actual);
62
+ }
63
+ );
51
64
  },
52
65
  });
53
66
  }
@@ -77,60 +90,39 @@ export abstract class Expectation<Expected, Actual = Expected>
77
90
  *
78
91
  * @returns {"soThat": function(...expectations: Array<Expectation<any, A>>): Expectation<any, A>}
79
92
  */
80
- static to<A>(relationshipName: string): {
81
- soThatActual: (...expectations: Array<Expectation<any, A>>) => Expectation<any, A>,
93
+ static to<E, A>(relationshipName: string): {
94
+ soThatActual: (...expectations: Array<Expectation<A>>) => Expectation<A>,
82
95
  } {
83
96
  return {
84
- soThatActual: (expectation: Expectation<any, A>): Expectation<any, A> => {
85
- return new ExpectationAlias<A>(relationshipName, expectation);
97
+ soThatActual: (expectation: Expectation<A>): Expectation<A> => {
98
+ return new Expectation<A>(
99
+ relationshipName,
100
+ async (actor: AnswersQuestions, actualValue: Answerable<A>): Promise<ExpectationOutcome<E, A>> => {
101
+ const outcome = await actor.answer(expectation.isMetFor(actualValue));
102
+
103
+ return outcome as ExpectationOutcome<E, A>;
104
+ }
105
+ );
86
106
  },
87
107
  };
88
108
  }
89
109
 
90
- abstract answeredBy(actor: AnswersQuestions): (actual: Actual) => Promise<ExpectationOutcome<Expected, Actual>>;
91
- }
92
-
93
- /**
94
- * @package
95
- */
96
- class DynamicallyGeneratedExpectation<Expected, Actual> extends Expectation<Expected, Actual> {
97
-
98
- constructor(
99
- private readonly description: string,
100
- private readonly predicate: Predicate<Actual, Expected>,
101
- private readonly expectedValue: Answerable<Expected>,
110
+ protected constructor(
111
+ private subject: string,
112
+ private readonly predicate: Predicate<Actual>,
102
113
  ) {
103
- super(`${ description } ${ formatted `${ expectedValue }` }`);
104
114
  }
105
115
 
106
- answeredBy(actor: AnswersQuestions): (actual: Actual) => Promise<ExpectationOutcome<Expected, Actual>> {
107
-
108
- return (actual: Actual) => actor.answer(this.expectedValue)
109
- .then(expected => {
110
- return this.predicate(actual, expected)
111
- ? new ExpectationMet(this.toString(), expected, actual)
112
- : new ExpectationNotMet(this.toString(), expected, actual);
113
- });
116
+ isMetFor(actual: Answerable<Actual>): QuestionAdapter<ExpectationOutcome<unknown, Actual>> {
117
+ return Question.about(this.subject, actor => this.predicate(actor, actual));
114
118
  }
115
- }
116
-
117
- /**
118
- * @package
119
- */
120
- class ExpectationAlias<Actual> extends Expectation<any, Actual> {
121
119
 
122
- constructor(
123
- subject: string,
124
- private readonly expectation: Expectation<any, Actual>,
125
- ) {
126
- super(subject);
120
+ describedAs(subject: string): this {
121
+ this.subject = subject;
122
+ return this;
127
123
  }
128
124
 
129
- answeredBy(actor: AnswersQuestions): (actual: Actual) => Promise<ExpectationOutcome<any, Actual>> {
130
-
131
- return (actual: Actual) =>
132
- this.expectation.answeredBy(actor)(actual).then(_ => _ instanceof ExpectationMet
133
- ? new ExpectationMet(this.subject, _.expected, _.actual)
134
- : new ExpectationNotMet(_.message, _.expected, _.actual));
125
+ toString(): string {
126
+ return this.subject;
135
127
  }
136
128
  }