@koenvanbelle/cypress-soft-assertions 1.0.8 → 2.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/README.md +64 -189
- package/dist/index.js +141 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,130 +1,90 @@
|
|
|
1
1
|
# Cypress Soft Assertions
|
|
2
2
|
|
|
3
|
-
A Cypress plugin that provides `soft_it()`
|
|
3
|
+
A Cypress plugin that provides `soft_it()` — a drop-in replacement for `it()` that makes all assertions soft. Assertions continue execution on failure, and all failures are aggregated and reported together at the end of the test.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why?
|
|
6
6
|
|
|
7
7
|
Standard Cypress assertions stop test execution at the first failure. With `soft_it()`:
|
|
8
8
|
|
|
9
|
-
- All assertions run even if some fail
|
|
10
|
-
- See all failures at once
|
|
11
|
-
- Drop-in replacement
|
|
12
|
-
- No manual tracking
|
|
9
|
+
- **All assertions run** even if some fail
|
|
10
|
+
- **See all failures at once** — no need to fix one issue and rerun to find the next
|
|
11
|
+
- **Drop-in replacement** — just change `it()` to `soft_it()`
|
|
12
|
+
- **No manual tracking** — failures are automatically aggregated and reported
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
Install the plugin as a development dependency:
|
|
17
|
-
|
|
18
16
|
```bash
|
|
19
17
|
npm install @koenvanbelle/cypress-soft-assertions --save-dev
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
yarn add @koenvanbelle/cypress-soft-assertions --dev
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Integration
|
|
29
|
-
|
|
30
|
-
### Step 1: Import the Plugin
|
|
20
|
+
## Setup
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
### 1. Import the plugin
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
Add the import to your Cypress support file (`cypress/support/e2e.ts` or `cypress/support/e2e.js`):
|
|
35
25
|
|
|
36
26
|
```typescript
|
|
37
27
|
import '@koenvanbelle/cypress-soft-assertions';
|
|
38
28
|
```
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
Or for JavaScript:
|
|
41
31
|
|
|
42
32
|
```javascript
|
|
43
33
|
require('@koenvanbelle/cypress-soft-assertions');
|
|
44
34
|
```
|
|
45
35
|
|
|
46
|
-
###
|
|
36
|
+
### 2. Add type definitions (TypeScript only)
|
|
47
37
|
|
|
48
|
-
|
|
38
|
+
Add a reference directive at the top of your test files:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
/// <reference types="@koenvanbelle/cypress-soft-assertions" />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or include the types in your `tsconfig.json`:
|
|
49
45
|
|
|
50
46
|
```json
|
|
51
47
|
{
|
|
52
48
|
"compilerOptions": {
|
|
53
|
-
"types": ["cypress", "cypress-soft-assertions"]
|
|
49
|
+
"types": ["cypress", "@koenvanbelle/cypress-soft-assertions"]
|
|
54
50
|
}
|
|
55
51
|
}
|
|
56
52
|
```
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
/// <reference types="cypress-soft-assertions" />
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Step 3: Use in Tests
|
|
54
|
+
## Usage
|
|
65
55
|
|
|
66
56
|
Replace `it()` with `soft_it()` in any test where you want soft assertion behavior:
|
|
67
57
|
|
|
68
58
|
```typescript
|
|
69
59
|
describe('Product Page', () => {
|
|
70
60
|
soft_it('validates all product details', () => {
|
|
71
|
-
cy.visit('
|
|
72
|
-
|
|
73
|
-
// All
|
|
61
|
+
cy.visit('/product/123');
|
|
62
|
+
|
|
63
|
+
// All assertions run even if some fail
|
|
74
64
|
cy.get('.product-name').should('have.text', 'Awesome Product');
|
|
75
65
|
cy.get('.product-price').should('have.text', '$99.99');
|
|
76
66
|
cy.get('.stock-status').should('have.text', 'In Stock');
|
|
77
|
-
|
|
67
|
+
cy.get('.rating').should('have.attr', 'data-stars', '5');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Supported assertion styles
|
|
78
73
|
|
|
79
74
|
`soft_it()` works with all Cypress assertion styles:
|
|
80
75
|
|
|
81
76
|
```typescript
|
|
82
77
|
soft_it('supports all assertion types', () => {
|
|
83
|
-
cy.visit('/page');
|
|
84
|
-
|
|
85
78
|
// .should() assertions
|
|
86
79
|
cy.get('.title').should('be.visible');
|
|
87
80
|
cy.get('.title').should('have.text', 'Welcome');
|
|
88
|
-
|
|
89
|
-
// .should() with callback
|
|
90
|
-
cy.get('.items').should(($items) => {
|
|
91
|
-
expect($items).to.have.length(5);
|
|
92
|
-
expect($items.first()).to.contain('Item 1');
|
|
93
|
-
});
|
|
94
|
-
|
|
81
|
+
|
|
95
82
|
// expect() in .then()
|
|
96
|
-
cy.get('.price').then($el => {
|
|
83
|
+
cy.get('.price').then(($el) => {
|
|
97
84
|
expect($el.text()).to.equal('$99.99');
|
|
98
85
|
});
|
|
99
|
-
|
|
100
|
-
// Chained assertions
|
|
101
|
-
cy.get('.button')
|
|
102
|
-
.should('be.visible')
|
|
103
|
-
.and('have.class', 'active')
|
|
104
|
-
.and('contain', 'Submit');
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
You can use both `soft_it()` and regular `it()` in the same test suite:
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
describe('User Profile', () => {
|
|
114
|
-
// Regular test - stops on first failure
|
|
115
|
-
it('loads the page', () => {
|
|
116
|
-
cy.visit('/profile');
|
|
117
|
-
cy.get('h1').should('be.visible');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Soft test - all assertions run
|
|
121
|
-
soft_it('validates profile fields', () => {
|
|
122
|
-
cy.get('.username').should('have.text', 'john_doe');
|
|
123
|
-
cy.get('.email').should('have.text', 'john@example.com');
|
|
124
|
-
cy.get('.member-since').should('contain', '2023');
|
|
125
|
-
cy.get('.posts-count').should('have.text', '42');
|
|
126
|
-
|
|
127
|
-
// Chained assertions
|
|
87
|
+
// Chained assertions with .and()
|
|
128
88
|
cy.get('.button')
|
|
129
89
|
.should('be.visible')
|
|
130
90
|
.and('have.class', 'active')
|
|
@@ -132,158 +92,73 @@ describe('User Profile', () => {
|
|
|
132
92
|
});
|
|
133
93
|
```
|
|
134
94
|
|
|
135
|
-
###
|
|
95
|
+
### Mixing `soft_it()` with regular `it()`
|
|
136
96
|
|
|
137
|
-
You can
|
|
97
|
+
You can use both in the same suite. Regular `it()` tests are unaffected:
|
|
138
98
|
|
|
139
99
|
```typescript
|
|
140
100
|
describe('User Profile', () => {
|
|
141
|
-
// Regular test
|
|
101
|
+
// Regular test — stops on first failure
|
|
142
102
|
it('loads the page', () => {
|
|
143
103
|
cy.visit('/profile');
|
|
144
104
|
cy.get('h1').should('be.visible');
|
|
145
105
|
});
|
|
146
|
-
|
|
147
|
-
// Soft test
|
|
106
|
+
|
|
107
|
+
// Soft test — all assertions run
|
|
148
108
|
soft_it('validates profile fields', () => {
|
|
149
109
|
cy.get('.username').should('have.text', 'john_doe');
|
|
150
110
|
cy.get('.email').should('have.text', 'john@example.com');
|
|
151
111
|
cy.get('.member-since').should('contain', '2023');
|
|
152
112
|
cy.get('.posts-count').should('have.text', '42');
|
|
153
113
|
});
|
|
154
|
-
|
|
155
|
-
// Another soft test
|
|
156
|
-
soft_it('validates account settings', () => {
|
|
157
|
-
cy.get('[data-test=notifications]').should('be.checked');
|
|
158
|
-
cy.get('[data-test=newsletter]').should('not.be.checked');
|
|
159
|
-
cy.get('[data-test=theme]').should('have.value', 'dark');
|
|
160
|
-
});
|
|
161
114
|
});
|
|
162
115
|
```
|
|
163
116
|
|
|
164
|
-
###
|
|
117
|
+
### `soft_it.only` and `soft_it.skip`
|
|
165
118
|
|
|
166
|
-
|
|
119
|
+
Works just like regular `it()`:
|
|
167
120
|
|
|
168
121
|
```typescript
|
|
169
122
|
describe('Test Suite', () => {
|
|
170
|
-
|
|
171
|
-
soft_it.only('validates important fields', () => {
|
|
123
|
+
soft_it.only('run only this test', () => {
|
|
172
124
|
cy.get('.field1').should('exist');
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
The plugin supports `.only` and `.skip` modifiers just like regular `it()`:
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
describe('Test Suite', () => {
|
|
179
|
-
// Run only this test
|
|
180
|
-
soft_it.only('validates important fields', () => {
|
|
181
|
-
cy.get('.field1').should('exist');
|
|
182
|
-
cy.get('.field2').should('exist');
|
|
183
125
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
soft_it.skip('validates optional fields', () => {
|
|
126
|
+
|
|
127
|
+
soft_it.skip('skip this test', () => {
|
|
187
128
|
cy.get('.optional').should('exist');
|
|
188
129
|
});
|
|
189
130
|
});
|
|
190
131
|
```
|
|
191
132
|
|
|
192
|
-
## How
|
|
193
|
-
|
|
194
|
-
`soft_it()` intercepts Chai assertions within the test block and captures failures instead of throwing them immediately. At the end of the test, all captured failures are aggregated and reported in a single error message.
|
|
195
|
-
|
|
196
|
-
The plugin works with:
|
|
197
|
-
- `.should()` assertions
|
|
198
|
-
- `expect()` assertions
|
|
199
|
-
- `assert()` assertions
|
|
200
|
-
- Custom Cypress commands that use assertions
|
|
201
|
-
- Assertion chains (`.and()`)
|
|
202
|
-
|
|
203
|
-
## Important Notes
|
|
204
|
-
|
|
205
|
-
### When Assertions Are Reported
|
|
133
|
+
## How it works
|
|
206
134
|
|
|
207
|
-
|
|
135
|
+
1. `soft_it()` patches Chai's assertion mechanism for the duration of the test.
|
|
136
|
+
2. When an assertion fails inside a retriable command (`.should()`, `.and()`), the error is staged under a stable token and rethrown so Cypress can retry. If the assertion eventually passes, the staged failure is cleared.
|
|
137
|
+
3. After a bounded number of retries, the assertion is swallowed and the failure is recorded so the test can continue to the next command.
|
|
138
|
+
4. Non-retriable assertion failures (`expect()` in `.then()` callbacks) are captured immediately.
|
|
139
|
+
5. Command-level failures (e.g. element-not-found timeouts) are caught by a `Cypress.on('fail')` handler and recorded as soft failures.
|
|
140
|
+
6. At the end of the test, all recorded failures are aggregated into a single `SoftAssertionError`.
|
|
208
141
|
|
|
209
|
-
|
|
142
|
+
## Error output
|
|
210
143
|
|
|
211
|
-
|
|
212
|
-
- **Soft `soft_it()`**: Runs all assertions, reports all failures at the end
|
|
144
|
+
When soft assertions fail, you get a single formatted report:
|
|
213
145
|
|
|
214
|
-
### Non-Assertion Errors
|
|
215
|
-
|
|
216
|
-
Non-assertion errors (network errors, timeouts, command errors) will still stop test execution immediately. `soft_it()` only makes assertions soft.
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
soft_it('example', () => {
|
|
220
|
-
cy.visit('/page'); // If this fails (timeout, 404), test stops immediately
|
|
221
|
-
cy.get('.missing').should('exist'); // This assertion is soft
|
|
222
|
-
cy.get('.other').should('exist'); // This assertion is soft
|
|
223
|
-
});
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Best Practices
|
|
227
|
-
|
|
228
|
-
### Use for Validation-Heavy Tests
|
|
229
|
-
|
|
230
|
-
`soft_it()` is ideal for tests that validate many fields:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
soft_it('validates user profile completeness', () => {
|
|
234
|
-
cy.get('.name').should('not.be.empty');
|
|
235
|
-
cy.get('.email').should('match', /@/);
|
|
236
|
-
cy.get('.phone').should('have.length.at.least', 10);
|
|
237
|
-
cy.get('.address').should('not.be.empty');
|
|
238
|
-
cy.get('.city').should('not.be.empty');
|
|
239
|
-
cy.get('.avatar').should('be.visible');
|
|
240
|
-
});
|
|
241
146
|
```
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cy.visit('/login');
|
|
250
|
-
cy.get('#username').type('user');
|
|
251
|
-
cy.get('#password').type('pass');
|
|
252
|
-
cy.get('button[type=submit]').click();
|
|
253
|
-
cy.url().should('include', '/dashboard');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
soft_it('validates dashboard widgets', () => {
|
|
257
|
-
cy.get('.widget-1').should('be.visible');
|
|
258
|
-
cy.get('.widget-2').should('be.visible');
|
|
259
|
-
cy.get('.widget-3').should('be.visible');
|
|
260
|
-
});
|
|
147
|
+
================================================================================
|
|
148
|
+
SOFT ASSERTION FAILURES (3 failed):
|
|
149
|
+
================================================================================
|
|
150
|
+
1. expected '<h1.title>' to have text 'Wrong Title', but the text was 'Products'
|
|
151
|
+
2. expected '<div.count>' to have text '999', but the text was '42'
|
|
152
|
+
3. expected '<div.missing>' to exist in the DOM
|
|
153
|
+
================================================================================
|
|
261
154
|
```
|
|
262
155
|
|
|
263
|
-
|
|
156
|
+
## Important notes
|
|
264
157
|
|
|
265
|
-
|
|
158
|
+
- **Retriable assertions are retried**: `.should()` and `.and()` assertions are retried by Cypress before being captured as soft failures. Assertions that eventually pass are not reported.
|
|
159
|
+
- **Non-assertion errors still stop execution**: Network errors, visit timeouts, and other command errors are captured as soft failures but may prevent subsequent commands from running.
|
|
160
|
+
- **State resets between tests**: Each `soft_it()` test starts with a clean slate — failures from one test never leak into another.
|
|
266
161
|
|
|
267
|
-
|
|
268
|
-
soft_it('validates navigation menu', () => {
|
|
269
|
-
cy.get('nav a').eq(0).should('have.text', 'Home');
|
|
270
|
-
cy.get('nav a').eq(1).should('have.text', 'Products');
|
|
271
|
-
cy.get('nav a').eq(2).should('have.text', 'About');
|
|
272
|
-
cy.get('nav a').eq(3).should('have.text', 'Contact');
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Error Output Format
|
|
277
|
-
|
|
278
|
-
When soft assertions fail, you receive a formatted error report:
|
|
162
|
+
## License
|
|
279
163
|
|
|
280
|
-
|
|
281
|
-
================================================================================
|
|
282
|
-
SOFT ASSERTION FAILURES (4 failed):
|
|
283
|
-
================================================================================
|
|
284
|
-
1. expected '<input#email>' to have value 'test@example.com', but the value was 'wrong@example.com'
|
|
285
|
-
2. expected '<div.order-total>' to contain '$99.99', but it contained '$89.99'
|
|
286
|
-
3. expected '<div.tax>' to contain '$8.75', but it contained '$7.50'
|
|
287
|
-
4. expected '<div.grand-total>' to contain '$113.74', but it contained '$102.49'
|
|
288
|
-
================================================================================
|
|
289
|
-
```
|
|
164
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -8,87 +8,149 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
let softAssertionErrors = [];
|
|
11
|
+
let retryAssertionFailures = new Map();
|
|
12
|
+
let retryAttemptCount = new Map();
|
|
11
13
|
let isInSoftTest = false;
|
|
12
14
|
let activeFailHandler = null;
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
let originalChaiAssert = null;
|
|
16
|
+
// After this many consecutive assertion failures on the same token, stop
|
|
17
|
+
// rethrowing (which would cause Cypress to retry) and swallow instead.
|
|
18
|
+
// This gives Cypress enough retry cycles for assertions that will eventually
|
|
19
|
+
// pass, while bounding the time spent on definitively failing assertions.
|
|
20
|
+
const MAX_RETHROWS = 10;
|
|
18
21
|
function captureSoftAssertion(error) {
|
|
19
22
|
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
20
23
|
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
21
24
|
const lastEntry = softAssertionErrors[softAssertionErrors.length - 1];
|
|
22
|
-
if (lastEntry
|
|
25
|
+
if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
|
|
26
|
+
softAssertionErrors.push({ message, stack });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getSubjectKey(assertionContext) {
|
|
30
|
+
const obj = assertionContext === null || assertionContext === void 0 ? void 0 : assertionContext._obj;
|
|
31
|
+
const first = Array.isArray(obj) ? obj[0] : obj === null || obj === void 0 ? void 0 : obj[0];
|
|
32
|
+
if (first && typeof first.id === 'string' && first.id.length > 0) {
|
|
33
|
+
return `#${first.id}`;
|
|
34
|
+
}
|
|
35
|
+
const selector = obj === null || obj === void 0 ? void 0 : obj.selector;
|
|
36
|
+
if (typeof selector === 'string' && selector.length > 0) {
|
|
37
|
+
return selector;
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
function toTokenPart(value) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return 'undefined';
|
|
44
|
+
if (value === null)
|
|
45
|
+
return 'null';
|
|
46
|
+
const kind = typeof value;
|
|
47
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
48
|
+
return String(value);
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
}
|
|
52
|
+
catch (_a) {
|
|
53
|
+
return String(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getAssertionToken(assertionContext, args) {
|
|
57
|
+
const subjectKey = getSubjectKey(assertionContext);
|
|
58
|
+
if (!subjectKey)
|
|
59
|
+
return '';
|
|
60
|
+
const expected = args === null || args === void 0 ? void 0 : args[3];
|
|
61
|
+
return `${subjectKey}|${toTokenPart(expected)}`;
|
|
62
|
+
}
|
|
63
|
+
function patchChaiAssertions() {
|
|
64
|
+
var _a;
|
|
65
|
+
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
66
|
+
if (!assertionProto || typeof assertionProto.assert !== 'function')
|
|
67
|
+
return;
|
|
68
|
+
if (!originalChaiAssert)
|
|
69
|
+
originalChaiAssert = assertionProto.assert;
|
|
70
|
+
if (assertionProto.assert === patchedAssertionAssert)
|
|
71
|
+
return;
|
|
72
|
+
assertionProto.assert = patchedAssertionAssert;
|
|
73
|
+
}
|
|
74
|
+
function patchedAssertionAssert(...args) {
|
|
75
|
+
if (!originalChaiAssert)
|
|
23
76
|
return;
|
|
77
|
+
try {
|
|
78
|
+
const result = originalChaiAssert.apply(this, args);
|
|
79
|
+
// Assertion passed — clear any staged failure for this token.
|
|
80
|
+
if (isInSoftTest) {
|
|
81
|
+
const token = getAssertionToken(this, args);
|
|
82
|
+
if (token) {
|
|
83
|
+
retryAssertionFailures.delete(token);
|
|
84
|
+
retryAttemptCount.delete(token);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isInSoftTest)
|
|
91
|
+
throw error;
|
|
92
|
+
const token = getAssertionToken(this, args);
|
|
93
|
+
if (token) {
|
|
94
|
+
// Stage the failure under a stable token so it can be cleared if
|
|
95
|
+
// a later retry succeeds.
|
|
96
|
+
retryAssertionFailures.set(token, {
|
|
97
|
+
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
98
|
+
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
99
|
+
});
|
|
100
|
+
const attempts = (retryAttemptCount.get(token) || 0) + 1;
|
|
101
|
+
retryAttemptCount.set(token, attempts);
|
|
102
|
+
if (attempts <= MAX_RETHROWS) {
|
|
103
|
+
// Rethrow to let Cypress retry the assertion. This gives retriable
|
|
104
|
+
// commands (should/and, retried from get/contains/etc.) a window
|
|
105
|
+
// to eventually pass.
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
// Past the retry budget — swallow so the command "succeeds" and Cypress
|
|
109
|
+
// moves on to the next queued command. The token stays in the Map and
|
|
110
|
+
// will be promoted to softAssertionErrors at finalization.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// No identifiable subject — capture directly (e.g. bare expect() calls
|
|
114
|
+
// in .then() callbacks).
|
|
115
|
+
captureSoftAssertion(error);
|
|
24
116
|
}
|
|
25
|
-
softAssertionErrors.push({
|
|
26
|
-
message,
|
|
27
|
-
stack
|
|
28
|
-
});
|
|
29
117
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Intercept Cypress failures and make assertion failures soft in soft_it() tests.
|
|
32
|
-
*/
|
|
33
118
|
function setupSoftAssertions() {
|
|
119
|
+
patchChaiAssertions();
|
|
34
120
|
if (!activeFailHandler) {
|
|
35
121
|
activeFailHandler = (error) => {
|
|
36
|
-
if (!isInSoftTest)
|
|
122
|
+
if (!isInSoftTest)
|
|
37
123
|
throw error;
|
|
38
|
-
|
|
124
|
+
// Final aggregated error must propagate to fail the test.
|
|
125
|
+
if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
|
|
126
|
+
throw error;
|
|
127
|
+
// Non-assertion command failures (e.g. element not found timeouts)
|
|
128
|
+
// are captured as soft failures.
|
|
39
129
|
captureSoftAssertion(error);
|
|
40
130
|
return false;
|
|
41
131
|
};
|
|
42
132
|
Cypress.on('fail', activeFailHandler);
|
|
43
133
|
}
|
|
44
134
|
}
|
|
45
|
-
function getTestTitle(test) {
|
|
46
|
-
if (test && typeof test.fullTitle === 'function') {
|
|
47
|
-
return test.fullTitle();
|
|
48
|
-
}
|
|
49
|
-
return String((test === null || test === void 0 ? void 0 : test.title) || '');
|
|
50
|
-
}
|
|
51
|
-
function installFinalizer() {
|
|
52
|
-
if (finalizerInstalled) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
afterEach(function () {
|
|
56
|
-
var _a, _b;
|
|
57
|
-
if (!isInSoftTest) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const currentTest = (this.currentTest || this.test);
|
|
61
|
-
if (!currentTest || getTestTitle(currentTest) !== activeSoftTestTitle) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const finalError = finalizeSoftTest();
|
|
65
|
-
if (!finalError) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
currentTest.err = finalError;
|
|
69
|
-
currentTest.state = 'failed';
|
|
70
|
-
const runner = (_b = (_a = Cypress === null || Cypress === void 0 ? void 0 : Cypress.mocha) === null || _a === void 0 ? void 0 : _a.getRunner) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
71
|
-
if (runner && typeof runner.fail === 'function') {
|
|
72
|
-
runner.fail(currentTest, finalError);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
throw finalError;
|
|
76
|
-
});
|
|
77
|
-
finalizerInstalled = true;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Restore original Chai assertion behavior.
|
|
81
|
-
*/
|
|
82
135
|
function restoreAssertions() {
|
|
136
|
+
var _a;
|
|
83
137
|
if (activeFailHandler) {
|
|
84
138
|
Cypress.off('fail', activeFailHandler);
|
|
85
139
|
activeFailHandler = null;
|
|
86
140
|
}
|
|
141
|
+
if (originalChaiAssert) {
|
|
142
|
+
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
143
|
+
if (assertionProto)
|
|
144
|
+
assertionProto.assert = originalChaiAssert;
|
|
145
|
+
}
|
|
87
146
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Report all collected soft assertion failures
|
|
90
|
-
*/
|
|
91
147
|
function buildSoftAssertionError() {
|
|
148
|
+
// Promote any remaining retry-tracked failures.
|
|
149
|
+
for (const entry of retryAssertionFailures.values()) {
|
|
150
|
+
captureSoftAssertion(entry);
|
|
151
|
+
}
|
|
152
|
+
retryAssertionFailures.clear();
|
|
153
|
+
retryAttemptCount.clear();
|
|
92
154
|
if (softAssertionErrors.length > 0) {
|
|
93
155
|
const errorMessages = softAssertionErrors
|
|
94
156
|
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
@@ -100,9 +162,8 @@ function buildSoftAssertionError() {
|
|
|
100
162
|
'='.repeat(80),
|
|
101
163
|
errorMessages,
|
|
102
164
|
'='.repeat(80),
|
|
103
|
-
''
|
|
165
|
+
'',
|
|
104
166
|
].join('\n');
|
|
105
|
-
// Clear errors
|
|
106
167
|
softAssertionErrors = [];
|
|
107
168
|
const error = new Error(finalMessage);
|
|
108
169
|
error.name = 'SoftAssertionError';
|
|
@@ -110,49 +171,50 @@ function buildSoftAssertionError() {
|
|
|
110
171
|
}
|
|
111
172
|
return null;
|
|
112
173
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Cleanup soft assertion state and report accumulated failures.
|
|
115
|
-
*/
|
|
116
174
|
function finalizeSoftTest() {
|
|
117
175
|
isInSoftTest = false;
|
|
118
176
|
restoreAssertions();
|
|
119
|
-
activeSoftTestTitle = null;
|
|
120
177
|
return buildSoftAssertionError();
|
|
121
178
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Cleanup soft assertion state without reporting (used on hard failures).
|
|
124
|
-
*/
|
|
125
179
|
function abortSoftTest() {
|
|
126
180
|
isInSoftTest = false;
|
|
127
181
|
restoreAssertions();
|
|
128
|
-
|
|
182
|
+
retryAssertionFailures.clear();
|
|
183
|
+
retryAttemptCount.clear();
|
|
129
184
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Create a soft_it variant from a Mocha it function.
|
|
132
|
-
*/
|
|
133
185
|
function createSoftIt(baseIt) {
|
|
134
|
-
installFinalizer();
|
|
135
186
|
return function (title, fn) {
|
|
136
187
|
return baseIt(title, function () {
|
|
137
188
|
isInSoftTest = true;
|
|
138
189
|
softAssertionErrors = [];
|
|
139
|
-
|
|
190
|
+
retryAssertionFailures.clear();
|
|
191
|
+
retryAttemptCount.clear();
|
|
140
192
|
setupSoftAssertions();
|
|
193
|
+
let result;
|
|
141
194
|
try {
|
|
142
|
-
|
|
143
|
-
if (result && typeof result.then === 'function') {
|
|
144
|
-
return result
|
|
145
|
-
.catch((error) => {
|
|
146
|
-
abortSoftTest();
|
|
147
|
-
throw error;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return result;
|
|
195
|
+
result = fn.call(this);
|
|
151
196
|
}
|
|
152
197
|
catch (error) {
|
|
153
198
|
abortSoftTest();
|
|
154
199
|
throw error;
|
|
155
200
|
}
|
|
201
|
+
// Finalize from inside the test chain (not from hooks) so Cypress counts
|
|
202
|
+
// the failure on the test itself.
|
|
203
|
+
return cy.then(() => {
|
|
204
|
+
return Cypress.Promise.resolve(result)
|
|
205
|
+
.catch((error) => {
|
|
206
|
+
if (isInSoftTest) {
|
|
207
|
+
abortSoftTest();
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
})
|
|
211
|
+
.then(() => {
|
|
212
|
+
const finalError = finalizeSoftTest();
|
|
213
|
+
if (finalError) {
|
|
214
|
+
throw finalError;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
156
218
|
});
|
|
157
219
|
};
|
|
158
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koenvanbelle/cypress-soft-assertions",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A Cypress plugin that provides soft_it() for soft assertions - all assertions continue on failure and are reported together",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|