@koenvanbelle/cypress-soft-assertions 2.4.2 → 2.4.4

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/dist/index.js CHANGED
@@ -81,7 +81,13 @@ function patchChaiAssertions() {
81
81
  assertionProto.assert = patchedAssertionAssert;
82
82
  }
83
83
  function resolveStableToken(assertionContext, args) {
84
- return (0, utils_1.resolveToken)((0, utils_1.getAssertionToken)(assertionContext, args), getRetryableCommandId(), args);
84
+ const assertionToken = (0, utils_1.getAssertionToken)(assertionContext, args);
85
+ if (assertionToken)
86
+ return assertionToken;
87
+ const commandId = getRetryableCommandId();
88
+ if (commandId)
89
+ return (0, utils_1.resolveToken)('', commandId, args);
90
+ return '';
85
91
  }
86
92
  function isRunningHookContext() {
87
93
  var _a;
@@ -95,23 +101,24 @@ function isRunningHookContext() {
95
101
  }
96
102
  }
97
103
  function patchedAssertionAssert(...args) {
104
+ var _a;
98
105
  if (!originalChaiAssert)
99
106
  return;
107
+ // Fast path for non-soft tests: avoid try/catch and token logic entirely.
108
+ if (!isInSoftTest) {
109
+ return originalChaiAssert.apply(this, args);
110
+ }
100
111
  try {
101
112
  const result = originalChaiAssert.apply(this, args);
102
113
  // Assertion passed — clear any staged failure for this token.
103
- if (isInSoftTest) {
104
- const token = resolveStableToken(this, args);
105
- if (token) {
106
- retryAssertionFailures.delete(token);
107
- retryFirstSeen.delete(token);
108
- }
114
+ const token = resolveStableToken(this, args);
115
+ if (token) {
116
+ retryAssertionFailures.delete(token);
117
+ retryFirstSeen.delete(token);
109
118
  }
110
119
  return result;
111
120
  }
112
121
  catch (error) {
113
- if (!isInSoftTest)
114
- throw error;
115
122
  if (isRunningHookContext())
116
123
  throw error;
117
124
  const errorEntry = {
@@ -120,22 +127,30 @@ function patchedAssertionAssert(...args) {
120
127
  };
121
128
  const token = resolveStableToken(this, args);
122
129
  if (token) {
123
- // Track when we first saw this failure.
124
- if (!retryFirstSeen.has(token)) {
125
- retryFirstSeen.set(token, Date.now());
126
- }
127
130
  // Stage the failure so it can be cleared if a later retry succeeds.
128
131
  retryAssertionFailures.set(token, errorEntry);
129
- // Check if the retry window has expired. Swallow slightly before
130
- // Cypress's own timeout to prevent it from firing the fail event.
131
- // Cypress retries every ~50ms, so subtracting 100ms ensures we
132
- // catch at least 1-2 more retries before the deadline.
133
- const elapsed = Date.now() - retryFirstSeen.get(token);
132
+ const current = cy.state('current');
133
+ const wallClockStartedAt = (_a = current === null || current === void 0 ? void 0 : current.get) === null || _a === void 0 ? void 0 : _a.call(current, 'wallClockStartedAt');
134
134
  const timeout = getEffectiveTimeout();
135
- const swallowAt = Math.max(timeout - 100, timeout * 0.9);
136
- if (elapsed < swallowAt) {
137
- // Still within the retry window rethrow so Cypress retries.
138
- throw error;
135
+ if (typeof wallClockStartedAt === 'number') {
136
+ const totalElapsed = Date.now() - wallClockStartedAt;
137
+ // Swallow only when very close to the actual command timeout.
138
+ // A 20ms buffer maximizes retry attempts while still preventing
139
+ // Cypress's global fail handler from aborting the test queue.
140
+ if (totalElapsed < timeout - 20) {
141
+ throw error;
142
+ }
143
+ }
144
+ else {
145
+ // Fallback for cases where wallClockStartedAt is missing.
146
+ if (!retryFirstSeen.has(token)) {
147
+ retryFirstSeen.set(token, Date.now());
148
+ }
149
+ const elapsed = Date.now() - retryFirstSeen.get(token);
150
+ const swallowAt = Math.max(timeout - 100, timeout * 0.9);
151
+ if (elapsed < swallowAt) {
152
+ throw error;
153
+ }
139
154
  }
140
155
  // Retry window expired. Swallow the error: Cypress considers the
141
156
  // assertion "passed", the command resolves, and the queue continues.
@@ -161,6 +176,12 @@ function setupSoftAssertions() {
161
176
  // Final aggregated error must propagate to fail the test.
162
177
  if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
163
178
  throw error;
179
+ const runnable = cy.state('runnable');
180
+ const retryStatusInfo = getRetryStatusInfo(runnable);
181
+ if (retryStatusInfo.shouldAttemptsContinue) {
182
+ abortSoftTest();
183
+ throw error;
184
+ }
164
185
  // Check if this is an assertion error that was already handled by
165
186
  // patchedAssertionAssert (swallowed after timeout). In that case
166
187
  // the fail handler should NOT fire. But for non-assertion command
@@ -220,6 +241,41 @@ function abortSoftTest() {
220
241
  retryAssertionFailures.clear();
221
242
  retryFirstSeen.clear();
222
243
  }
244
+ function getRetryStatusInfo(test) {
245
+ var _a;
246
+ const currentRetry = typeof (test === null || test === void 0 ? void 0 : test.currentRetry) === 'function'
247
+ ? test.currentRetry()
248
+ : typeof Cypress.currentRetry === 'number'
249
+ ? Cypress.currentRetry
250
+ : 0;
251
+ const maxRetries = typeof (test === null || test === void 0 ? void 0 : test.retries) === 'function'
252
+ ? test.retries()
253
+ : typeof (test === null || test === void 0 ? void 0 : test._retries) === 'number'
254
+ ? test._retries
255
+ : typeof Cypress.getTestRetries === 'function'
256
+ ? (_a = Cypress.getTestRetries()) !== null && _a !== void 0 ? _a : 0
257
+ : 0;
258
+ return {
259
+ attempts: currentRetry + 1,
260
+ shouldAttemptsContinue: currentRetry < maxRetries,
261
+ };
262
+ }
263
+ function finalizeSoftTestInQueue(expectsSoftFailure) {
264
+ return cy.then(() => {
265
+ if (!isInSoftTest)
266
+ return;
267
+ const finalError = finalizeSoftTest();
268
+ if (expectsSoftFailure) {
269
+ if (!finalError) {
270
+ throw new Error('Expected SoftAssertionError but soft_it.expectFailure test completed without one.');
271
+ }
272
+ return;
273
+ }
274
+ if (finalError) {
275
+ throw finalError;
276
+ }
277
+ });
278
+ }
223
279
  function createSoftIt(baseIt, options) {
224
280
  return function (title, fn) {
225
281
  return baseIt(title, function () {
@@ -230,7 +286,14 @@ function createSoftIt(baseIt, options) {
230
286
  retryFirstSeen.clear();
231
287
  setupSoftAssertions();
232
288
  try {
233
- return fn.call(this);
289
+ const result = fn.call(this);
290
+ if (result && typeof result.then === 'function' && !Cypress.isCy(result)) {
291
+ return Cypress.Promise.resolve(result).then(() => {
292
+ finalizeSoftTestInQueue(expectSoftFailureCurrentSoftTest);
293
+ });
294
+ }
295
+ finalizeSoftTestInQueue(expectSoftFailureCurrentSoftTest);
296
+ return result;
234
297
  }
235
298
  catch (error) {
236
299
  abortSoftTest();
@@ -291,11 +354,15 @@ afterEach(function () {
291
354
  if (finalError) {
292
355
  const test = this.currentTest;
293
356
  if (test) {
357
+ const retryStatusInfo = getRetryStatusInfo(test);
358
+ if (retryStatusInfo.shouldAttemptsContinue) {
359
+ throw finalError;
360
+ }
294
361
  test.err = finalError;
295
362
  test._cypressTestStatusInfo = {
296
363
  outerStatus: 'failed',
297
- shouldAttemptsContinue: false,
298
- attempts: typeof test.currentRetry === 'function' ? test.currentRetry() + 1 : 1,
364
+ shouldAttemptsContinue: retryStatusInfo.shouldAttemptsContinue,
365
+ attempts: retryStatusInfo.attempts,
299
366
  strategy: 'detect-flake-and-pass-on-threshold',
300
367
  };
301
368
  const prevAttempts = Array.isArray(test.prevAttempts) ? test.prevAttempts : [];
package/dist/utils.js CHANGED
@@ -84,10 +84,11 @@ function appendUniqueError(errors, message, stack) {
84
84
  */
85
85
  function mergeRetryFailures(softErrors, retryFailures) {
86
86
  const result = [...softErrors];
87
+ const seenMessages = new Set(result.map(e => e.message));
87
88
  for (const entry of retryFailures.values()) {
88
- const isDuplicate = result.some(e => e.message === entry.message);
89
- if (!isDuplicate) {
89
+ if (!seenMessages.has(entry.message)) {
90
90
  result.push(entry);
91
+ seenMessages.add(entry.message);
91
92
  }
92
93
  }
93
94
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koenvanbelle/cypress-soft-assertions",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
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",