@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.
- package/CHANGELOG.md +83 -1883
- package/README.md +21 -21
- package/lib/Ensure.d.ts +75 -84
- package/lib/Ensure.d.ts.map +1 -0
- package/lib/Ensure.js +103 -123
- package/lib/Ensure.js.map +1 -1
- package/lib/EnsureEventually.d.ts +97 -0
- package/lib/EnsureEventually.d.ts.map +1 -0
- package/lib/EnsureEventually.js +143 -0
- package/lib/EnsureEventually.js.map +1 -0
- package/lib/expectations/and.d.ts +22 -1
- package/lib/expectations/and.d.ts.map +1 -0
- package/lib/expectations/and.js +30 -5
- package/lib/expectations/and.js.map +1 -1
- package/lib/expectations/contain.d.ts +27 -2
- package/lib/expectations/contain.d.ts.map +1 -0
- package/lib/expectations/contain.js +25 -5
- package/lib/expectations/contain.js.map +1 -1
- package/lib/expectations/containAtLeastOneItemThat.d.ts +23 -1
- package/lib/expectations/containAtLeastOneItemThat.d.ts.map +1 -0
- package/lib/expectations/containAtLeastOneItemThat.js +29 -7
- package/lib/expectations/containAtLeastOneItemThat.js.map +1 -1
- package/lib/expectations/containItemsWhereEachItem.d.ts +22 -0
- package/lib/expectations/containItemsWhereEachItem.d.ts.map +1 -0
- package/lib/expectations/containItemsWhereEachItem.js +29 -7
- package/lib/expectations/containItemsWhereEachItem.js.map +1 -1
- package/lib/expectations/endsWith.d.ts +22 -2
- package/lib/expectations/endsWith.d.ts.map +1 -0
- package/lib/expectations/endsWith.js +20 -5
- package/lib/expectations/endsWith.js.map +1 -1
- package/lib/expectations/equals.d.ts +28 -2
- package/lib/expectations/equals.d.ts.map +1 -0
- package/lib/expectations/equals.js +26 -5
- package/lib/expectations/equals.js.map +1 -1
- package/lib/expectations/includes.d.ts +22 -2
- package/lib/expectations/includes.d.ts.map +1 -0
- package/lib/expectations/includes.js +20 -5
- package/lib/expectations/includes.js.map +1 -1
- package/lib/expectations/index.d.ts +3 -0
- package/lib/expectations/index.d.ts.map +1 -0
- package/lib/expectations/index.js +7 -1
- package/lib/expectations/index.js.map +1 -1
- package/lib/expectations/isAfter.d.ts +42 -2
- package/lib/expectations/isAfter.d.ts.map +1 -0
- package/lib/expectations/isAfter.js +40 -5
- package/lib/expectations/isAfter.js.map +1 -1
- package/lib/expectations/isBefore.d.ts +42 -2
- package/lib/expectations/isBefore.d.ts.map +1 -0
- package/lib/expectations/isBefore.js +40 -5
- package/lib/expectations/isBefore.js.map +1 -1
- package/lib/expectations/isCloseTo.d.ts +24 -0
- package/lib/expectations/isCloseTo.d.ts.map +1 -0
- package/lib/expectations/isCloseTo.js +37 -0
- package/lib/expectations/isCloseTo.js.map +1 -0
- package/lib/expectations/isFalse.d.ts +19 -0
- package/lib/expectations/isFalse.d.ts.map +1 -0
- package/lib/expectations/isFalse.js +18 -0
- package/lib/expectations/isFalse.js.map +1 -1
- package/lib/expectations/isGreaterThan.d.ts +45 -2
- package/lib/expectations/isGreaterThan.d.ts.map +1 -0
- package/lib/expectations/isGreaterThan.js +43 -5
- package/lib/expectations/isGreaterThan.js.map +1 -1
- package/lib/expectations/isLessThan.d.ts +45 -2
- package/lib/expectations/isLessThan.d.ts.map +1 -0
- package/lib/expectations/isLessThan.js +43 -5
- package/lib/expectations/isLessThan.js.map +1 -1
- package/lib/expectations/isPresent.d.ts +58 -8
- package/lib/expectations/isPresent.d.ts.map +1 -0
- package/lib/expectations/isPresent.js +60 -11
- package/lib/expectations/isPresent.js.map +1 -1
- package/lib/expectations/isTrue.d.ts +19 -0
- package/lib/expectations/isTrue.d.ts.map +1 -0
- package/lib/expectations/isTrue.js +18 -0
- package/lib/expectations/isTrue.js.map +1 -1
- package/lib/expectations/matches.d.ts +22 -2
- package/lib/expectations/matches.d.ts.map +1 -0
- package/lib/expectations/matches.js +20 -5
- package/lib/expectations/matches.js.map +1 -1
- package/lib/expectations/not.d.ts +23 -1
- package/lib/expectations/not.d.ts.map +1 -0
- package/lib/expectations/not.js +32 -10
- package/lib/expectations/not.js.map +1 -1
- package/lib/expectations/or.d.ts +22 -1
- package/lib/expectations/or.d.ts.map +1 -0
- package/lib/expectations/or.js +29 -9
- package/lib/expectations/or.js.map +1 -1
- package/lib/expectations/property.d.ts +62 -0
- package/lib/expectations/property.d.ts.map +1 -0
- package/lib/expectations/property.js +85 -0
- package/lib/expectations/property.js.map +1 -0
- package/lib/expectations/startsWith.d.ts +22 -2
- package/lib/expectations/startsWith.d.ts.map +1 -0
- package/lib/expectations/startsWith.js +20 -5
- package/lib/expectations/startsWith.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +6 -5
- package/lib/index.js.map +1 -1
- package/package.json +19 -42
- package/src/Ensure.ts +110 -136
- package/src/EnsureEventually.ts +173 -0
- package/src/expectations/and.ts +38 -14
- package/src/expectations/contain.ts +30 -5
- package/src/expectations/containAtLeastOneItemThat.ts +40 -10
- package/src/expectations/containItemsWhereEachItem.ts +35 -5
- package/src/expectations/endsWith.ts +25 -5
- package/src/expectations/equals.ts +31 -5
- package/src/expectations/includes.ts +25 -5
- package/src/expectations/index.ts +2 -0
- package/src/expectations/isAfter.ts +45 -5
- package/src/expectations/isBefore.ts +45 -5
- package/src/expectations/isCloseTo.ts +40 -0
- package/src/expectations/isFalse.ts +19 -1
- package/src/expectations/isGreaterThan.ts +48 -5
- package/src/expectations/isLessThan.ts +48 -5
- package/src/expectations/isPresent.ts +62 -13
- package/src/expectations/isTrue.ts +19 -1
- package/src/expectations/matches.ts +25 -5
- package/src/expectations/not.ts +28 -5
- package/src/expectations/or.ts +28 -6
- package/src/expectations/property.ts +93 -0
- package/src/expectations/startsWith.ts +25 -5
- package/src/index.ts +1 -1
- package/tsconfig.build.json +10 -0
- 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 {
|
|
18
|
-
|
|
19
|
-
import {
|
|
18
|
+
import { FileSystemLocation } from '@serenity-js/core/lib/io';
|
|
19
|
+
|
|
20
|
+
import { EnsureEventually } from './EnsureEventually';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* @
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* await actorCalled('Erica').attemptsTo(
|
|
36
|
+
* Ensure.that('Hello world!', equals('Hello world!'))
|
|
37
|
+
* )
|
|
38
|
+
* ```
|
|
37
39
|
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { actorCalled } from '@serenity-js/core'
|
|
44
|
+
* import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
|
|
43
45
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
46
|
+
* await actorCalled('Erica').attemptsTo(
|
|
47
|
+
* Ensure.that('Hello world!', and(startsWith('Hello'), endsWith('!'))
|
|
48
|
+
* )
|
|
49
|
+
* ```
|
|
47
50
|
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
* @
|
|
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 {
|
|
68
|
-
*
|
|
77
|
+
* @param {Answerable<Actual_Type>} actual
|
|
78
|
+
* An {@apilink Answerable} describing the actual state of the system.
|
|
69
79
|
*
|
|
70
|
-
* @
|
|
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<
|
|
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
|
-
* @
|
|
78
|
-
*
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
if (outcome instanceof ExpectationNotMet) {
|
|
130
|
+
const actualDescription = d`${ this.actual }`;
|
|
131
|
+
const message = `Expected ${ actualDescription } to ${ outcome.message }`;
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
* @
|
|
127
|
-
*
|
|
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
|
|
131
|
-
*
|
|
150
|
+
* @param typeOfRuntimeError
|
|
151
|
+
* A constructor function producing a subtype of {@apilink RuntimeError} to throw, e.g. {@apilink TestCompromisedError}
|
|
132
152
|
*
|
|
133
|
-
* @param
|
|
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
|
-
|
|
140
|
-
}
|
|
157
|
+
const location = this.instantiationLocation();
|
|
141
158
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
}
|
package/src/expectations/and.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
1
|
+
import { Expectation } from '@serenity-js/core';
|
|
2
2
|
import { equal } from 'tiny-types/lib/objects';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
37
|
+
constructor(private readonly expectation: Expectation<Item>) {
|
|
17
38
|
super(
|
|
39
|
+
'containAtLeastOneItemThat',
|
|
18
40
|
ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
|
|
19
|
-
async (actor: AnswersQuestions, actual: Answerable<
|
|
41
|
+
async (actor: AnswersQuestions, actual: Answerable<Item[]>) => {
|
|
20
42
|
|
|
21
|
-
const items:
|
|
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
|
-
|
|
49
|
+
ExpectationDetails.of('containAtLeastOneItemThat', unanswered),
|
|
50
|
+
unanswered,
|
|
27
51
|
items,
|
|
28
52
|
);
|
|
29
53
|
}
|
|
30
54
|
|
|
31
|
-
let outcome: ExpectationOutcome
|
|
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(
|
|
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
|
}
|