@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 +92 -25
- package/dist/utils.js +3 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
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
|
-
|
|
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:
|
|
298
|
-
attempts:
|
|
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
|
-
|
|
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.
|
|
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",
|