@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.
Files changed (3) hide show
  1. package/README.md +72 -188
  2. package/dist/index.js +152 -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,82 @@ 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
146
  ```
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
- ```
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
264
-
265
- Use `soft_it()` to group validations of related elements:
156
+ ## Important notes
266
157
 
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
- ```
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
- ## Error Output Format
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
- When soft assertions fail, you receive a formatted error report:
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 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
+ 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 && lastEntry.message === message && lastEntry.stack === stack) {
23
- return;
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 getCurrentCommandName() {
31
- var _a;
32
- const state = cy === null || cy === void 0 ? void 0 : cy.state;
33
- if (typeof state !== 'function') {
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 current = state('current');
37
- if (!current) {
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
- if (typeof current.get === 'function') {
41
- return String(current.get('name') || '');
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 isRetryTimeoutError(error) {
46
- const message = String((error === null || error === void 0 ? void 0 : error.message) || '');
47
- return /Timed out retrying after/i.test(message);
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 isRetriableAssertionCommand(commandName) {
50
- return commandName === 'should' || commandName === 'and';
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
- const commandName = getCurrentCommandName();
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
- activeSoftTestTitle = null;
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
- activeSoftTestTitle = getTestTitle(this.currentTest || this.test);
207
+ retryAssertionFailures.clear();
208
+ retryFirstSeen.clear();
167
209
  setupSoftAssertions();
210
+ let result;
168
211
  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;
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": "1.0.9",
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",