@koenvanbelle/cypress-soft-assertions 2.0.0 → 2.1.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.
- package/README.md +9 -0
- package/dist/index.js +89 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,6 +158,15 @@ SOFT ASSERTION FAILURES (3 failed):
|
|
|
158
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
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
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 }
|
|
165
|
+
|
|
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.
|
|
161
170
|
|
|
162
171
|
## License
|
|
163
172
|
|
package/dist/index.js
CHANGED
|
@@ -9,15 +9,26 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
let softAssertionErrors = [];
|
|
11
11
|
let retryAssertionFailures = new Map();
|
|
12
|
-
let
|
|
12
|
+
let retryFirstSeen = new Map();
|
|
13
13
|
let isInSoftTest = false;
|
|
14
14
|
let activeFailHandler = null;
|
|
15
15
|
let originalChaiAssert = null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
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
|
+
}
|
|
21
32
|
function captureSoftAssertion(error) {
|
|
22
33
|
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
23
34
|
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
@@ -53,6 +64,29 @@ function toTokenPart(value) {
|
|
|
53
64
|
return String(value);
|
|
54
65
|
}
|
|
55
66
|
}
|
|
67
|
+
function getRetryableCommandId() {
|
|
68
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
69
|
+
try {
|
|
70
|
+
const current = cy.state('current');
|
|
71
|
+
if (!current)
|
|
72
|
+
return '';
|
|
73
|
+
// In Cypress 15, inside a .should()/.and() callback, cy.state('current')
|
|
74
|
+
// points to the parent command (e.g. 'wrap', 'window', 'get'). The
|
|
75
|
+
// 'followedByShouldCallback' attribute is set to true, and
|
|
76
|
+
// 'currentAssertionCommand' points to the should/and command object.
|
|
77
|
+
const assertionCmd = (_a = current.get) === null || _a === void 0 ? void 0 : _a.call(current, 'currentAssertionCommand');
|
|
78
|
+
if (assertionCmd) {
|
|
79
|
+
const id = (_d = (_c = (_b = assertionCmd.get) === null || _b === void 0 ? void 0 : _b.call(assertionCmd, 'id')) !== null && _c !== void 0 ? _c : assertionCmd.id) !== null && _d !== void 0 ? _d : '';
|
|
80
|
+
return id ? String(id) : '';
|
|
81
|
+
}
|
|
82
|
+
if ((_e = current.get) === null || _e === void 0 ? void 0 : _e.call(current, 'followedByShouldCallback')) {
|
|
83
|
+
const id = (_g = (_f = current.get) === null || _f === void 0 ? void 0 : _f.call(current, 'id')) !== null && _g !== void 0 ? _g : '';
|
|
84
|
+
return id ? String(id) : '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch ( /* ignore */_h) { /* ignore */ }
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
56
90
|
function getAssertionToken(assertionContext, args) {
|
|
57
91
|
const subjectKey = getSubjectKey(assertionContext);
|
|
58
92
|
if (!subjectKey)
|
|
@@ -81,7 +115,14 @@ function patchedAssertionAssert(...args) {
|
|
|
81
115
|
const token = getAssertionToken(this, args);
|
|
82
116
|
if (token) {
|
|
83
117
|
retryAssertionFailures.delete(token);
|
|
84
|
-
|
|
118
|
+
retryFirstSeen.delete(token);
|
|
119
|
+
}
|
|
120
|
+
// Also clear command-based fallback tokens for this command
|
|
121
|
+
const retryableCid = getRetryableCommandId();
|
|
122
|
+
if (retryableCid) {
|
|
123
|
+
const fallbackToken = `__cmd__|${retryableCid}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
|
|
124
|
+
retryAssertionFailures.delete(fallbackToken);
|
|
125
|
+
retryFirstSeen.delete(fallbackToken);
|
|
85
126
|
}
|
|
86
127
|
}
|
|
87
128
|
return result;
|
|
@@ -97,21 +138,48 @@ function patchedAssertionAssert(...args) {
|
|
|
97
138
|
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
98
139
|
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
99
140
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (!retryFirstSeen.has(token)) {
|
|
143
|
+
retryFirstSeen.set(token, now);
|
|
144
|
+
}
|
|
145
|
+
const elapsed = now - retryFirstSeen.get(token);
|
|
146
|
+
const timeout = getEffectiveTimeout();
|
|
147
|
+
// Use 75% of the timeout as the retry window. This ensures the plugin
|
|
148
|
+
// swallows the error before Cypress's own timeout fires (which would
|
|
149
|
+
// route through the fail handler and prevent finalization).
|
|
150
|
+
if (elapsed < timeout * 0.75) {
|
|
151
|
+
// Still within the command timeout window — rethrow to let Cypress
|
|
152
|
+
// retry the assertion. If it eventually passes, the token is cleared.
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
// Past the timeout budget — swallow so the command "succeeds" and
|
|
156
|
+
// Cypress moves on to the next queued command. The token stays in the
|
|
157
|
+
// Map and will be promoted to softAssertionErrors at finalization.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// No identifiable subject from the DOM. If we're inside a retryable
|
|
161
|
+
// command (.should() / .and()), derive a token from the command ID so the
|
|
162
|
+
// retry-window logic still applies (e.g. window property assertions).
|
|
163
|
+
const retryableCid = getRetryableCommandId();
|
|
164
|
+
if (retryableCid) {
|
|
165
|
+
const fallbackToken = `__cmd__|${retryableCid}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
|
|
166
|
+
retryAssertionFailures.set(fallbackToken, {
|
|
167
|
+
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
168
|
+
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
169
|
+
});
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (!retryFirstSeen.has(fallbackToken)) {
|
|
172
|
+
retryFirstSeen.set(fallbackToken, now);
|
|
173
|
+
}
|
|
174
|
+
const elapsed = now - retryFirstSeen.get(fallbackToken);
|
|
175
|
+
const timeout = getEffectiveTimeout();
|
|
176
|
+
if (elapsed < timeout * 0.75) {
|
|
106
177
|
throw error;
|
|
107
178
|
}
|
|
108
|
-
// Past
|
|
109
|
-
// moves on to the next queued command. The token stays in the Map and
|
|
110
|
-
// will be promoted to softAssertionErrors at finalization.
|
|
179
|
+
// Past retry budget — swallow and let Cypress move on.
|
|
111
180
|
return;
|
|
112
181
|
}
|
|
113
|
-
//
|
|
114
|
-
// in .then() callbacks).
|
|
182
|
+
// Bare expect() in .then() callbacks — capture directly.
|
|
115
183
|
captureSoftAssertion(error);
|
|
116
184
|
}
|
|
117
185
|
}
|
|
@@ -150,7 +218,7 @@ function buildSoftAssertionError() {
|
|
|
150
218
|
captureSoftAssertion(entry);
|
|
151
219
|
}
|
|
152
220
|
retryAssertionFailures.clear();
|
|
153
|
-
|
|
221
|
+
retryFirstSeen.clear();
|
|
154
222
|
if (softAssertionErrors.length > 0) {
|
|
155
223
|
const errorMessages = softAssertionErrors
|
|
156
224
|
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
@@ -180,7 +248,7 @@ function abortSoftTest() {
|
|
|
180
248
|
isInSoftTest = false;
|
|
181
249
|
restoreAssertions();
|
|
182
250
|
retryAssertionFailures.clear();
|
|
183
|
-
|
|
251
|
+
retryFirstSeen.clear();
|
|
184
252
|
}
|
|
185
253
|
function createSoftIt(baseIt) {
|
|
186
254
|
return function (title, fn) {
|
|
@@ -188,7 +256,7 @@ function createSoftIt(baseIt) {
|
|
|
188
256
|
isInSoftTest = true;
|
|
189
257
|
softAssertionErrors = [];
|
|
190
258
|
retryAssertionFailures.clear();
|
|
191
|
-
|
|
259
|
+
retryFirstSeen.clear();
|
|
192
260
|
setupSoftAssertions();
|
|
193
261
|
let result;
|
|
194
262
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koenvanbelle/cypress-soft-assertions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|