@koenvanbelle/cypress-soft-assertions 1.0.9 → 2.0.1
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 +72 -188
- package/dist/index.js +152 -100
- 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,82 @@ 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
146
|
```
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
```
|
|
242
|
-
|
|
243
|
-
### Use Regular it() for Critical Setup
|
|
244
|
-
|
|
245
|
-
Use regular `it()` for setup steps that must succeed:
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
it('logs in successfully', () => {
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
Use `soft_it()` to group validations of related elements:
|
|
156
|
+
## Important notes
|
|
266
157
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
```
|
|
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.
|
|
161
|
+
- **Timeout-aware retries**: The plugin respects Cypress's `defaultCommandTimeout` and per-command `{ timeout }` overrides. Retriable assertions (`.should()`, `.and()`) are retried for up to 75% of the effective timeout before being captured as soft failures. For slow applications, increase the timeout to give assertions more time to pass:
|
|
162
|
+
```typescript
|
|
163
|
+
// Global: set in cypress.config.ts
|
|
164
|
+
e2e: { defaultCommandTimeout: 10000 }
|
|
275
165
|
|
|
276
|
-
|
|
166
|
+
// Per-command: override on individual commands
|
|
167
|
+
cy.get('.slow-element', { timeout: 15000 }).should('be.visible');
|
|
168
|
+
```
|
|
169
|
+
- **Cypress Studio / Command Log**: When a soft assertion fails definitively, the plugin swallows the error so the test can continue. Cypress treats the command as resolved, which means it may not appear as a failed step in the Cypress Studio command log or may look like the assertion was skipped. The assertions **do** execute and failures **are** captured — they just don't show visually in the command log. Check the final `SoftAssertionError` report for the complete list of failures.
|
|
277
170
|
|
|
278
|
-
|
|
171
|
+
## License
|
|
279
172
|
|
|
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
|
-
```
|
|
173
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -8,114 +8,166 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
let softAssertionErrors = [];
|
|
11
|
+
let retryAssertionFailures = new Map();
|
|
12
|
+
let retryFirstSeen = new Map();
|
|
11
13
|
let isInSoftTest = false;
|
|
12
14
|
let activeFailHandler = null;
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
let originalChaiAssert = null;
|
|
16
|
+
function getEffectiveTimeout() {
|
|
17
|
+
var _a;
|
|
18
|
+
try {
|
|
19
|
+
const current = cy.state('current');
|
|
20
|
+
const perCommand = (_a = current === null || current === void 0 ? void 0 : current.get) === null || _a === void 0 ? void 0 : _a.call(current, 'timeout');
|
|
21
|
+
if (typeof perCommand === 'number')
|
|
22
|
+
return perCommand;
|
|
23
|
+
}
|
|
24
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
25
|
+
try {
|
|
26
|
+
return Cypress.config('defaultCommandTimeout') || 4000;
|
|
27
|
+
}
|
|
28
|
+
catch (_c) {
|
|
29
|
+
return 4000;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
function captureSoftAssertion(error) {
|
|
19
33
|
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
20
34
|
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
21
35
|
const lastEntry = softAssertionErrors[softAssertionErrors.length - 1];
|
|
22
|
-
if (lastEntry
|
|
23
|
-
|
|
36
|
+
if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
|
|
37
|
+
softAssertionErrors.push({ message, stack });
|
|
24
38
|
}
|
|
25
|
-
softAssertionErrors.push({
|
|
26
|
-
message,
|
|
27
|
-
stack
|
|
28
|
-
});
|
|
29
39
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (typeof
|
|
34
|
-
return
|
|
40
|
+
function getSubjectKey(assertionContext) {
|
|
41
|
+
const obj = assertionContext === null || assertionContext === void 0 ? void 0 : assertionContext._obj;
|
|
42
|
+
const first = Array.isArray(obj) ? obj[0] : obj === null || obj === void 0 ? void 0 : obj[0];
|
|
43
|
+
if (first && typeof first.id === 'string' && first.id.length > 0) {
|
|
44
|
+
return `#${first.id}`;
|
|
35
45
|
}
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
46
|
+
const selector = obj === null || obj === void 0 ? void 0 : obj.selector;
|
|
47
|
+
if (typeof selector === 'string' && selector.length > 0) {
|
|
48
|
+
return selector;
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
function toTokenPart(value) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return 'undefined';
|
|
55
|
+
if (value === null)
|
|
56
|
+
return 'null';
|
|
57
|
+
const kind = typeof value;
|
|
58
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
59
|
+
return String(value);
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(value);
|
|
39
62
|
}
|
|
40
|
-
|
|
41
|
-
return String(
|
|
63
|
+
catch (_a) {
|
|
64
|
+
return String(value);
|
|
42
65
|
}
|
|
43
|
-
return String(current.name || ((_a = current.attributes) === null || _a === void 0 ? void 0 : _a.name) || '');
|
|
44
66
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
67
|
+
function getAssertionToken(assertionContext, args) {
|
|
68
|
+
const subjectKey = getSubjectKey(assertionContext);
|
|
69
|
+
if (!subjectKey)
|
|
70
|
+
return '';
|
|
71
|
+
const expected = args === null || args === void 0 ? void 0 : args[3];
|
|
72
|
+
return `${subjectKey}|${toTokenPart(expected)}`;
|
|
48
73
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
74
|
+
function patchChaiAssertions() {
|
|
75
|
+
var _a;
|
|
76
|
+
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
77
|
+
if (!assertionProto || typeof assertionProto.assert !== 'function')
|
|
78
|
+
return;
|
|
79
|
+
if (!originalChaiAssert)
|
|
80
|
+
originalChaiAssert = assertionProto.assert;
|
|
81
|
+
if (assertionProto.assert === patchedAssertionAssert)
|
|
82
|
+
return;
|
|
83
|
+
assertionProto.assert = patchedAssertionAssert;
|
|
84
|
+
}
|
|
85
|
+
function patchedAssertionAssert(...args) {
|
|
86
|
+
if (!originalChaiAssert)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
const result = originalChaiAssert.apply(this, args);
|
|
90
|
+
// Assertion passed — clear any staged failure for this token.
|
|
91
|
+
if (isInSoftTest) {
|
|
92
|
+
const token = getAssertionToken(this, args);
|
|
93
|
+
if (token) {
|
|
94
|
+
retryAssertionFailures.delete(token);
|
|
95
|
+
retryFirstSeen.delete(token);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!isInSoftTest)
|
|
102
|
+
throw error;
|
|
103
|
+
const token = getAssertionToken(this, args);
|
|
104
|
+
if (token) {
|
|
105
|
+
// Stage the failure under a stable token so it can be cleared if
|
|
106
|
+
// a later retry succeeds.
|
|
107
|
+
retryAssertionFailures.set(token, {
|
|
108
|
+
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
109
|
+
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
110
|
+
});
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
if (!retryFirstSeen.has(token)) {
|
|
113
|
+
retryFirstSeen.set(token, now);
|
|
114
|
+
}
|
|
115
|
+
const elapsed = now - retryFirstSeen.get(token);
|
|
116
|
+
const timeout = getEffectiveTimeout();
|
|
117
|
+
// Use 75% of the timeout as the retry window. This ensures the plugin
|
|
118
|
+
// swallows the error before Cypress's own timeout fires (which would
|
|
119
|
+
// route through the fail handler and prevent finalization).
|
|
120
|
+
if (elapsed < timeout * 0.75) {
|
|
121
|
+
// Still within the command timeout window — rethrow to let Cypress
|
|
122
|
+
// retry the assertion. If it eventually passes, the token is cleared.
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
// Past the timeout budget — swallow so the command "succeeds" and
|
|
126
|
+
// Cypress moves on to the next queued command. The token stays in the
|
|
127
|
+
// Map and will be promoted to softAssertionErrors at finalization.
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// No identifiable subject — capture directly (e.g. bare expect() calls
|
|
131
|
+
// in .then() callbacks).
|
|
132
|
+
captureSoftAssertion(error);
|
|
133
|
+
}
|
|
51
134
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Intercept Cypress failures and make assertion failures soft in soft_it() tests.
|
|
54
|
-
*/
|
|
55
135
|
function setupSoftAssertions() {
|
|
136
|
+
patchChaiAssertions();
|
|
56
137
|
if (!activeFailHandler) {
|
|
57
138
|
activeFailHandler = (error) => {
|
|
58
|
-
if (!isInSoftTest)
|
|
139
|
+
if (!isInSoftTest)
|
|
59
140
|
throw error;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (isRetriableAssertionCommand(commandName) && !isRetryTimeoutError(error)) {
|
|
63
|
-
// Let Cypress continue polling/retrying for retriable assertions.
|
|
141
|
+
// Final aggregated error must propagate to fail the test.
|
|
142
|
+
if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
|
|
64
143
|
throw error;
|
|
65
|
-
|
|
144
|
+
// Non-assertion command failures (e.g. element not found timeouts)
|
|
145
|
+
// are captured as soft failures.
|
|
66
146
|
captureSoftAssertion(error);
|
|
67
147
|
return false;
|
|
68
148
|
};
|
|
69
149
|
Cypress.on('fail', activeFailHandler);
|
|
70
150
|
}
|
|
71
151
|
}
|
|
72
|
-
function getTestTitle(test) {
|
|
73
|
-
if (test && typeof test.fullTitle === 'function') {
|
|
74
|
-
return test.fullTitle();
|
|
75
|
-
}
|
|
76
|
-
return String((test === null || test === void 0 ? void 0 : test.title) || '');
|
|
77
|
-
}
|
|
78
|
-
function installFinalizer() {
|
|
79
|
-
if (finalizerInstalled) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
afterEach(function () {
|
|
83
|
-
var _a, _b;
|
|
84
|
-
if (!isInSoftTest) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const currentTest = (this.currentTest || this.test);
|
|
88
|
-
if (!currentTest || getTestTitle(currentTest) !== activeSoftTestTitle) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const finalError = finalizeSoftTest();
|
|
92
|
-
if (!finalError) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
currentTest.err = finalError;
|
|
96
|
-
currentTest.state = 'failed';
|
|
97
|
-
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);
|
|
98
|
-
if (runner && typeof runner.fail === 'function') {
|
|
99
|
-
runner.fail(currentTest, finalError);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
throw finalError;
|
|
103
|
-
});
|
|
104
|
-
finalizerInstalled = true;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Restore original Chai assertion behavior.
|
|
108
|
-
*/
|
|
109
152
|
function restoreAssertions() {
|
|
153
|
+
var _a;
|
|
110
154
|
if (activeFailHandler) {
|
|
111
155
|
Cypress.off('fail', activeFailHandler);
|
|
112
156
|
activeFailHandler = null;
|
|
113
157
|
}
|
|
158
|
+
if (originalChaiAssert) {
|
|
159
|
+
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
160
|
+
if (assertionProto)
|
|
161
|
+
assertionProto.assert = originalChaiAssert;
|
|
162
|
+
}
|
|
114
163
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Report all collected soft assertion failures
|
|
117
|
-
*/
|
|
118
164
|
function buildSoftAssertionError() {
|
|
165
|
+
// Promote any remaining retry-tracked failures.
|
|
166
|
+
for (const entry of retryAssertionFailures.values()) {
|
|
167
|
+
captureSoftAssertion(entry);
|
|
168
|
+
}
|
|
169
|
+
retryAssertionFailures.clear();
|
|
170
|
+
retryFirstSeen.clear();
|
|
119
171
|
if (softAssertionErrors.length > 0) {
|
|
120
172
|
const errorMessages = softAssertionErrors
|
|
121
173
|
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
@@ -127,9 +179,8 @@ function buildSoftAssertionError() {
|
|
|
127
179
|
'='.repeat(80),
|
|
128
180
|
errorMessages,
|
|
129
181
|
'='.repeat(80),
|
|
130
|
-
''
|
|
182
|
+
'',
|
|
131
183
|
].join('\n');
|
|
132
|
-
// Clear errors
|
|
133
184
|
softAssertionErrors = [];
|
|
134
185
|
const error = new Error(finalMessage);
|
|
135
186
|
error.name = 'SoftAssertionError';
|
|
@@ -137,49 +188,50 @@ function buildSoftAssertionError() {
|
|
|
137
188
|
}
|
|
138
189
|
return null;
|
|
139
190
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Cleanup soft assertion state and report accumulated failures.
|
|
142
|
-
*/
|
|
143
191
|
function finalizeSoftTest() {
|
|
144
192
|
isInSoftTest = false;
|
|
145
193
|
restoreAssertions();
|
|
146
|
-
activeSoftTestTitle = null;
|
|
147
194
|
return buildSoftAssertionError();
|
|
148
195
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Cleanup soft assertion state without reporting (used on hard failures).
|
|
151
|
-
*/
|
|
152
196
|
function abortSoftTest() {
|
|
153
197
|
isInSoftTest = false;
|
|
154
198
|
restoreAssertions();
|
|
155
|
-
|
|
199
|
+
retryAssertionFailures.clear();
|
|
200
|
+
retryFirstSeen.clear();
|
|
156
201
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Create a soft_it variant from a Mocha it function.
|
|
159
|
-
*/
|
|
160
202
|
function createSoftIt(baseIt) {
|
|
161
|
-
installFinalizer();
|
|
162
203
|
return function (title, fn) {
|
|
163
204
|
return baseIt(title, function () {
|
|
164
205
|
isInSoftTest = true;
|
|
165
206
|
softAssertionErrors = [];
|
|
166
|
-
|
|
207
|
+
retryAssertionFailures.clear();
|
|
208
|
+
retryFirstSeen.clear();
|
|
167
209
|
setupSoftAssertions();
|
|
210
|
+
let result;
|
|
168
211
|
try {
|
|
169
|
-
|
|
170
|
-
if (result && typeof result.then === 'function') {
|
|
171
|
-
return result
|
|
172
|
-
.catch((error) => {
|
|
173
|
-
abortSoftTest();
|
|
174
|
-
throw error;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return result;
|
|
212
|
+
result = fn.call(this);
|
|
178
213
|
}
|
|
179
214
|
catch (error) {
|
|
180
215
|
abortSoftTest();
|
|
181
216
|
throw error;
|
|
182
217
|
}
|
|
218
|
+
// Finalize from inside the test chain (not from hooks) so Cypress counts
|
|
219
|
+
// the failure on the test itself.
|
|
220
|
+
return cy.then(() => {
|
|
221
|
+
return Cypress.Promise.resolve(result)
|
|
222
|
+
.catch((error) => {
|
|
223
|
+
if (isInSoftTest) {
|
|
224
|
+
abortSoftTest();
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
})
|
|
228
|
+
.then(() => {
|
|
229
|
+
const finalError = finalizeSoftTest();
|
|
230
|
+
if (finalError) {
|
|
231
|
+
throw finalError;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
183
235
|
});
|
|
184
236
|
};
|
|
185
237
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koenvanbelle/cypress-soft-assertions",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
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",
|