@serenity-js/assertions 3.0.0-rc.4 → 3.0.0-rc.41

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 (120) hide show
  1. package/CHANGELOG.md +72 -1837
  2. package/README.md +14 -12
  3. package/lib/Ensure.d.ts +53 -86
  4. package/lib/Ensure.d.ts.map +1 -0
  5. package/lib/Ensure.js +81 -130
  6. package/lib/Ensure.js.map +1 -1
  7. package/lib/expectations/and.d.ts +22 -1
  8. package/lib/expectations/and.d.ts.map +1 -0
  9. package/lib/expectations/and.js +32 -7
  10. package/lib/expectations/and.js.map +1 -1
  11. package/lib/expectations/contain.d.ts +27 -2
  12. package/lib/expectations/contain.d.ts.map +1 -0
  13. package/lib/expectations/contain.js +25 -5
  14. package/lib/expectations/contain.js.map +1 -1
  15. package/lib/expectations/containAtLeastOneItemThat.d.ts +23 -1
  16. package/lib/expectations/containAtLeastOneItemThat.d.ts.map +1 -0
  17. package/lib/expectations/containAtLeastOneItemThat.js +39 -10
  18. package/lib/expectations/containAtLeastOneItemThat.js.map +1 -1
  19. package/lib/expectations/containItemsWhereEachItem.d.ts +23 -1
  20. package/lib/expectations/containItemsWhereEachItem.d.ts.map +1 -0
  21. package/lib/expectations/containItemsWhereEachItem.js +39 -10
  22. package/lib/expectations/containItemsWhereEachItem.js.map +1 -1
  23. package/lib/expectations/endsWith.d.ts +22 -2
  24. package/lib/expectations/endsWith.d.ts.map +1 -0
  25. package/lib/expectations/endsWith.js +20 -5
  26. package/lib/expectations/endsWith.js.map +1 -1
  27. package/lib/expectations/equals.d.ts +28 -2
  28. package/lib/expectations/equals.d.ts.map +1 -0
  29. package/lib/expectations/equals.js +26 -5
  30. package/lib/expectations/equals.js.map +1 -1
  31. package/lib/expectations/includes.d.ts +22 -2
  32. package/lib/expectations/includes.d.ts.map +1 -0
  33. package/lib/expectations/includes.js +20 -5
  34. package/lib/expectations/includes.js.map +1 -1
  35. package/lib/expectations/index.d.ts +3 -0
  36. package/lib/expectations/index.d.ts.map +1 -0
  37. package/lib/expectations/index.js +7 -1
  38. package/lib/expectations/index.js.map +1 -1
  39. package/lib/expectations/isAfter.d.ts +42 -2
  40. package/lib/expectations/isAfter.d.ts.map +1 -0
  41. package/lib/expectations/isAfter.js +40 -5
  42. package/lib/expectations/isAfter.js.map +1 -1
  43. package/lib/expectations/isBefore.d.ts +42 -2
  44. package/lib/expectations/isBefore.d.ts.map +1 -0
  45. package/lib/expectations/isBefore.js +40 -5
  46. package/lib/expectations/isBefore.js.map +1 -1
  47. package/lib/expectations/isCloseTo.d.ts +24 -0
  48. package/lib/expectations/isCloseTo.d.ts.map +1 -0
  49. package/lib/expectations/isCloseTo.js +37 -0
  50. package/lib/expectations/isCloseTo.js.map +1 -0
  51. package/lib/expectations/isFalse.d.ts +19 -0
  52. package/lib/expectations/isFalse.d.ts.map +1 -0
  53. package/lib/expectations/isFalse.js +18 -0
  54. package/lib/expectations/isFalse.js.map +1 -1
  55. package/lib/expectations/isGreaterThan.d.ts +45 -2
  56. package/lib/expectations/isGreaterThan.d.ts.map +1 -0
  57. package/lib/expectations/isGreaterThan.js +43 -5
  58. package/lib/expectations/isGreaterThan.js.map +1 -1
  59. package/lib/expectations/isLessThan.d.ts +45 -2
  60. package/lib/expectations/isLessThan.d.ts.map +1 -0
  61. package/lib/expectations/isLessThan.js +43 -5
  62. package/lib/expectations/isLessThan.js.map +1 -1
  63. package/lib/expectations/isPresent.d.ts +64 -0
  64. package/lib/expectations/isPresent.d.ts.map +1 -0
  65. package/lib/expectations/isPresent.js +99 -0
  66. package/lib/expectations/isPresent.js.map +1 -0
  67. package/lib/expectations/isTrue.d.ts +19 -0
  68. package/lib/expectations/isTrue.d.ts.map +1 -0
  69. package/lib/expectations/isTrue.js +18 -0
  70. package/lib/expectations/isTrue.js.map +1 -1
  71. package/lib/expectations/matches.d.ts +22 -2
  72. package/lib/expectations/matches.d.ts.map +1 -0
  73. package/lib/expectations/matches.js +20 -5
  74. package/lib/expectations/matches.js.map +1 -1
  75. package/lib/expectations/not.d.ts +23 -1
  76. package/lib/expectations/not.d.ts.map +1 -0
  77. package/lib/expectations/not.js +33 -12
  78. package/lib/expectations/not.js.map +1 -1
  79. package/lib/expectations/or.d.ts +22 -1
  80. package/lib/expectations/or.d.ts.map +1 -0
  81. package/lib/expectations/or.js +40 -15
  82. package/lib/expectations/or.js.map +1 -1
  83. package/lib/expectations/property.d.ts +61 -1
  84. package/lib/expectations/property.d.ts.map +1 -0
  85. package/lib/expectations/property.js +67 -10
  86. package/lib/expectations/property.js.map +1 -1
  87. package/lib/expectations/startsWith.d.ts +22 -2
  88. package/lib/expectations/startsWith.d.ts.map +1 -0
  89. package/lib/expectations/startsWith.js +20 -5
  90. package/lib/expectations/startsWith.js.map +1 -1
  91. package/lib/index.d.ts +1 -1
  92. package/lib/index.d.ts.map +1 -0
  93. package/lib/index.js +5 -5
  94. package/lib/index.js.map +1 -1
  95. package/package.json +17 -41
  96. package/src/Ensure.ts +88 -146
  97. package/src/expectations/and.ts +43 -18
  98. package/src/expectations/contain.ts +30 -5
  99. package/src/expectations/containAtLeastOneItemThat.ts +68 -14
  100. package/src/expectations/containItemsWhereEachItem.ts +68 -14
  101. package/src/expectations/endsWith.ts +25 -5
  102. package/src/expectations/equals.ts +31 -5
  103. package/src/expectations/includes.ts +25 -5
  104. package/src/expectations/index.ts +2 -0
  105. package/src/expectations/isAfter.ts +45 -5
  106. package/src/expectations/isBefore.ts +45 -5
  107. package/src/expectations/isCloseTo.ts +40 -0
  108. package/src/expectations/isFalse.ts +18 -0
  109. package/src/expectations/isGreaterThan.ts +48 -5
  110. package/src/expectations/isLessThan.ts +48 -5
  111. package/src/expectations/isPresent.ts +107 -0
  112. package/src/expectations/isTrue.ts +18 -0
  113. package/src/expectations/matches.ts +25 -5
  114. package/src/expectations/not.ts +38 -15
  115. package/src/expectations/or.ts +51 -26
  116. package/src/expectations/property.ts +80 -20
  117. package/src/expectations/startsWith.ts +25 -5
  118. package/src/index.ts +0 -1
  119. package/tsconfig.build.json +10 -0
  120. package/tsconfig.eslint.json +0 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serenity-js/assertions",
3
- "version": "3.0.0-rc.4",
3
+ "version": "3.0.0-rc.41",
4
4
  "description": "Screenplay-style assertion library",
5
5
  "author": {
6
6
  "name": "Jan Molak",
@@ -26,58 +26,34 @@
26
26
  "testing"
27
27
  ],
28
28
  "scripts": {
29
- "clean": "rimraf .nyc_output lib target",
30
- "lint": "eslint --ext ts --config ../../.eslintrc.yml .",
31
- "lint:fix": "npm run lint -- --fix",
32
- "test": "nyc --report-dir ../../target/coverage/assertions mocha --config ../../.mocharc.yml 'spec/**/*.spec.*'",
33
- "compile": "tsc --project tsconfig.json",
34
- "site": "esdoc -c .esdoc.js"
29
+ "clean": "rimraf '../../target/coverage/assertions'",
30
+ "test": "nyc mocha --config ../../.mocharc.yml 'spec/**/*.spec.*'",
31
+ "compile": "rimraf lib && tsc --project tsconfig.build.json"
35
32
  },
36
33
  "repository": {
37
34
  "type": "git",
38
- "url": "https://github.com/serenity-js/serenity-js.git"
35
+ "url": "https://github.com/serenity-js/serenity-js.git",
36
+ "directory": "packages/assertions"
39
37
  },
40
38
  "bugs": {
41
39
  "url": "https://github.com/serenity-js/serenity-js/issues"
42
40
  },
43
41
  "engines": {
44
- "node": "^14 || ^16",
42
+ "node": "^14 || ^16 || ^18",
45
43
  "npm": "^6 || ^7 || ^8"
46
44
  },
47
45
  "dependencies": {
48
- "@serenity-js/core": "3.0.0-rc.4",
49
- "tiny-types": "^1.17.0"
46
+ "@serenity-js/core": "3.0.0-rc.41",
47
+ "tiny-types": "^1.19.0"
50
48
  },
51
49
  "devDependencies": {
52
- "@documentation/esdoc-template": "3.0.0",
53
50
  "@integration/testing-tools": "3.0.0",
54
- "@types/chai": "^4.3.0",
55
- "@types/mocha": "^9.0.0",
56
- "mocha": "^9.1.3",
57
- "ts-node": "^10.4.0",
58
- "typescript": "^4.5.2"
59
- },
60
- "nyc": {
61
- "include": [
62
- "src/**/*.ts"
63
- ],
64
- "exclude": [
65
- "src/**/*.d.ts",
66
- "lib",
67
- "node_modules",
68
- "spec"
69
- ],
70
- "extension": [
71
- ".ts"
72
- ],
73
- "require": [
74
- "ts-node/register"
75
- ],
76
- "reporter": [
77
- "json"
78
- ],
79
- "cache": true,
80
- "all": true
81
- },
82
- "gitHead": "126a1cad42bcb1416ca68e62f54bf0d3eb3ea157"
51
+ "@types/chai": "^4.3.4",
52
+ "@types/mocha": "^10.0.1",
53
+ "mocha": "^10.2.0",
54
+ "nyc": "15.1.0",
55
+ "ts-node": "^10.9.1",
56
+ "typescript": "^4.9.4"
57
+ },
58
+ "gitHead": "e74f8f4989f18fe37edde3f8500b2084289d385b"
83
59
  }
package/src/Ensure.ts CHANGED
@@ -1,202 +1,144 @@
1
1
  import {
2
+ Activity,
2
3
  Answerable,
3
4
  AnswersQuestions,
4
5
  AssertionError,
5
6
  CollectsArtifacts,
7
+ d,
6
8
  Expectation,
7
9
  ExpectationMet,
8
10
  ExpectationNotMet,
9
- ExpectationOutcome,
11
+ f,
10
12
  Interaction,
11
13
  LogicError,
14
+ RaiseErrors,
12
15
  RuntimeError,
13
16
  UsesAbilities,
14
17
  } from '@serenity-js/core';
15
- import { formatted } from '@serenity-js/core/lib/io';
16
- import { inspected } from '@serenity-js/core/lib/io/inspected';
17
- import { Artifact, AssertionReport, Name } from '@serenity-js/core/lib/model';
18
- import { match } from 'tiny-types';
18
+ import { FileSystemLocation } from '@serenity-js/core/lib/io';
19
19
 
20
20
  /**
21
- * @desc
22
- * Used to perform verification of the system under test.
21
+ * The {@apilink Interaction|interaction} to `Ensure`
22
+ * verifies if the resolved value of the provided {@apilink Answerable}
23
+ * meets the specified {@apilink Expectation}.
24
+ * If not, it throws an {@apilink AssertionError}.
23
25
  *
24
- * Resolves any `Answerable` describing the actual
25
- * state and ensures that its value meets the {@link @serenity-js/core/lib/screenplay/questions~Expectation}s provided.
26
+ * Use `Ensure` to verify the state of the system under test.
26
27
  *
27
- * @example <caption>Usage with static values</caption>
28
- * import { actorCalled } from '@serenity-js/core';
29
- * import { Ensure, equals } from '@serenity-js/assertions';
28
+ * ## Basic usage with static values
29
+ * ```ts
30
+ * import { actorCalled } from '@serenity-js/core'
31
+ * import { Ensure, equals } from '@serenity-js/assertions'
30
32
  *
31
- * const actor = actorCalled('Erica');
33
+ * await actorCalled('Erica').attemptsTo(
34
+ * Ensure.that('Hello world!', equals('Hello world!'))
35
+ * )
36
+ * ```
32
37
  *
33
- * actor.attemptsTo(
34
- * Ensure.that('Hello world!', equals('Hello world!'))
35
- * );
38
+ * ## Composing expectations with `and`
36
39
  *
37
- * @example <caption>Composing expectations with `and`</caption>
38
- * import { actorCalled } from '@serenity-js/core';
39
- * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions';
40
+ * ```ts
41
+ * import { actorCalled } from '@serenity-js/core'
42
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
40
43
  *
41
- * const actor = actorCalled('Erica');
44
+ * await actorCalled('Erica').attemptsTo(
45
+ * Ensure.that('Hello world!', and(startsWith('Hello'), endsWith('!'))
46
+ * )
47
+ * ```
42
48
  *
43
- * actor.attemptsTo(
44
- * Ensure.that('Hello world!', and(startsWith('Hello'), endsWith('!'))
45
- * );
49
+ * ## Overriding the type of Error thrown upon assertion failure
46
50
  *
47
- * @example <caption>Overriding the type of Error thrown upon assertion failure</caption>
48
- * import { actorCalled, TestCompromisedError } from '@serenity-js/core';
49
- * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions';
50
- * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest';
51
+ * ```ts
52
+ * import { actorCalled, TestCompromisedError } from '@serenity-js/core'
53
+ * import { and, Ensure, startsWith, endsWith } from '@serenity-js/assertions'
54
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
51
55
  *
52
- * const actor = actorCalled('Erica')
53
- * .whoCan(CallAnApi.at('https://example.com'));
56
+ * await actorCalled('Erica')
57
+ * .whoCan(CallAnApi.at('https://example.com'))
58
+ * .attemptsTo(
59
+ * Send.a(GetRequest.to('/api/health')),
60
+ * Ensure.that(LastResponse.status(), equals(200))
61
+ * .otherwiseFailWith(TestCompromisedError, 'The server is down, please cheer it up!')
62
+ * )
63
+ * ```
54
64
  *
55
- * actor.attemptsTo(
56
- * Send.a(GetRequest.to('/api/health')),
57
- * Ensure.that(LastResponse.status(), equals(200))
58
- * .otherwiseFailWith(TestCompromisedError, 'The server is down, please cheer it up!')
59
- * );
60
- *
61
- * @extends {@serenity-js/core/lib/screenplay~Interaction}
65
+ * @group Interactions
62
66
  */
63
67
  export class Ensure<Actual> extends Interaction {
68
+
64
69
  /**
70
+ * @param {Answerable<Actual_Type>} actual
71
+ * An {@apilink Answerable} describing the actual state of the system.
65
72
  *
66
- * @param {@serenity-js/core/lib/screenplay~Answerable<T>} actual
67
- * @param {@serenity-js/core/lib/screenplay/questions~Expectation<any, A>} expectation
73
+ * @param {Expectation<Actual_Type>} expectation
74
+ * An {@apilink Expectation} you expect the `actual` value to meet
68
75
  *
69
- * @returns {Ensure<A>}
76
+ * @returns {Ensure<Actual_Type>}
70
77
  */
71
- static that<A>(actual: Answerable<A>, expectation: Expectation<any, A>): Ensure<A> {
72
- return new Ensure(actual, expectation);
78
+ static that<Actual_Type>(actual: Answerable<Actual_Type>, expectation: Expectation<Actual_Type>): Ensure<Actual_Type> {
79
+ return new Ensure(actual, expectation, Activity.callerLocation(5));
73
80
  }
74
81
 
75
82
  /**
76
- * @param {@serenity-js/core/lib/screenplay~Answerable<T>} actual
77
- * @param {@serenity-js/core/lib/screenplay/questions~Expectation<T>} expectation
83
+ * @param actual
84
+ * @param expectation
85
+ * @param location
78
86
  */
79
- constructor(
87
+ private constructor(
80
88
  protected readonly actual: Answerable<Actual>,
81
89
  protected readonly expectation: Expectation<Actual>,
90
+ location: FileSystemLocation,
82
91
  ) {
83
- super();
92
+ super(d`#actor ensures that ${ actual } does ${ expectation }`, location);
84
93
  }
85
94
 
86
95
  /**
87
- * @desc
88
- * Makes the provided {@link @serenity-js/core/lib/screenplay/actor~Actor}
89
- * perform this {@link @serenity-js/core/lib/screenplay~Interaction}.
90
- *
91
- * @param {UsesAbilities & CollectsArtifacts & AnswersQuestions} actor
92
- * @returns {Promise<void>}
93
- *
94
- * @see {@link @serenity-js/core/lib/screenplay/actor~Actor}
95
- * @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
96
- * @see {@link @serenity-js/core/lib/screenplay/actor~CollectsArtifacts}
97
- * @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
96
+ * @inheritDoc
98
97
  */
99
- performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> {
100
- return Promise.all([
101
- actor.answer(this.actual),
102
- actor.answer(this.expectation),
103
- ]).then(([ actual, expectation ]) =>
104
- expectation(actual).then(outcome =>
105
- match<ExpectationOutcome<unknown, Actual>, void>(outcome)
106
- .when(ExpectationNotMet, o => {
107
- actor.collect(this.artifactFrom(o.expected, o.actual), new Name(`Assertion Report`));
98
+ async performAs(actor: UsesAbilities & AnswersQuestions & CollectsArtifacts): Promise<void> {
99
+ const outcome = await actor.answer(this.expectation.isMetFor(this.actual));
108
100
 
109
- throw this.errorForOutcome(o);
110
- })
111
- .when(ExpectationMet, _ => void 0)
112
- .else(o => {
113
- throw new LogicError(formatted `An Expectation should return an instance of an ExpectationOutcome, not ${ o }`);
114
- }),
115
- ),
116
- );
117
- }
101
+ if (outcome instanceof ExpectationNotMet) {
102
+ const actualDescription = d`${ this.actual }`;
103
+ const message = `Expected ${ actualDescription } to ${ outcome.message }`;
118
104
 
119
- /**
120
- * @desc
121
- * Generates a description to be used when reporting this {@link @serenity-js/core/lib/screenplay~Activity}.
122
- *
123
- * @returns {string}
124
- */
125
- toString(): string {
126
- return formatted `#actor ensures that ${ this.actual } does ${ this.expectation }`;
105
+ throw RaiseErrors.as(actor).create(AssertionError, {
106
+ message,
107
+ expectation: outcome.expectation,
108
+ diff: { expected: outcome.expected, actual: outcome.actual },
109
+ location: this.instantiationLocation(),
110
+ });
111
+ }
112
+
113
+ if (! (outcome instanceof ExpectationMet)) {
114
+ throw new LogicError(f`Expectation#isMetFor(actual) should return an instance of an ExpectationOutcome, not ${ outcome }`);
115
+ }
127
116
  }
128
117
 
129
118
  /**
130
- * @desc
131
- * Overrides the default {@link @serenity-js/core/lib/errors~AssertionError} thrown when
132
- * the actual value does not meet the expectations set.
119
+ * Overrides the default {@apilink AssertionError} thrown when
120
+ * the actual value does not meet the expectation.
133
121
  *
134
- * @param {Function} typeOfRuntimeError
135
- * The type of RuntimeError to throw, i.e. TestCompromisedError
122
+ * @param typeOfRuntimeError
123
+ * A constructor function producing a subtype of {@apilink RuntimeError} to throw, e.g. {@apilink TestCompromisedError}
136
124
  *
137
- * @param {string} message
125
+ * @param message
138
126
  * The message explaining the failure
139
- *
140
- * @returns {@serenity-js/core/lib/screenplay~Interaction}
141
127
  */
142
128
  otherwiseFailWith(typeOfRuntimeError: new (message: string, cause?: Error) => RuntimeError, message?: string): Interaction {
143
- return new EnsureOrFailWithCustomError(this.actual, this.expectation, typeOfRuntimeError, message);
144
- }
145
-
146
- /**
147
- * @desc
148
- * Maps an {@link @serenity-js/core/lib/screenplay/questions/expectations~ExpectationOutcome} to appropriate {@link @serenity-js/core/lib/errors~RuntimeError}.
149
- *
150
- * @param {@serenity-js/core/lib/screenplay/questions/expectations~ExpectationOutcome} outcome
151
- * @returns {@serenity-js/core/lib/errors~RuntimeError}
152
- *
153
- * @protected
154
- */
155
- protected errorForOutcome(outcome: ExpectationOutcome<any, Actual>): RuntimeError {
156
- return this.asAssertionError(outcome);
157
- }
158
-
159
- /**
160
- * @desc
161
- * Maps an {@link Outcome} to {@link @serenity-js/core/lib/errors~AssertionError}.
162
- *
163
- * @param {Outcome} outcome
164
- * @returns {@serenity-js/core/lib/errors~AssertionError}
165
- *
166
- * @protected
167
- */
168
- protected asAssertionError(outcome: ExpectationOutcome<any, Actual>): AssertionError {
169
- return new AssertionError(
170
- `Expected ${ formatted`${ this.actual }` } to ${ outcome.message }`,
171
- outcome.expected,
172
- outcome.actual,
173
- );
174
- }
129
+ const location = this.instantiationLocation();
175
130
 
176
- private artifactFrom(expected: any, actual: Actual): Artifact {
177
- return AssertionReport.fromJSON({
178
- expected: inspected(expected),
179
- actual: inspected(actual),
131
+ return Interaction.where(this.toString(), async actor => {
132
+ try {
133
+ await this.performAs(actor);
134
+ }
135
+ catch (error) {
136
+ throw RaiseErrors.as(actor).create(typeOfRuntimeError, {
137
+ message: message ?? error.message,
138
+ location,
139
+ cause: error
140
+ });
141
+ }
180
142
  });
181
143
  }
182
144
  }
183
-
184
- /**
185
- * @package
186
- */
187
- class EnsureOrFailWithCustomError<Actual> extends Ensure<Actual> {
188
- constructor(
189
- actual: Answerable<Actual>,
190
- expectation: Expectation<Actual>,
191
- private readonly typeOfRuntimeError: new (message: string, cause?: Error) => RuntimeError,
192
- private readonly message?: string,
193
- ) {
194
- super(actual, expectation);
195
- }
196
-
197
- protected errorForOutcome(outcome: ExpectationOutcome<any, Actual>): RuntimeError {
198
- const assertionError = this.asAssertionError(outcome);
199
-
200
- return new this.typeOfRuntimeError(this.message || assertionError.message, assertionError);
201
- }
202
- }
@@ -1,29 +1,54 @@
1
- import { AnswersQuestions, Expectation, ExpectationNotMet, ExpectationOutcome } 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<any, Actual>>): Expectation<any, 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
 
8
27
  /**
9
28
  * @package
10
29
  */
11
- class And<Actual> extends Expectation<any, Actual> {
12
- constructor(private readonly expectations: Array<Expectation<any, Actual>>) {
13
- super(expectations.map(assertion => assertion.toString()).join(' and '));
14
- }
30
+ class And<Actual> extends Expectation<Actual> {
31
+ private static readonly Separator = ' and ';
32
+
33
+ constructor(private readonly expectations: Array<Expectation<Actual>>) {
34
+ const description = expectations.map(expectation => expectation.toString()).join(And.Separator);
35
+
36
+ super(
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))
15
44
 
16
- answeredBy(actor: AnswersQuestions): (actual: Actual) => Promise<ExpectationOutcome<any, Actual>> {
45
+ if (outcome instanceof ExpectationNotMet) {
46
+ return new ExpectationNotMet(description, outcome.expectation, outcome.expected, outcome.actual);
47
+ }
48
+ }
17
49
 
18
- return (actual: any) =>
19
- this.expectations.reduce(
20
- (previous, current) =>
21
- previous.then(outcome =>
22
- match(outcome)
23
- .when(ExpectationNotMet, o => o)
24
- .else(_ => current.answeredBy(actor)(actual)),
25
- ),
26
- Promise.resolve(void 0),
27
- );
50
+ return new ExpectationMet(description, outcome?.expectation, outcome?.expected, outcome?.actual);
51
+ }
52
+ );
28
53
  }
29
54
  }
@@ -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, 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,26 +1,80 @@
1
- import { AnswersQuestions, Expectation, ExpectationMet, ExpectationNotMet, ExpectationOutcome } from '@serenity-js/core';
2
- import { formatted } from '@serenity-js/core/lib/io';
1
+ import { Answerable, AnswersQuestions, d, Expectation, ExpectationDetails, ExpectationMet, ExpectationNotMet, ExpectationOutcome, Unanswered } from '@serenity-js/core';
3
2
 
4
- export function containAtLeastOneItemThat<Actual>(expectation: Expectation<any, Actual>): Expectation<any, 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[]> {
5
25
  return new ContainAtLeastOneItemThatMeetsExpectation(expectation);
6
26
  }
7
27
 
8
28
  /**
9
29
  * @package
10
30
  */
11
- class ContainAtLeastOneItemThatMeetsExpectation<Expected, Actual> extends Expectation<Expected, Actual[]> {
12
- constructor(private readonly expectation: Expectation<Expected, Actual>) {
13
- super(formatted `contain at least one item that does ${ expectation }`);
31
+ class ContainAtLeastOneItemThatMeetsExpectation<Item> extends Expectation<Item[]> {
32
+
33
+ private static descriptionFor(expectation: Expectation<any>) {
34
+ return d`contain at least one item that does ${ expectation }`;
14
35
  }
15
36
 
16
- answeredBy(actor: AnswersQuestions): (actual: Actual[]) => Promise<ExpectationOutcome<Expected, Actual[]>> {
17
- return (actual: Actual[]) =>
18
- actual.length === 0
19
- ? Promise.resolve(new ExpectationNotMet(this.toString(), undefined, actual))
20
- : Promise.all(actual.map(item => this.expectation.answeredBy(actor)(item)))
21
- .then(results => results.some(result => result instanceof ExpectationMet)
22
- ? new ExpectationMet(this.toString(), results[0].expected, actual)
23
- : new ExpectationNotMet(this.toString(), results[0].expected, actual),
37
+ constructor(private readonly expectation: Expectation<Item>) {
38
+ super(
39
+ 'containAtLeastOneItemThat',
40
+ ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
41
+ async (actor: AnswersQuestions, actual: Answerable<Item[]>) => {
42
+
43
+ const items: Item[] = await actor.answer(actual);
44
+
45
+ if (! items || items.length === 0) {
46
+ const unanswered = new Unanswered();
47
+ return new ExpectationNotMet(
48
+ ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
49
+ ExpectationDetails.of('containAtLeastOneItemThat', unanswered),
50
+ unanswered,
51
+ items,
24
52
  );
53
+ }
54
+
55
+ let outcome: ExpectationOutcome;
56
+
57
+ for (const item of items) {
58
+
59
+ outcome = await actor.answer(expectation.isMetFor(item))
60
+
61
+ if (outcome instanceof ExpectationMet) {
62
+ return new ExpectationMet(
63
+ ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
64
+ ExpectationDetails.of('containAtLeastOneItemThat', outcome.expectation),
65
+ outcome.expected,
66
+ items,
67
+ );
68
+ }
69
+ }
70
+
71
+ return new ExpectationNotMet(
72
+ ContainAtLeastOneItemThatMeetsExpectation.descriptionFor(expectation),
73
+ ExpectationDetails.of('containAtLeastOneItemThat', outcome.expectation),
74
+ outcome.expected,
75
+ items,
76
+ );
77
+ }
78
+ );
25
79
  }
26
80
  }