@koenvanbelle/cypress-soft-assertions 1.0.9 → 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.
Files changed (3) hide show
  1. package/README.md +64 -189
  2. package/dist/index.js +135 -100
  3. 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()` - a drop-in replacement for `it()` that makes all assertions soft. Assertions continue execution on failure, and all failures are aggregated and reported at the end of the test.
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
- ## Overview
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 - 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 automatically aggregated and reported
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
- or using Yarn:
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
- Add the import to your Cypress support file. This registers the `soft_it()` function globally.
22
+ ### 1. Import the plugin
33
23
 
34
- **For TypeScript projects** - Edit `cypress/support/e2e.ts` (or `cypress/support/commands.ts`):
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
- **For JavaScript projects** - Edit `cypress/support/e2e.js` (or `cypress/support/commands.js`):
30
+ Or for JavaScript:
41
31
 
42
32
  ```javascript
43
33
  require('@koenvanbelle/cypress-soft-assertions');
44
34
  ```
45
35
 
46
- ### Step 2: Add Type Definitions (TypeScript only)
36
+ ### 2. Add type definitions (TypeScript only)
47
37
 
48
- If you're using TypeScript, ensure your `tsconfig.json` includes the plugin types:
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
- Alternatively, add a reference directive at the top of your test files:
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('https://example.com/product/123');
72
-
73
- // All these assertions will run even if some fail
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
- Supported Assertion Types
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
- ### Mixing soft_it() with Regular it()
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
- ### Multiple Tests
95
+ ### Mixing `soft_it()` with regular `it()`
136
96
 
137
- You can mix `soft_it()` with regular `it()` tests:
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 - stops on first failure
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 - all assertions run
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
- ### Using soft_it.only and soft_it.skip
117
+ ### `soft_it.only` and `soft_it.skip`
165
118
 
166
- Just like regular `it()`:
119
+ Works just like regular `it()`:
167
120
 
168
121
  ```typescript
169
122
  describe('Test Suite', () => {
170
- // Run only this test
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
- // Skip this test
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 It Works
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
- Failures are reported at the end of the test after all Cypress commands complete. The test will be marked as failed, and all assertion failures will be listed together.
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
- ### Regular vs Soft Tests
142
+ ## Error output
210
143
 
211
- - **Regular `it()`**: Stops at first assertion failure
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
- ### 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
- ### Group Related Validations
156
+ ## Important notes
264
157
 
265
- Use `soft_it()` to group validations of related elements:
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
- ```typescript
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,114 +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 activeSoftTestTitle = null;
14
- let finalizerInstalled = false;
15
- /**
16
- * Track a soft assertion failure so it can be reported at test end.
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 && lastEntry.message === message && lastEntry.stack === stack) {
23
- return;
25
+ if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
26
+ softAssertionErrors.push({ message, stack });
24
27
  }
25
- softAssertionErrors.push({
26
- message,
27
- stack
28
- });
29
28
  }
30
- function getCurrentCommandName() {
31
- var _a;
32
- const state = cy === null || cy === void 0 ? void 0 : cy.state;
33
- if (typeof state !== 'function') {
34
- return '';
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}`;
35
34
  }
36
- const current = state('current');
37
- if (!current) {
38
- return '';
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);
39
51
  }
40
- if (typeof current.get === 'function') {
41
- return String(current.get('name') || '');
52
+ catch (_a) {
53
+ return String(value);
42
54
  }
43
- return String(current.name || ((_a = current.attributes) === null || _a === void 0 ? void 0 : _a.name) || '');
44
55
  }
45
- function isRetryTimeoutError(error) {
46
- const message = String((error === null || error === void 0 ? void 0 : error.message) || '');
47
- return /Timed out retrying after/i.test(message);
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)}`;
48
62
  }
49
- function isRetriableAssertionCommand(commandName) {
50
- return commandName === 'should' || commandName === 'and';
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)
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);
116
+ }
51
117
  }
52
- /**
53
- * Intercept Cypress failures and make assertion failures soft in soft_it() tests.
54
- */
55
118
  function setupSoftAssertions() {
119
+ patchChaiAssertions();
56
120
  if (!activeFailHandler) {
57
121
  activeFailHandler = (error) => {
58
- if (!isInSoftTest) {
122
+ if (!isInSoftTest)
59
123
  throw error;
60
- }
61
- const commandName = getCurrentCommandName();
62
- if (isRetriableAssertionCommand(commandName) && !isRetryTimeoutError(error)) {
63
- // Let Cypress continue polling/retrying for retriable assertions.
124
+ // Final aggregated error must propagate to fail the test.
125
+ if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
64
126
  throw error;
65
- }
127
+ // Non-assertion command failures (e.g. element not found timeouts)
128
+ // are captured as soft failures.
66
129
  captureSoftAssertion(error);
67
130
  return false;
68
131
  };
69
132
  Cypress.on('fail', activeFailHandler);
70
133
  }
71
134
  }
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
135
  function restoreAssertions() {
136
+ var _a;
110
137
  if (activeFailHandler) {
111
138
  Cypress.off('fail', activeFailHandler);
112
139
  activeFailHandler = null;
113
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
+ }
114
146
  }
115
- /**
116
- * Report all collected soft assertion failures
117
- */
118
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();
119
154
  if (softAssertionErrors.length > 0) {
120
155
  const errorMessages = softAssertionErrors
121
156
  .map((entry, index) => ` ${index + 1}. ${entry.message}`)
@@ -127,9 +162,8 @@ function buildSoftAssertionError() {
127
162
  '='.repeat(80),
128
163
  errorMessages,
129
164
  '='.repeat(80),
130
- ''
165
+ '',
131
166
  ].join('\n');
132
- // Clear errors
133
167
  softAssertionErrors = [];
134
168
  const error = new Error(finalMessage);
135
169
  error.name = 'SoftAssertionError';
@@ -137,49 +171,50 @@ function buildSoftAssertionError() {
137
171
  }
138
172
  return null;
139
173
  }
140
- /**
141
- * Cleanup soft assertion state and report accumulated failures.
142
- */
143
174
  function finalizeSoftTest() {
144
175
  isInSoftTest = false;
145
176
  restoreAssertions();
146
- activeSoftTestTitle = null;
147
177
  return buildSoftAssertionError();
148
178
  }
149
- /**
150
- * Cleanup soft assertion state without reporting (used on hard failures).
151
- */
152
179
  function abortSoftTest() {
153
180
  isInSoftTest = false;
154
181
  restoreAssertions();
155
- activeSoftTestTitle = null;
182
+ retryAssertionFailures.clear();
183
+ retryAttemptCount.clear();
156
184
  }
157
- /**
158
- * Create a soft_it variant from a Mocha it function.
159
- */
160
185
  function createSoftIt(baseIt) {
161
- installFinalizer();
162
186
  return function (title, fn) {
163
187
  return baseIt(title, function () {
164
188
  isInSoftTest = true;
165
189
  softAssertionErrors = [];
166
- activeSoftTestTitle = getTestTitle(this.currentTest || this.test);
190
+ retryAssertionFailures.clear();
191
+ retryAttemptCount.clear();
167
192
  setupSoftAssertions();
193
+ let result;
168
194
  try {
169
- const result = fn.call(this);
170
- if (result && typeof result.then === 'function') {
171
- return result
172
- .catch((error) => {
173
- abortSoftTest();
174
- throw error;
175
- });
176
- }
177
- return result;
195
+ result = fn.call(this);
178
196
  }
179
197
  catch (error) {
180
198
  abortSoftTest();
181
199
  throw error;
182
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
+ });
183
218
  });
184
219
  };
185
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koenvanbelle/cypress-soft-assertions",
3
- "version": "1.0.9",
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",