@serenity-js/core 3.28.0 → 3.29.2

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.
@@ -0,0 +1,684 @@
1
+ import { ensure, isNumber, isString, type Predicate } from 'tiny-types';
2
+
3
+ import type { UsesAbilities } from '../abilities';
4
+ import type { Answerable } from '../Answerable';
5
+ import type { QuestionAdapter } from '../Question';
6
+ import { Question } from '../Question';
7
+ import type { AnswersQuestions } from './AnswersQuestions';
8
+ import type { MetaQuestion } from './MetaQuestion';
9
+ import { the } from './tag-functions';
10
+
11
+ /**
12
+ * Provides methods to perform calculations on numeric values returned by other [questions](https://serenity-js.org/api/core/class/Question/).
13
+ *
14
+ * ## Example
15
+ *
16
+ * The examples in this section demonstrate interacting with an HTML widget representing a price list.
17
+ *
18
+ * ```html
19
+ * <ul id="price-list">
20
+ * <li class="item">
21
+ * <span class="name">apples</span>
22
+ * <span class="quantity">5</span>
23
+ * <span class="price">£2.25</span>
24
+ * </li>
25
+ * <li class="item">
26
+ * <span class="name">apricots</span>
27
+ * <span class="quantity">4</span>
28
+ * <span class="price">£3.70</span>
29
+ * </li>
30
+ * <li class="item">
31
+ * <span class="name">bananas</span>
32
+ * <span class="quantity">2</span>
33
+ * <span class="price">£1.50</span>
34
+ * </li>
35
+ * </ul>
36
+ * ```
37
+ *
38
+ * ### Lean Page Objects
39
+ *
40
+ * To represent our example HTML widget,
41
+ * we follow the [Lean Page Objects pattern](https://serenity-js.org/handbook/web-testing/page-objects-pattern/)
42
+ * to define the `PriceList` and `PriceListItem` classes
43
+ * and use the Serenity/JS [Page Element Query Language (PEQL)](https://serenity-js.org/handbook/web-testing/page-element-query-language/)
44
+ * to identify the elements of interest.
45
+ *
46
+ * ```ts
47
+ * // ui/price-list.ts
48
+ *
49
+ * import { PageElement, PageElements, By } from '@serenity-js/web'
50
+ *
51
+ * export class PriceList {
52
+ * static container = () =>
53
+ * PageElement.located(By.id('price-list'))
54
+ * .describedAs('price list')
55
+ *
56
+ * static items = () =>
57
+ * PageElements.located(PriceListItem.containerSelector())
58
+ * .of(this.container())
59
+ * .describedAs('items')
60
+ * }
61
+ *
62
+ * export class PriceListItem {
63
+ * static containerSelector = () =>
64
+ * By.css('.item')
65
+ *
66
+ * static container = () =>
67
+ * PageElement.located(this.containerSelector())
68
+ * .describedAs('item')
69
+ *
70
+ * static name = () =>
71
+ * PageElement.located(By.css('.name'))
72
+ * .of(this.container())
73
+ * .describedAs('name')
74
+ *
75
+ * static quantity = () =>
76
+ * PageElement.located(By.css('.quantity'))
77
+ * .of(this.container())
78
+ * .describedAs('quantity')
79
+ *
80
+ * static price = () =>
81
+ * PageElement.located(By.css('.price'))
82
+ * .of(this.container())
83
+ * .describedAs('price')
84
+ * }
85
+ * ```
86
+ *
87
+ * @group Questions
88
+ */
89
+ export class Numeric {
90
+
91
+ /**
92
+ * Returns a [`Question`](https://serenity-js.org/api/core/class/Question/) that sums up the values provided
93
+ * and throws if any of the values is not a `number`.
94
+ *
95
+ * #### Adding static numbers
96
+ *
97
+ * The most basic example of using the `Numeric.sum` method is to add up a few numbers.
98
+ *
99
+ * ```ts
100
+ * import { actorCalled, Numeric } from '@serenity-js/core'
101
+ * import { Ensure, equals } from '@serenity-js/assertions'
102
+ *
103
+ * await actorCalled('Zoé').attemptsTo(
104
+ * Ensure.that(
105
+ * Numeric.sum(1, 2, 3),
106
+ * equals(6),
107
+ * )
108
+ * )
109
+ * ```
110
+ *
111
+ * The numbers can be provided individually, as an array, or as a combination of both.
112
+ *
113
+ * ```ts
114
+ * import { actorCalled, Numeric } from '@serenity-js/core'
115
+ * import { Ensure, equals } from '@serenity-js/assertions'
116
+ *
117
+ * await actorCalled('Zoé').attemptsTo(
118
+ * Ensure.that(
119
+ * Numeric.sum([ 1, 2 ], 3, [ 4, 5 ]),
120
+ * equals(15),
121
+ * )
122
+ * )
123
+ * ```
124
+ *
125
+ * #### Adding numbers returned by other questions
126
+ *
127
+ * Apart from adding static numbers, you can also add numbers returned by other questions.
128
+ * This is particularly useful when you need to calculate the sum of numbers extracted from a list of UI elements
129
+ * or from an API response.
130
+ *
131
+ * ```ts
132
+ * import { actorCalled, Numeric, Question } from '@serenity-js/core'
133
+ * import { Ensure, equals } from '@serenity-js/assertions'
134
+ *
135
+ * const myNumber = () =>
136
+ * Question.about('my number', actor => 42)
137
+ *
138
+ * const myArrayOfNumbers = () =>
139
+ * Question.about('my array of numbers', actor => [ 1, 2, 3 ])
140
+ *
141
+ * const myObjectWithNumbers = () =>
142
+ * Question.about('my object with numbers', actor => ({ a: 1, b: 2, c: 3 }))
143
+ *
144
+ * await actorCalled('Zoé').attemptsTo(
145
+ * Ensure.that(
146
+ * Numeric.sum(
147
+ * myNumber(), // a question returning a number
148
+ * myArrayOfNumbers(), // a question returning an array of numbers
149
+ * ),
150
+ * equals(48),
151
+ * ),
152
+ * Ensure.that(
153
+ * Numeric.sum(
154
+ * myObjectWithNumbers() // a question returning an object with numbers
155
+ * .as(Object.values), // from which we extract the numeric values
156
+ * ),
157
+ * equals(6),
158
+ * ),
159
+ * )
160
+ * ```
161
+ *
162
+ * Of course, you can also mix and match static numbers with numbers returned by questions.
163
+ *
164
+ * ```ts
165
+ * import { actorCalled, Numeric, Question } from '@serenity-js/core'
166
+ * import { Ensure, equals } from '@serenity-js/assertions'
167
+ *
168
+ * const myObjectWithNumbers = () =>
169
+ * Question.about('my object with numbers', actor => ({ a: 1, b: 2, c: 3 }))
170
+ *
171
+ * await actorCalled('Zoé').attemptsTo(
172
+ * Ensure.that(
173
+ * Numeric.sum(
174
+ * myObjectWithNumbers().as(Object.values),
175
+ * [ 4, 5 ],
176
+ * 6,
177
+ * ),
178
+ * equals(21),
179
+ * ),
180
+ * )
181
+ * ```
182
+ *
183
+ * #### Adding numbers extracted from a UI
184
+ *
185
+ * To add numbers extracted from a UI:
186
+ * - use the [`PageElement`](https://serenity-js.org/api/web/class/PageElement) and [`PageElements`](https://serenity-js.org/api/web/class/PageElements) classes to identify the elements of interest,
187
+ * - use the [`Text.of`](https://serenity-js.org/api/web/class/Text/) or [`Text.ofAll`](https://serenity-js.org/api/web/class/Text/) questions to extract the text of the element or elements,
188
+ * - and then interpret this text as number using either the [`.as(Number)`](https://serenity-js.org/api/core/class/Question/#as) mapping function,
189
+ * or the [`Numeric.intValue()`](https://serenity-js.org/api/core/class/Numeric/#intValue) or [`Numeric.floatValue()`](https://serenity-js.org/api/core/class/Numeric/#floatValue) meta-questions.
190
+ *
191
+ * For example, we could calculate the sum of quantities of items in our example price list by specifying each element explicitly
192
+ * and mapping its text to [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number):
193
+ *
194
+ * ```ts
195
+ * import { actorCalled, Numeric } from '@serenity-js/core'
196
+ * import { Ensure, equals } from '@serenity-js/assertions'
197
+ * import { Text } from '@serenity-js/web'
198
+ * import { PriceList, PriceListItem } from './ui/price-list'
199
+ *
200
+ * await actorCalled('Zoé').attemptsTo(
201
+ * Ensure.that(
202
+ * Numeric.sum(
203
+ * Text.of(PriceListItem.quantity().of(PriceList.items().first())).as(Number),
204
+ * Text.of(PriceListItem.quantity().of(PriceList.items().at(1))).as(Number),
205
+ * Text.of(PriceListItem.quantity().of(PriceList.items().last())).as(Number),
206
+ * ),
207
+ * equals(11),
208
+ * ),
209
+ * )
210
+ * ```
211
+ *
212
+ * A more elegant approach would be to use the Serenity/JS [Page Element Query Language (PEQL)](https://serenity-js.org/handbook/web-testing/page-element-query-language/#mapping-page-elements-in-a-collection)
213
+ * to map each item in the collection to its quantity and then calculate the sum.
214
+ *
215
+ * ```ts
216
+ * import { actorCalled, Numeric } from '@serenity-js/core'
217
+ * import { Ensure, equals } from '@serenity-js/assertions'
218
+ * import { Text } from '@serenity-js/web'
219
+ * import { PriceList, PriceListItem } from './ui/price-list'
220
+ *
221
+ * await actorCalled('Zoé').attemptsTo(
222
+ * Ensure.that(
223
+ * Numeric.sum(
224
+ * PriceList.items() // get all the li.item elements
225
+ * .eachMappedTo(
226
+ * Text.of(PriceListItem.quantity()) // extract the text of the .quantity element
227
+ * )
228
+ * .eachMappedTo( // interpret the quantity as an integer value
229
+ * Numeric.intValue(),
230
+ * ),
231
+ * ),
232
+ * equals(11), // 5 apples, 4 apricots, 2 bananas
233
+ * )
234
+ * )
235
+ * ```
236
+ *
237
+ * Using PEQL allows us to express the intent more concisely and, for example,
238
+ * introduce helper functions that limit the scope of the operation to a subset of elements.
239
+ *
240
+ * ```ts
241
+ * import { actorCalled, Expectation, Numeric, the } from '@serenity-js/core'
242
+ * import { Ensure, equals, startsWith } from '@serenity-js/assertions'
243
+ * import { PriceList, PriceListItem } from './ui/price-list'
244
+ *
245
+ * const quantitiesOfItemsWhichName = (expectation: Expectation<string>) =>
246
+ * PriceList.items() // get all the li.item elements
247
+ * .where( // leave only those which name matches the expectation
248
+ * Text.of(PriceListItem.name()),
249
+ * expectation
250
+ * )
251
+ * .eachMappedTo(
252
+ * Text.of(PriceListItem.quantity()) // extract the text of the .quantity element
253
+ * )
254
+ * .eachMappedTo( // interpret the quantity as an integer value
255
+ * Numeric.intValue(),
256
+ * )
257
+ * .describedAs(the`quantities of items which name does ${ expectation }`)
258
+ *
259
+ * await actorCalled('Zoé').attemptsTo(
260
+ * Ensure.that(
261
+ * Numeric.sum(
262
+ * quantitiesOfItemsWhichName(startsWith('ap')), // apples and apricots
263
+ * ),
264
+ * equals(9), // 5 apples, 4 apricots
265
+ * )
266
+ * )
267
+ * ```
268
+ *
269
+ * #### Learn more
270
+ *
271
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
272
+ * and examples.
273
+ *
274
+ * @param values
275
+ */
276
+ static sum(...values: Array<Answerable<number | number[]>>): QuestionAdapter<number> {
277
+ return Question.about<number>(the`the sum of ${ values }`, async actor => {
278
+ const numbers = await actor.answer(this.flatten(values, isNumber()));
279
+
280
+ return numbers.sort()
281
+ .reduce((acc, current) => acc + current, 0);
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Returns a [`Question`](https://serenity-js.org/api/core/class/Question/) that calculates the difference between
287
+ * two numbers and throws if any of the values is not a `number`.
288
+ *
289
+ * #### Subtracting numbers
290
+ *
291
+ * ```ts
292
+ * import { actorCalled, Numeric } from '@serenity-js/core'
293
+ * import { Ensure, equals } from '@serenity-js/assertions'
294
+ * import { Text } from '@serenity-js/web'
295
+ * import { PriceList, PriceListItem } from './ui/price-list'
296
+ *
297
+ * await actorCalled('Zoé').attemptsTo(
298
+ * Ensure.that(
299
+ * Numeric.difference(
300
+ * Text.of(PriceListItem.quantity().of(PriceList.items().first())).as(Number), // 5 (apples)
301
+ * 2, // - 2
302
+ * ),
303
+ * equals(3),
304
+ * ),
305
+ * )
306
+ * ```
307
+ *
308
+ * #### Learn more
309
+ *
310
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
311
+ * and examples.
312
+ *
313
+ * @param minuend
314
+ * @param subtrahend
315
+ */
316
+ static difference(minuend: Answerable<number>, subtrahend: Answerable<number>): QuestionAdapter<number> {
317
+ return Question.about<number>(the`the difference between ${ minuend } and ${ subtrahend }`, async actor => {
318
+ const minuendValue = await actor.answer(minuend);
319
+ const subtrahendValue = await actor.answer(subtrahend);
320
+
321
+ return ensure(this.descriptionOf(minuendValue), minuendValue, isNumber())
322
+ - ensure(this.descriptionOf(subtrahendValue), subtrahendValue, isNumber());
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Returns a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that calculates
328
+ * the ceiling of a number and throws if the value is not a `number`.
329
+ *
330
+ * #### Calculating the ceiling of a number
331
+ *
332
+ * ```ts
333
+ * import { actorCalled, Numeric } from '@serenity-js/core'
334
+ * import { Ensure, equals } from '@serenity-js/assertions'
335
+ * import { Text } from '@serenity-js/web'
336
+ * import { PriceList, PriceListItem } from './ui/price-list'
337
+ *
338
+ * await actorCalled('Zoé').attemptsTo(
339
+ * Ensure.that(
340
+ * Numeric.ceiling().of(
341
+ * Text.of(PriceListItem.price().of(PriceList.items().first())) // '£2.25' (apples)
342
+ * .replace('£', '') // '2.25'
343
+ * .as(Number.parseFloat), // 2.25
344
+ * ),
345
+ * equals(3),
346
+ * ),
347
+ * )
348
+ * ```
349
+ *
350
+ * #### Learn more
351
+ *
352
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
353
+ * and examples.
354
+ */
355
+ static ceiling(): MetaQuestion<number, QuestionAdapter<number>> {
356
+ return {
357
+ of: (value: Answerable<number>) =>
358
+ Question.about(the`the ceiling of ${ value }`, async (actor: AnswersQuestions & UsesAbilities) => {
359
+ const answer = await actor.answer(value);
360
+
361
+ return Math.ceil(ensure(this.descriptionOf(answer), answer, isNumber()));
362
+ }),
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Returns a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that calculates
368
+ * the floor of a number and throws if the value is not a `number`.
369
+ *
370
+ * #### Calculating the floor of a number
371
+ *
372
+ * ```ts
373
+ * import { actorCalled, Numeric } from '@serenity-js/core'
374
+ * import { Ensure, equals } from '@serenity-js/assertions'
375
+ * import { Text } from '@serenity-js/web'
376
+ * import { PriceList, PriceListItem } from './ui/price-list'
377
+ *
378
+ * await actorCalled('Zoé').attemptsTo(
379
+ * Ensure.that(
380
+ * Numeric.floor().of(
381
+ * Text.of(PriceListItem.price().of(PriceList.items().first())) // '£2.25' (apples)
382
+ * .replace('£', '') // '2.25'
383
+ * .as(Number.parseFloat), // 2.25
384
+ * ),
385
+ * equals(2),
386
+ * ),
387
+ * )
388
+ * ```
389
+ *
390
+ * #### Learn more
391
+ *
392
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
393
+ * and examples.
394
+ */
395
+ static floor(): MetaQuestion<number, QuestionAdapter<number>> {
396
+ return {
397
+ of: (value: Answerable<number>) =>
398
+ Question.about(the`the floor of ${ value }`, async (actor: AnswersQuestions & UsesAbilities) => {
399
+ const answer = await actor.answer(value);
400
+
401
+ return Math.floor(ensure(this.descriptionOf(answer), answer, isNumber()));
402
+ }),
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Returns a [`Question`](https://serenity-js.org/api/core/class/Question/) that calculates
408
+ * the maximum value in the lists of numbers provided and throws if any of the values is not a `number`.
409
+ *
410
+ * #### Calculating the maximum value
411
+ *
412
+ * ```ts
413
+ * import { actorCalled, Numeric } from '@serenity-js/core'
414
+ * import { Ensure, equals } from '@serenity-js/assertions'
415
+ * import { Text } from '@serenity-js/web'
416
+ * import { PriceList, PriceListItem } from './ui/price-list'
417
+ *
418
+ * await actorCalled('Zoé').attemptsTo(
419
+ * Ensure.that(
420
+ * Numeric.max(
421
+ * PriceList.items() // get all the li.item elements
422
+ * .eachMappedTo(
423
+ * Text.of(PriceListItem.quantity()) // extract the text of the .quantity element
424
+ * )
425
+ * .eachMappedTo( // interpret the quantity as an integer value
426
+ * Numeric.intValue(),
427
+ * ),
428
+ * ),
429
+ * equals(5), // 5 (apples)
430
+ * )
431
+ * )
432
+ * ```
433
+ *
434
+ * #### Learn more
435
+ *
436
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
437
+ * and examples.
438
+ *
439
+ * @param values
440
+ */
441
+ static max(...values: Array<Answerable<number | number[]>>): QuestionAdapter<number> {
442
+ return Question.about<number>(the`the max of ${ values }`, async actor => {
443
+ const numbers = await actor.answer(this.flatten(values, isNumber()));
444
+
445
+ return numbers.sort().pop();
446
+ });
447
+ }
448
+
449
+ /**
450
+ * Returns a [`Question`](https://serenity-js.org/api/core/class/Question/) that calculates
451
+ * the minimum value in the lists of numbers provided and throws if any of the values is not a `number`.
452
+ *
453
+ * #### Calculating the minimum value
454
+ *
455
+ * ```ts
456
+ * import { actorCalled, Numeric } from '@serenity-js/core'
457
+ * import { Ensure, equals } from '@serenity-js/assertions'
458
+ * import { Text } from '@serenity-js/web'
459
+ * import { PriceList, PriceListItem } from './ui/price-list'
460
+ *
461
+ * await actorCalled('Zoé').attemptsTo(
462
+ * Ensure.that(
463
+ * Numeric.min(
464
+ * PriceList.items() // get all the li.item elements
465
+ * .eachMappedTo(
466
+ * Text.of(PriceListItem.quantity()) // extract the text of the .quantity element
467
+ * )
468
+ * .eachMappedTo( // interpret the quantity as an integer value
469
+ * Numeric.intValue(),
470
+ * ),
471
+ * ),
472
+ * equals(2), // 2 (bananas)
473
+ * )
474
+ * )
475
+ * ```
476
+ *
477
+ * #### Learn more
478
+ *
479
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
480
+ * and examples.
481
+ *
482
+ * @param values
483
+ */
484
+ static min(...values: Array<Answerable<number | number[]>>): QuestionAdapter<number> {
485
+ return Question.about<number>(the`the min of ${ values }`, async actor => {
486
+ const numbers = await actor.answer(this.flatten(values, isNumber()));
487
+
488
+ return numbers.sort().shift();
489
+ });
490
+ }
491
+
492
+ /**
493
+ * Returns a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that parses a string `value`
494
+ * and returns an integer of the specified `base`.
495
+ * Leading whitespace in the value to parse argument is ignored.
496
+ *
497
+ * #### Parsing a string as an integer
498
+ *
499
+ * ```ts
500
+ * import { actorCalled, Numeric } from '@serenity-js/core'
501
+ * import { Ensure, equals } from '@serenity-js/assertions'
502
+ * import { Text } from '@serenity-js/web'
503
+ * import { PriceList, PriceListItem } from './ui/price-list'
504
+ *
505
+ * await actorCalled('Zoé').attemptsTo(
506
+ * Ensure.that(
507
+ * Numeric.intValue().of(
508
+ * Text.of( // '5' (apples)
509
+ * PriceListItem.quantity().of(
510
+ * PriceList.items().first()
511
+ * )
512
+ * )
513
+ * ),
514
+ * equals(5),
515
+ * ),
516
+ * )
517
+ * ```
518
+ *
519
+ * #### Learn more
520
+ *
521
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
522
+ * and examples.
523
+ *
524
+ * @param base
525
+ * An integer between 2 and 36 that represents the base in mathematical numeral systems of the string.
526
+ * If base is undefined or 0, it is assumed to be 10 except when the number begins with the code unit pairs 0x or 0X, in which case a radix of 16 is assumed.
527
+ */
528
+ static intValue(base?: Answerable<number>): MetaQuestion<string, QuestionAdapter<number>> {
529
+ return {
530
+ /**
531
+ * @param value
532
+ * The value to parse, coerced to a string. Leading whitespace in this argument is ignored.
533
+ */
534
+ of: (value: Answerable<string>) =>
535
+ Question.about<Promise<number>>(the`the integer value of ${ value }`,
536
+ async actor => {
537
+ const description = this.descriptionOf(value);
538
+ const stringValue = ensure(description, await actor.answer(value), isString());
539
+ const maybeBase = await actor.answer(base)
540
+
541
+ const radix = maybeBase !== undefined && maybeBase !== null
542
+ ? ensure(`base ${ this.descriptionOf(base) }`, maybeBase, isNumber())
543
+ : undefined;
544
+
545
+ const parsed = Number.parseInt(stringValue, radix);
546
+
547
+ if (Number.isNaN(parsed)) {
548
+ throw new TypeError(`Parsing ${ description } as an integer value returned a NaN`);
549
+ }
550
+
551
+ return parsed;
552
+ }),
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Returns a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that parses a string `value`
558
+ * and returns a [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).
559
+ * Leading whitespace in the value to parse argument is ignored.
560
+ *
561
+ * #### Parsing a string as a bigint
562
+ *
563
+ * ```ts
564
+ * import { actorCalled, Numeric } from '@serenity-js/core'
565
+ * import { Ensure, equals } from '@serenity-js/assertions'
566
+ * import { Text } from '@serenity-js/web'
567
+ * import { PriceList, PriceListItem } from './ui/price-list'
568
+ *
569
+ * await actorCalled('Zoé').attemptsTo(
570
+ * Ensure.that(
571
+ * Numeric.bigIntValue().of(
572
+ * Text.of( // '5' (apples)
573
+ * PriceListItem.quantity().of(PriceList.items().first())
574
+ * )
575
+ * ),
576
+ * equals(BigInt('5')),
577
+ * ),
578
+ * )
579
+ * ```
580
+ *
581
+ * #### Learn more
582
+ *
583
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
584
+ * and examples.
585
+ */
586
+ static bigIntValue(): MetaQuestion<string, QuestionAdapter<bigint>> {
587
+ return {
588
+ /**
589
+ * @param value
590
+ * The value to parse, coerced to a string. Leading whitespace in this argument is ignored.
591
+ */
592
+ of: (value: Answerable<string>) =>
593
+ Question.about<Promise<bigint>>(the`the bigint value of ${ value }`, async actor => {
594
+ const description = this.descriptionOf(value);
595
+ const stringValue = ensure(description, await actor.answer(value), isString());
596
+
597
+ try {
598
+ return BigInt(stringValue);
599
+ }
600
+ catch(error) {
601
+ throw new TypeError(`Parsing ${ description } as a bigint value returned an error: ${ error.message || error }`);
602
+ }
603
+ }),
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Returns a [`MetaQuestion`](https://serenity-js.org/api/core/interface/MetaQuestion/) that parses a string `value`
609
+ * and returns a floating-point number.
610
+ *
611
+ * #### Parsing a string as a floating point number
612
+ *
613
+ * ```ts
614
+ * import { actorCalled, Numeric } from '@serenity-js/core'
615
+ * import { Ensure, equals } from '@serenity-js/assertions'
616
+ * import { Text } from '@serenity-js/web'
617
+ * import { PriceList, PriceListItem } from './ui/price-list'
618
+ *
619
+ * await actorCalled('Zoé').attemptsTo(
620
+ * Ensure.that(
621
+ * Numeric.floatValue().of(
622
+ * Text.of( // '£2.25'
623
+ * PriceListItem.price().of(PriceList.items().first())
624
+ * ).replace('£', '') // '2.25'
625
+ * ),
626
+ * equals(2.25),
627
+ * ),
628
+ * )
629
+ * ```
630
+ *
631
+ * #### Learn more
632
+ *
633
+ * View the [`Numeric` API documentation](https://serenity-js.org/api/core/class/Numeric) for more details
634
+ * and examples.
635
+ */
636
+ static floatValue(): MetaQuestion<string, QuestionAdapter<number>> {
637
+ return {
638
+ /**
639
+ * @param value
640
+ * The value to parse, coerced to a string. Leading whitespace in this argument is ignored.
641
+ */
642
+ of: (value: Answerable<string>) =>
643
+ Question.about<Promise<number>>(the`the float value of ${ value }`, async actor => {
644
+ const description = this.descriptionOf(value);
645
+ const maybeNumber = ensure(description, await actor.answer(value), isString());
646
+
647
+ const parsed = Number.parseFloat(maybeNumber);
648
+
649
+ if (Number.isNaN(parsed)) {
650
+ throw new TypeError(`Parsing ${ description } as a float value returned a NaN`);
651
+ }
652
+
653
+ return parsed;
654
+ }),
655
+ }
656
+ }
657
+
658
+ private static flatten<T>(items: Array<Answerable<T | T[]>>, ...predicates: Array<Predicate<T>>): QuestionAdapter<T[]> {
659
+ return Question.about('flatten', async actor => {
660
+ const result: T[] = [];
661
+
662
+ for (const item of items) {
663
+ const valueOrValues = await actor.answer(item);
664
+ const values = Array.isArray(valueOrValues)
665
+ ? valueOrValues
666
+ : [ valueOrValues ];
667
+
668
+ const valuesOfCorrectType = values.map(value => ensure(this.descriptionOf(value), value, ...predicates));
669
+
670
+ result.push(...valuesOfCorrectType);
671
+ }
672
+
673
+ return result;
674
+ });
675
+ }
676
+
677
+ private static descriptionOf(value: unknown): string {
678
+ if (value === undefined) {
679
+ return 'undefined';
680
+ }
681
+
682
+ return Question.formattedValue().of(value).toString();
683
+ }
684
+ }
@@ -8,5 +8,6 @@ export * from './expectations';
8
8
  export * from './List';
9
9
  export * from './Masked';
10
10
  export * from './MetaQuestion';
11
+ export * from './Numeric';
11
12
  export * from './tag-functions';
12
13
  export * from './Unanswered';