@serenity-js/assertions 3.0.0-rc.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +83 -1883
  2. package/README.md +21 -21
  3. package/lib/Ensure.d.ts +75 -84
  4. package/lib/Ensure.d.ts.map +1 -0
  5. package/lib/Ensure.js +103 -123
  6. package/lib/Ensure.js.map +1 -1
  7. package/lib/EnsureEventually.d.ts +97 -0
  8. package/lib/EnsureEventually.d.ts.map +1 -0
  9. package/lib/EnsureEventually.js +143 -0
  10. package/lib/EnsureEventually.js.map +1 -0
  11. package/lib/expectations/and.d.ts +22 -1
  12. package/lib/expectations/and.d.ts.map +1 -0
  13. package/lib/expectations/and.js +30 -5
  14. package/lib/expectations/and.js.map +1 -1
  15. package/lib/expectations/contain.d.ts +27 -2
  16. package/lib/expectations/contain.d.ts.map +1 -0
  17. package/lib/expectations/contain.js +25 -5
  18. package/lib/expectations/contain.js.map +1 -1
  19. package/lib/expectations/containAtLeastOneItemThat.d.ts +23 -1
  20. package/lib/expectations/containAtLeastOneItemThat.d.ts.map +1 -0
  21. package/lib/expectations/containAtLeastOneItemThat.js +29 -7
  22. package/lib/expectations/containAtLeastOneItemThat.js.map +1 -1
  23. package/lib/expectations/containItemsWhereEachItem.d.ts +22 -0
  24. package/lib/expectations/containItemsWhereEachItem.d.ts.map +1 -0
  25. package/lib/expectations/containItemsWhereEachItem.js +29 -7
  26. package/lib/expectations/containItemsWhereEachItem.js.map +1 -1
  27. package/lib/expectations/endsWith.d.ts +22 -2
  28. package/lib/expectations/endsWith.d.ts.map +1 -0
  29. package/lib/expectations/endsWith.js +20 -5
  30. package/lib/expectations/endsWith.js.map +1 -1
  31. package/lib/expectations/equals.d.ts +28 -2
  32. package/lib/expectations/equals.d.ts.map +1 -0
  33. package/lib/expectations/equals.js +26 -5
  34. package/lib/expectations/equals.js.map +1 -1
  35. package/lib/expectations/includes.d.ts +22 -2
  36. package/lib/expectations/includes.d.ts.map +1 -0
  37. package/lib/expectations/includes.js +20 -5
  38. package/lib/expectations/includes.js.map +1 -1
  39. package/lib/expectations/index.d.ts +3 -0
  40. package/lib/expectations/index.d.ts.map +1 -0
  41. package/lib/expectations/index.js +7 -1
  42. package/lib/expectations/index.js.map +1 -1
  43. package/lib/expectations/isAfter.d.ts +42 -2
  44. package/lib/expectations/isAfter.d.ts.map +1 -0
  45. package/lib/expectations/isAfter.js +40 -5
  46. package/lib/expectations/isAfter.js.map +1 -1
  47. package/lib/expectations/isBefore.d.ts +42 -2
  48. package/lib/expectations/isBefore.d.ts.map +1 -0
  49. package/lib/expectations/isBefore.js +40 -5
  50. package/lib/expectations/isBefore.js.map +1 -1
  51. package/lib/expectations/isCloseTo.d.ts +24 -0
  52. package/lib/expectations/isCloseTo.d.ts.map +1 -0
  53. package/lib/expectations/isCloseTo.js +37 -0
  54. package/lib/expectations/isCloseTo.js.map +1 -0
  55. package/lib/expectations/isFalse.d.ts +19 -0
  56. package/lib/expectations/isFalse.d.ts.map +1 -0
  57. package/lib/expectations/isFalse.js +18 -0
  58. package/lib/expectations/isFalse.js.map +1 -1
  59. package/lib/expectations/isGreaterThan.d.ts +45 -2
  60. package/lib/expectations/isGreaterThan.d.ts.map +1 -0
  61. package/lib/expectations/isGreaterThan.js +43 -5
  62. package/lib/expectations/isGreaterThan.js.map +1 -1
  63. package/lib/expectations/isLessThan.d.ts +45 -2
  64. package/lib/expectations/isLessThan.d.ts.map +1 -0
  65. package/lib/expectations/isLessThan.js +43 -5
  66. package/lib/expectations/isLessThan.js.map +1 -1
  67. package/lib/expectations/isPresent.d.ts +58 -8
  68. package/lib/expectations/isPresent.d.ts.map +1 -0
  69. package/lib/expectations/isPresent.js +60 -11
  70. package/lib/expectations/isPresent.js.map +1 -1
  71. package/lib/expectations/isTrue.d.ts +19 -0
  72. package/lib/expectations/isTrue.d.ts.map +1 -0
  73. package/lib/expectations/isTrue.js +18 -0
  74. package/lib/expectations/isTrue.js.map +1 -1
  75. package/lib/expectations/matches.d.ts +22 -2
  76. package/lib/expectations/matches.d.ts.map +1 -0
  77. package/lib/expectations/matches.js +20 -5
  78. package/lib/expectations/matches.js.map +1 -1
  79. package/lib/expectations/not.d.ts +23 -1
  80. package/lib/expectations/not.d.ts.map +1 -0
  81. package/lib/expectations/not.js +32 -10
  82. package/lib/expectations/not.js.map +1 -1
  83. package/lib/expectations/or.d.ts +22 -1
  84. package/lib/expectations/or.d.ts.map +1 -0
  85. package/lib/expectations/or.js +29 -9
  86. package/lib/expectations/or.js.map +1 -1
  87. package/lib/expectations/property.d.ts +62 -0
  88. package/lib/expectations/property.d.ts.map +1 -0
  89. package/lib/expectations/property.js +85 -0
  90. package/lib/expectations/property.js.map +1 -0
  91. package/lib/expectations/startsWith.d.ts +22 -2
  92. package/lib/expectations/startsWith.d.ts.map +1 -0
  93. package/lib/expectations/startsWith.js +20 -5
  94. package/lib/expectations/startsWith.js.map +1 -1
  95. package/lib/index.d.ts +2 -1
  96. package/lib/index.d.ts.map +1 -0
  97. package/lib/index.js +6 -5
  98. package/lib/index.js.map +1 -1
  99. package/package.json +19 -42
  100. package/src/Ensure.ts +110 -136
  101. package/src/EnsureEventually.ts +173 -0
  102. package/src/expectations/and.ts +38 -14
  103. package/src/expectations/contain.ts +30 -5
  104. package/src/expectations/containAtLeastOneItemThat.ts +40 -10
  105. package/src/expectations/containItemsWhereEachItem.ts +35 -5
  106. package/src/expectations/endsWith.ts +25 -5
  107. package/src/expectations/equals.ts +31 -5
  108. package/src/expectations/includes.ts +25 -5
  109. package/src/expectations/index.ts +2 -0
  110. package/src/expectations/isAfter.ts +45 -5
  111. package/src/expectations/isBefore.ts +45 -5
  112. package/src/expectations/isCloseTo.ts +40 -0
  113. package/src/expectations/isFalse.ts +19 -1
  114. package/src/expectations/isGreaterThan.ts +48 -5
  115. package/src/expectations/isLessThan.ts +48 -5
  116. package/src/expectations/isPresent.ts +62 -13
  117. package/src/expectations/isTrue.ts +19 -1
  118. package/src/expectations/matches.ts +25 -5
  119. package/src/expectations/not.ts +28 -5
  120. package/src/expectations/or.ts +28 -6
  121. package/src/expectations/property.ts +93 -0
  122. package/src/expectations/startsWith.ts +25 -5
  123. package/src/index.ts +1 -1
  124. package/tsconfig.build.json +10 -0
  125. package/tsconfig.eslint.json +0 -10
package/src/Ensure.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Activity,
2
3
  Answerable,
3
4
  AnswersQuestions,
4
5
  AssertionError,
@@ -7,192 +8,165 @@ import {
7
8
  Expectation,
8
9
  ExpectationMet,
9
10
  ExpectationNotMet,
10
- ExpectationOutcome,
11
11
  f,
12
12
  Interaction,
13
13
  LogicError,
14
+ RaiseErrors,
14
15
  RuntimeError,
15
16
  UsesAbilities,
16
17
  } from '@serenity-js/core';
17
- import { inspected } from '@serenity-js/core/lib/io/inspected';
18
- import { Artifact, AssertionReport, Name } from '@serenity-js/core/lib/model';
19
- import { match } from 'tiny-types';
18
+ import { FileSystemLocation } from '@serenity-js/core/lib/io';
19
+
20
+ import { EnsureEventually } from './EnsureEventually';
20
21
 
21
22
  /**
22
- * @desc
23
- * Used to perform verification of the system under test.
24
- *
25
- * Resolves any `Answerable` describing the actual
26
- * state and ensures that its value meets the {@link @serenity-js/core/lib/screenplay/questions~Expectation}s provided.
23
+ * The {@apilink Interaction|interaction} to `Ensure`
24
+ * verifies if the resolved value of the provided {@apilink Answerable}
25
+ * meets the specified {@apilink Expectation}.
26
+ * If not, it throws an {@apilink AssertionError}.
27
27
  *
28
- * @example <caption>Usage with static values</caption>
29
- * import { actorCalled } from '@serenity-js/core';
30
- * import { Ensure, equals } from '@serenity-js/assertions';
28
+ * Use `Ensure` to verify the state of the system under test.
31
29
  *
32
- * const actor = actorCalled('Erica');
30
+ * ## Basic usage with static values
31
+ * ```ts
32
+ * import { actorCalled } from '@serenity-js/core'
33
+ * import { Ensure, equals } from '@serenity-js/assertions'
33
34
  *
34
- * actor.attemptsTo(
35
- * Ensure.that('Hello world!', equals('Hello world!'))
36
- * );
35
+ * await actorCalled('Erica').attemptsTo(
36
+ * Ensure.that('Hello world!', equals('Hello world!'))
37
+ * )
38
+ * ```
37
39
  *
38
- * @example <caption>Composing expectations with `and`</caption>
39
- * import { actorCalled } from '@serenity-js/core';
40
- * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions';
40
+ * ## Composing expectations with `and`
41
41
  *
42
- * const actor = actorCalled('Erica');
42
+ * ```ts
43
+ * import { actorCalled } from '@serenity-js/core'
44
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
43
45
  *
44
- * actor.attemptsTo(
45
- * Ensure.that('Hello world!', and(startsWith('Hello'), endsWith('!'))
46
- * );
46
+ * await actorCalled('Erica').attemptsTo(
47
+ * Ensure.that('Hello world!', and(startsWith('Hello'), endsWith('!'))
48
+ * )
49
+ * ```
47
50
  *
48
- * @example <caption>Overriding the type of Error thrown upon assertion failure</caption>
49
- * import { actorCalled, TestCompromisedError } from '@serenity-js/core';
50
- * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions';
51
- * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest';
51
+ * ## Overriding the type of Error thrown upon assertion failure
52
52
  *
53
- * const actor = actorCalled('Erica')
54
- * .whoCan(CallAnApi.at('https://example.com'));
53
+ * ```ts
54
+ * import { actorCalled, TestCompromisedError } from '@serenity-js/core'
55
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
56
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
55
57
  *
56
- * actor.attemptsTo(
57
- * Send.a(GetRequest.to('/api/health')),
58
- * Ensure.that(LastResponse.status(), equals(200))
59
- * .otherwiseFailWith(TestCompromisedError, 'The server is down, please cheer it up!')
60
- * );
58
+ * await actorCalled('Erica')
59
+ * .whoCan(CallAnApi.at('https://example.com'))
60
+ * .attemptsTo(
61
+ * Send.a(GetRequest.to('/api/health')),
62
+ * Ensure.that(LastResponse.status(), equals(200))
63
+ * .otherwiseFailWith(TestCompromisedError, 'The server is down, please cheer it up!')
64
+ * )
65
+ * ```
61
66
  *
62
- * @extends {@serenity-js/core/lib/screenplay~Interaction}
67
+ * @group Activities
63
68
  */
64
69
  export class Ensure<Actual> extends Interaction {
70
+
65
71
  /**
72
+ * Creates an {@apilink Interaction|interaction} to `Ensure`, which
73
+ * verifies if the resolved value of the provided {@apilink Answerable}
74
+ * meets the specified {@apilink Expectation}.
75
+ * If not, it immediately throws an {@apilink AssertionError}.
66
76
  *
67
- * @param {@serenity-js/core/lib/screenplay~Answerable<T>} actual
68
- * @param {@serenity-js/core/lib/screenplay/questions~Expectation<any, A>} expectation
77
+ * @param {Answerable<Actual_Type>} actual
78
+ * An {@apilink Answerable} describing the actual state of the system.
69
79
  *
70
- * @returns {Ensure<A>}
80
+ * @param {Expectation<Actual_Type>} expectation
81
+ * An {@apilink Expectation} you expect the `actual` value to meet
82
+ *
83
+ * @returns {Ensure<Actual_Type>}
71
84
  */
72
- static that<A>(actual: Answerable<A>, expectation: Expectation<A>): Ensure<A> {
73
- return new Ensure(actual, expectation);
85
+ static that<Actual_Type>(actual: Answerable<Actual_Type>, expectation: Expectation<Actual_Type>): Ensure<Actual_Type> {
86
+ return new Ensure(actual, expectation, Activity.callerLocation(5));
74
87
  }
75
88
 
76
89
  /**
77
- * @param {@serenity-js/core/lib/screenplay~Answerable<T>} actual
78
- * @param {@serenity-js/core/lib/screenplay/questions~Expectation<T>} expectation
90
+ * Creates an {@apilink Interaction|interaction} to {@apilink EnsureEventually}, which
91
+ * verifies if the resolved value of the provided {@apilink Answerable}
92
+ * meets the specified {@apilink Expectation} within the expected timeframe.
93
+ *
94
+ * If the expectation is not met by the time the timeout expires, the interaction throws an {@apilink AssertionError}.
95
+ * `EnsureEventually` ignores retries the evaluation if resolving the `actual` results in an {@apilink OptionalNotPresentError},
96
+ * but rethrows any other errors.
97
+ *
98
+ * @param {Answerable<Actual_Type>} actual
99
+ * An {@apilink Answerable} describing the actual state of the system.
100
+ *
101
+ * @param {Expectation<Actual_Type>} expectation
102
+ * An {@apilink Expectation} you expect the `actual` value to meet
103
+ *
104
+ * @returns {Ensure<Actual_Type>}
79
105
  */
80
- constructor(
106
+ static eventually<Actual_Type>(actual: Answerable<Actual_Type>, expectation: Expectation<Actual_Type>): EnsureEventually<Actual_Type> {
107
+ return new EnsureEventually(actual, expectation, Activity.callerLocation(5));
108
+ }
109
+
110
+ /**
111
+ * @param actual
112
+ * @param expectation
113
+ * @param location
114
+ */
115
+ private constructor(
81
116
  protected readonly actual: Answerable<Actual>,
82
117
  protected readonly expectation: Expectation<Actual>,
118
+ location: FileSystemLocation,
83
119
  ) {
84
- super();
120
+ super(d`#actor ensures that ${ actual } does ${ expectation }`, location);
85
121
  }
86
122
 
87
123
  /**
88
- * @desc
89
- * Makes the provided {@link @serenity-js/core/lib/screenplay/actor~Actor}
90
- * perform this {@link @serenity-js/core/lib/screenplay~Interaction}.
91
- *
92
- * @param {UsesAbilities & CollectsArtifacts & AnswersQuestions} actor
93
- * @returns {Promise<void>}
94
- *
95
- * @see {@link @serenity-js/core/lib/screenplay/actor~Actor}
96
- * @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
97
- * @see {@link @serenity-js/core/lib/screenplay/actor~CollectsArtifacts}
98
- * @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
124
+ * @inheritDoc
99
125
  */
100
126
  async performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> {
101
127
  const outcome = await actor.answer(this.expectation.isMetFor(this.actual));
102
128
 
103
- return match<ExpectationOutcome<unknown, Actual>, void>(outcome)
104
- .when(ExpectationNotMet, o => {
105
- actor.collect(this.artifactFrom(o.expected, o.actual), new Name(`Assertion Report`));
129
+ if (outcome instanceof ExpectationNotMet) {
130
+ const actualDescription = d`${ this.actual }`;
131
+ const message = `Expected ${ actualDescription } to ${ outcome.message }`;
106
132
 
107
- throw this.errorForOutcome(o);
108
- })
109
- .when(ExpectationMet, _ => void 0)
110
- .else(o => {
111
- throw new LogicError(f`Expectation#isMetFor(actual) should return an instance of an ExpectationOutcome, not ${ o }`);
133
+ throw RaiseErrors.as(actor).create(AssertionError, {
134
+ message,
135
+ expectation: outcome.expectation,
136
+ diff: { expected: outcome.expected, actual: outcome.actual },
137
+ location: this.instantiationLocation(),
112
138
  });
113
- }
139
+ }
114
140
 
115
- /**
116
- * @desc
117
- * Generates a description to be used when reporting this {@link @serenity-js/core/lib/screenplay~Activity}.
118
- *
119
- * @returns {string}
120
- */
121
- toString(): string {
122
- return d`#actor ensures that ${ this.actual } does ${ this.expectation }`;
141
+ if (! (outcome instanceof ExpectationMet)) {
142
+ throw new LogicError(f`Expectation#isMetFor(actual) should return an instance of an ExpectationOutcome, not ${ outcome }`);
143
+ }
123
144
  }
124
145
 
125
146
  /**
126
- * @desc
127
- * Overrides the default {@link @serenity-js/core/lib/errors~AssertionError} thrown when
128
- * the actual value does not meet the expectations set.
147
+ * Overrides the default {@apilink AssertionError} thrown when
148
+ * the actual value does not meet the expectation.
129
149
  *
130
- * @param {Function} typeOfRuntimeError
131
- * The type of RuntimeError to throw, i.e. TestCompromisedError
150
+ * @param typeOfRuntimeError
151
+ * A constructor function producing a subtype of {@apilink RuntimeError} to throw, e.g. {@apilink TestCompromisedError}
132
152
  *
133
- * @param {string} message
153
+ * @param message
134
154
  * The message explaining the failure
135
- *
136
- * @returns {@serenity-js/core/lib/screenplay~Interaction}
137
155
  */
138
156
  otherwiseFailWith(typeOfRuntimeError: new (message: string, cause?: Error) => RuntimeError, message?: string): Interaction {
139
- return new EnsureOrFailWithCustomError(this.actual, this.expectation, typeOfRuntimeError, message);
140
- }
157
+ const location = this.instantiationLocation();
141
158
 
142
- /**
143
- * @desc
144
- * Maps an {@link @serenity-js/core/lib/screenplay/questions/expectations~ExpectationOutcome} to appropriate {@link @serenity-js/core/lib/errors~RuntimeError}.
145
- *
146
- * @param {@serenity-js/core/lib/screenplay/questions/expectations~ExpectationOutcome} outcome
147
- * @returns {@serenity-js/core/lib/errors~RuntimeError}
148
- *
149
- * @protected
150
- */
151
- protected errorForOutcome(outcome: ExpectationOutcome<any, Actual>): RuntimeError {
152
- return this.asAssertionError(outcome);
153
- }
154
-
155
- /**
156
- * @desc
157
- * Maps an {@link Outcome} to {@link @serenity-js/core/lib/errors~AssertionError}.
158
- *
159
- * @param {Outcome} outcome
160
- * @returns {@serenity-js/core/lib/errors~AssertionError}
161
- *
162
- * @protected
163
- */
164
- protected asAssertionError(outcome: ExpectationOutcome<any, Actual>): AssertionError {
165
- return new AssertionError(
166
- `Expected ${ d`${ this.actual }` } to ${ outcome.message }`,
167
- outcome.expected,
168
- outcome.actual,
169
- );
170
- }
171
-
172
- private artifactFrom(expected: any, actual: Actual): Artifact {
173
- return AssertionReport.fromJSON({
174
- expected: inspected(expected),
175
- actual: inspected(actual),
159
+ return Interaction.where(this.toString(), async actor => {
160
+ try {
161
+ await this.performAs(actor);
162
+ }
163
+ catch (error) {
164
+ throw RaiseErrors.as(actor).create(typeOfRuntimeError, {
165
+ message: message ?? error.message,
166
+ location,
167
+ cause: error
168
+ });
169
+ }
176
170
  });
177
171
  }
178
172
  }
179
-
180
- /**
181
- * @package
182
- */
183
- class EnsureOrFailWithCustomError<Actual> extends Ensure<Actual> {
184
- constructor(
185
- actual: Answerable<Actual>,
186
- expectation: Expectation<Actual>,
187
- private readonly typeOfRuntimeError: new (message: string, cause?: Error) => RuntimeError,
188
- private readonly message?: string,
189
- ) {
190
- super(actual, expectation);
191
- }
192
-
193
- protected errorForOutcome(outcome: ExpectationOutcome<any, Actual>): RuntimeError {
194
- const assertionError = this.asAssertionError(outcome);
195
-
196
- return new this.typeOfRuntimeError(this.message || assertionError.message, assertionError);
197
- }
198
- }
@@ -0,0 +1,173 @@
1
+ import {
2
+ Answerable,
3
+ AnswersQuestions,
4
+ AssertionError,
5
+ CollectsArtifacts,
6
+ d,
7
+ Duration,
8
+ Expectation,
9
+ ExpectationMet,
10
+ ExpectationOutcome,
11
+ Interaction,
12
+ ListItemNotFoundError,
13
+ RaiseErrors,
14
+ RuntimeError,
15
+ ScheduleWork,
16
+ TimeoutExpiredError,
17
+ UsesAbilities,
18
+ } from '@serenity-js/core';
19
+ import { FileSystemLocation } from '@serenity-js/core/lib/io';
20
+
21
+ /**
22
+ * The {@apilink Interaction|interaction} to `EnsureEventually`
23
+ * verifies if the resolved value of the provided {@apilink Answerable}
24
+ * meets the specified {@apilink Expectation} within the expected timeframe.
25
+ *
26
+ * If the expectation is not met by the time the timeout expires, the interaction throws an {@apilink AssertionError}.
27
+ * `EnsureEventually` retries the evaluation if resolving the `actual` results in an {@apilink ListItemNotFoundError},
28
+ * but rethrows any other errors.
29
+ *
30
+ * :::tip Use the factory method
31
+ * Use the factory method {@apilink Ensure.eventually} to instantiate this interaction.
32
+ * :::
33
+ *
34
+ * ## Basic usage with dynamic values
35
+ * ```ts
36
+ * import { actorCalled } from '@serenity-js/core'
37
+ * import { Ensure, equals } from '@serenity-js/assertions'
38
+ * import { Text, PageElement, By } from '@serenity-js/web'
39
+ *
40
+ * await actorCalled('Erica').attemptsTo(
41
+ * Ensure.eventually(
42
+ * Text.of(PageElement.located(By.css('h1'))),
43
+ * equals('Learn Serenity/JS!')
44
+ * )
45
+ * )
46
+ * ```
47
+ *
48
+ * ## Composing expectations with `and`
49
+ *
50
+ * ```ts
51
+ * import { actorCalled } from '@serenity-js/core'
52
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
53
+ * import { Text, PageElement, By } from '@serenity-js/web'
54
+ *
55
+ * await actorCalled('Erica').attemptsTo(
56
+ * Ensure.eventually(
57
+ * Text.of(PageElement.located(By.css('h1'))),
58
+ * and(startsWith('Serenity'), endsWith('!'))
59
+ * )
60
+ * )
61
+ * ```
62
+ *
63
+ * ## Overriding the type of Error thrown upon assertion failure
64
+ *
65
+ * ```ts
66
+ * import { actorCalled } from '@serenity-js/core'
67
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
68
+ * import { Text, PageElement, By } from '@serenity-js/web'
69
+ *
70
+ * await actorCalled('Erica').attemptsTo(
71
+ * Ensure.eventually(
72
+ * Text.of(PageElement.located(By.css('h1'))),
73
+ * and(startsWith('Serenity'), endsWith('!'))
74
+ * ).otherwiseFailWith(LogicError, `Looks like we're not on the right page`)
75
+ * )
76
+ * ```
77
+ *
78
+ * @experimental
79
+ *
80
+ * @group Activities
81
+ */
82
+ export class EnsureEventually<Actual> extends Interaction {
83
+ /**
84
+ * @param actual
85
+ * @param expectation
86
+ * @param location
87
+ * @param timeout
88
+ */
89
+ constructor(
90
+ protected readonly actual: Answerable<Actual>,
91
+ protected readonly expectation: Expectation<Actual>,
92
+ location: FileSystemLocation,
93
+ protected readonly timeout?: Duration,
94
+ ) {
95
+ super(d`#actor ensures that ${ actual } does eventually ${ expectation }`, location);
96
+ }
97
+
98
+ /**
99
+ * Override the default timeout set via {@apilink SerenityConfig.interactionTimeout}.
100
+ *
101
+ * @param timeout
102
+ */
103
+ timeoutAfter(timeout: Duration): EnsureEventually<Actual> {
104
+ return new EnsureEventually<Actual>(this.actual, this.expectation, this.instantiationLocation(), timeout);
105
+ }
106
+
107
+ /**
108
+ * @inheritDoc
109
+ */
110
+ async performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> {
111
+ await ScheduleWork.as(actor).repeatUntil<ExpectationOutcome>(
112
+ () => actor.answer(this.expectation.isMetFor(this.actual)),
113
+ {
114
+ exitCondition: outcome =>
115
+ outcome instanceof ExpectationMet,
116
+
117
+ delayBetweenInvocations: (invocation) => invocation === 0
118
+ ? Duration.ofMilliseconds(0) // perform the first evaluation straight away
119
+ : Duration.ofMilliseconds(2 ** invocation * 100), // use simple exponential backoff strategy for subsequent calls
120
+
121
+ timeout: this.timeout,
122
+
123
+ errorHandler: (error, outcome) => {
124
+ if (error instanceof ListItemNotFoundError) {
125
+ return; // ignore, lists might get populated later
126
+ }
127
+
128
+ if (error instanceof TimeoutExpiredError) {
129
+
130
+ const actualDescription = d`${ this.actual }`;
131
+ const message = outcome ? `Expected ${ actualDescription } to eventually ${ outcome?.message }` : error.message;
132
+
133
+ throw RaiseErrors.as(actor).create(AssertionError, {
134
+ message,
135
+ expectation: outcome?.expectation,
136
+ diff: outcome && { expected: outcome?.expected, actual: outcome?.actual },
137
+ location: this.instantiationLocation(),
138
+ cause: error,
139
+ });
140
+ }
141
+
142
+ throw error;
143
+ },
144
+ },
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Overrides the default {@apilink AssertionError} thrown when
150
+ * the actual value does not meet the expectation.
151
+ *
152
+ * @param typeOfRuntimeError
153
+ * A constructor function producing a subtype of {@apilink RuntimeError} to throw, e.g. {@apilink TestCompromisedError}
154
+ *
155
+ * @param message
156
+ * The message explaining the failure
157
+ */
158
+ otherwiseFailWith(typeOfRuntimeError: new (message: string, cause?: Error) => RuntimeError, message?: string): Interaction {
159
+ const location = this.instantiationLocation();
160
+
161
+ return Interaction.where(this.toString(), async actor => {
162
+ try {
163
+ await this.performAs(actor);
164
+ } catch (error) {
165
+ throw RaiseErrors.as(actor).create(typeOfRuntimeError, {
166
+ message: message ?? error.message,
167
+ location,
168
+ cause: error,
169
+ });
170
+ }
171
+ });
172
+ }
173
+ }
@@ -1,7 +1,26 @@
1
- import { Answerable, AnswersQuestions, Expectation, ExpectationNotMet } from '@serenity-js/core';
2
- import { match } from 'tiny-types';
1
+ import { Answerable, AnswersQuestions, Expectation, ExpectationMet, ExpectationNotMet, ExpectationOutcome } from '@serenity-js/core';
3
2
 
4
- export function and<Actual>(...expectations: Array<Expectation<Actual>>): Expectation<Actual> {
3
+ /**
4
+ * Creates an {@apilink Expectation|expectation} that is met when all the `expectations` are met for the given actual value.
5
+ *
6
+ * Use `and` to combine several expectations using logical "and",
7
+ *
8
+ * ## Combining several expectations
9
+ *
10
+ * ```ts
11
+ * import { actorCalled } from '@serenity-js/core'
12
+ * import { Ensure, and, startsWith, endsWith } from '@serenity-js/assertions'
13
+ *
14
+ * await actorCalled('Ester').attemptsTo(
15
+ * Ensure.that('Hello World!', and(startsWith('Hello'), endsWith('!'))),
16
+ * )
17
+ * ```
18
+ *
19
+ * @param expectations
20
+ *
21
+ * @group Expectations
22
+ */
23
+ export function and<Actual_Type>(...expectations: Array<Expectation<Actual_Type>>): Expectation<Actual_Type> {
5
24
  return new And(expectations);
6
25
  }
7
26
 
@@ -12,18 +31,23 @@ class And<Actual> extends Expectation<Actual> {
12
31
  private static readonly Separator = ' and ';
13
32
 
14
33
  constructor(private readonly expectations: Array<Expectation<Actual>>) {
34
+ const description = expectations.map(expectation => expectation.toString()).join(And.Separator);
35
+
15
36
  super(
16
- expectations.map(expectation => expectation.toString()).join(And.Separator),
17
- (actor: AnswersQuestions, actual: Answerable<Actual>) => {
18
- return expectations.reduce(
19
- (previous, current) =>
20
- previous.then(outcome =>
21
- match(outcome)
22
- .when(ExpectationNotMet, o => o)
23
- .else(_ => actor.answer(current.isMetFor(actual))),
24
- ),
25
- Promise.resolve(void 0),
26
- );
37
+ 'and',
38
+ description,
39
+ async (actor: AnswersQuestions, actual: Answerable<Actual>) => {
40
+ let outcome: ExpectationOutcome;
41
+
42
+ for (const expectation of expectations) {
43
+ outcome = await actor.answer(expectation.isMetFor(actual))
44
+
45
+ if (outcome instanceof ExpectationNotMet) {
46
+ return new ExpectationNotMet(description, outcome.expectation, outcome.expected, outcome.actual);
47
+ }
48
+ }
49
+
50
+ return new ExpectationMet(description, outcome?.expectation, outcome?.expected, outcome?.actual);
27
51
  }
28
52
  );
29
53
  }
@@ -1,7 +1,32 @@
1
- import { Answerable, Expectation } from '@serenity-js/core';
1
+ import { Expectation } from '@serenity-js/core';
2
2
  import { equal } from 'tiny-types/lib/objects';
3
3
 
4
- export function contain<Item>(expected: Answerable<Item>): Expectation<Item[]> {
5
- return Expectation.thatActualShould<Item, Item[]>('contain', expected)
6
- .soThat((actualValue, expectedValue) => !! ~ actualValue.findIndex(av => equal(av, expectedValue)));
7
- }
4
+ /**
5
+ * Produces an {@apilink Expectation|expectation} that is met when the actual array of `Item[]` contains
6
+ * at least one `Item` that is equal to the resolved value of `expected`.
7
+ *
8
+ * Note that the equality check performs comparison **by value**
9
+ * using [TinyTypes `equal`](https://github.com/jan-molak/tiny-types/blob/master/src/objects/equal.ts).
10
+ *
11
+ * ## Ensuring that the array contains the given item
12
+ *
13
+ * ```ts
14
+ * import { actorCalled } from '@serenity-js/core'
15
+ * import { Ensure, and, startsWith, endsWith } from '@serenity-js/assertions'
16
+ *
17
+ * const items = [ { name: 'apples' }, { name: 'bananas' } ]
18
+ *
19
+ * await actorCalled('Ester').attemptsTo(
20
+ * Ensure.that(items, contain({ name: 'bananas' })),
21
+ * )
22
+ * ```
23
+ *
24
+ * @param expected
25
+ *
26
+ * @group Expectations
27
+ */
28
+ export const contain = Expectation.define(
29
+ 'contain', 'contain',
30
+ <Item>(actual: Item[], expected: Item) =>
31
+ actual.some(item => equal(item, expected))
32
+ );
@@ -1,34 +1,58 @@
1
- import { Answerable, AnswersQuestions, d, Expectation, ExpectationMet, ExpectationNotMet, ExpectationOutcome } from '@serenity-js/core';
1
+ import { Answerable, AnswersQuestions, d, Expectation, ExpectationDetails, ExpectationMet, ExpectationNotMet, ExpectationOutcome, Unanswered } from '@serenity-js/core';
2
2
 
3
- export function containAtLeastOneItemThat<Actual>(expectation: Expectation<Actual>): Expectation<Actual[]> {
3
+ /**
4
+ * Produces an {@apilink Expectation|expectation} that is met when the actual array of `Item[]` contains
5
+ * at least one `Item` for which the `expectation` is met.
6
+ *
7
+ * ## Ensuring that at least one item in an array meets the expectation
8
+ *
9
+ * ```ts
10
+ * import { actorCalled } from '@serenity-js/core'
11
+ * import { Ensure, containAtLeastOneItemThat, isGreaterThan } from '@serenity-js/assertions'
12
+ *
13
+ * const items = [ 10, 15, 20 ]
14
+ *
15
+ * await actorCalled('Ester').attemptsTo(
16
+ * Ensure.that(items, containAtLeastOneItemThat(isGreaterThan(18))),
17
+ * )
18
+ * ```
19
+ *
20
+ * @param expectation
21
+ *
22
+ * @group Expectations
23
+ */
24
+ export function containAtLeastOneItemThat<Item>(expectation: Expectation<Item>): Expectation<Item[]> {
4
25
  return new ContainAtLeastOneItemThatMeetsExpectation(expectation);
5
26
  }
6
27
 
7
28
  /**
8
29
  * @package
9
30
  */
10
- class ContainAtLeastOneItemThatMeetsExpectation<Actual> extends Expectation<Actual[]> {
31
+ class ContainAtLeastOneItemThatMeetsExpectation<Item> extends Expectation<Item[]> {
11
32
 
12
33
  private static descriptionFor(expectation: Expectation<any>) {
13
34
  return d`contain at least one item that does ${ expectation }`;
14
35
  }
15
36
 
16
- constructor(private readonly expectation: Expectation<Actual>) {
37
+ constructor(private readonly expectation: Expectation<Item>) {
17
38
  super(
39
+ 'containAtLeastOneItemThat',
18
40
  ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
19
- async (actor: AnswersQuestions, actual: Answerable<Actual[]>) => {
41
+ async (actor: AnswersQuestions, actual: Answerable<Item[]>) => {
20
42
 
21
- const items: Actual[] = await actor.answer(actual);
43
+ const items: Item[] = await actor.answer(actual);
22
44
 
23
45
  if (! items || items.length === 0) {
46
+ const unanswered = new Unanswered();
24
47
  return new ExpectationNotMet(
25
48
  ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
26
- undefined,
49
+ ExpectationDetails.of('containAtLeastOneItemThat', unanswered),
50
+ unanswered,
27
51
  items,
28
52
  );
29
53
  }
30
54
 
31
- let outcome: ExpectationOutcome<unknown, Actual>;
55
+ let outcome: ExpectationOutcome;
32
56
 
33
57
  for (const item of items) {
34
58
 
@@ -37,13 +61,19 @@ class ContainAtLeastOneItemThatMeetsExpectation<Actual> extends Expectation<Actu
37
61
  if (outcome instanceof ExpectationMet) {
38
62
  return new ExpectationMet(
39
63
  ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
64
+ ExpectationDetails.of('containAtLeastOneItemThat', outcome.expectation),
40
65
  outcome.expected,
41
- items
66
+ items,
42
67
  );
43
68
  }
44
69
  }
45
70
 
46
- return new ExpectationNotMet(ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation), outcome.expected, items);
71
+ return new ExpectationNotMet(
72
+ ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
73
+ ExpectationDetails.of('containAtLeastOneItemThat', outcome.expectation),
74
+ outcome.expected,
75
+ items,
76
+ );
47
77
  }
48
78
  );
49
79
  }