@koenvanbelle/cypress-soft-assertions 2.1.1 → 2.3.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.
- package/README.md +81 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +101 -119
- package/dist/utils.d.ts +59 -0
- package/dist/utils.js +130 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -168,6 +168,87 @@ SOFT ASSERTION FAILURES (3 failed):
|
|
|
168
168
|
```
|
|
169
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.
|
|
170
170
|
|
|
171
|
+
## Compatibility: cypress-translation-checker
|
|
172
|
+
|
|
173
|
+
`@koenvanbelle/cypress-soft-assertions` is compatible with `cypress-translation-checker` (verified with `cypress-translation-checker@1.3.4` and Cypress 15).
|
|
174
|
+
|
|
175
|
+
### Install
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm install --save-dev @koenvanbelle/cypress-soft-assertions cypress-translation-checker
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 1. Register translation-checker tasks in Cypress config
|
|
182
|
+
|
|
183
|
+
`cypress-translation-checker` uses Cypress tasks to persist collected page results. Add these in `setupNodeEvents`.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { defineConfig } from 'cypress';
|
|
187
|
+
|
|
188
|
+
const translationResults = new Map<string, { url: string; errors: any[]; testContext: string }>();
|
|
189
|
+
|
|
190
|
+
export default defineConfig({
|
|
191
|
+
e2e: {
|
|
192
|
+
setupNodeEvents(on, config) {
|
|
193
|
+
on('task', {
|
|
194
|
+
storeTranslationResult({
|
|
195
|
+
url,
|
|
196
|
+
errors,
|
|
197
|
+
testContext,
|
|
198
|
+
}: {
|
|
199
|
+
url: string;
|
|
200
|
+
errors: any[];
|
|
201
|
+
testContext: string;
|
|
202
|
+
}) {
|
|
203
|
+
translationResults.set(url, { url, errors, testContext });
|
|
204
|
+
return null;
|
|
205
|
+
},
|
|
206
|
+
getTranslationResults() {
|
|
207
|
+
return Array.from(translationResults.values());
|
|
208
|
+
},
|
|
209
|
+
clearTranslationResults() {
|
|
210
|
+
translationResults.clear();
|
|
211
|
+
return null;
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return config;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 2. Import both plugins in support file
|
|
222
|
+
|
|
223
|
+
Enable soft assertions and then enable automatic translation checks.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import '@koenvanbelle/cypress-soft-assertions';
|
|
227
|
+
import { enableAutoTranslationCheck } from 'cypress-translation-checker/commands';
|
|
228
|
+
|
|
229
|
+
enableAutoTranslationCheck({
|
|
230
|
+
waitTime: 500,
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 3. Write tests normally with `soft_it`
|
|
235
|
+
|
|
236
|
+
Soft assertion failures are still aggregated into one `SoftAssertionError` at test end.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
soft_it('works with automatic translation checks', () => {
|
|
240
|
+
cy.visit('/');
|
|
241
|
+
cy.get('h1').should('have.text', 'Expected title');
|
|
242
|
+
cy.get('#status').should('have.text', 'Ready');
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Notes
|
|
247
|
+
|
|
248
|
+
- If translation-checker tasks are missing, Cypress will fail with unknown task errors.
|
|
249
|
+
- Both plugins register hooks/events, but they can run together safely with the setup above.
|
|
250
|
+
- Keep translation-checker in non-invasive mode (`failOnError: false` in auto mode) so functional assertions remain the source of test pass/fail behavior.
|
|
251
|
+
|
|
171
252
|
## License
|
|
172
253
|
|
|
173
254
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* Provides soft_it() function that wraps Cypress tests to make all assertions soft.
|
|
5
5
|
* Assertions don't stop execution on failure - they continue and all failures are
|
|
6
6
|
* aggregated and reported at the end of the test.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Chai's Assertion.prototype.assert is patched to intercept assertion failures.
|
|
10
|
+
* - During retries, failures are rethrown so Cypress can retry.
|
|
11
|
+
* - Once the retry window expires, the error is swallowed (not rethrown).
|
|
12
|
+
* This lets Cypress consider the command "passed" and continue the queue.
|
|
13
|
+
* - A fail handler catches non-assertion errors (e.g. element-not-found timeouts).
|
|
14
|
+
* - An afterEach hook finalizes: aggregates all failures and reports them.
|
|
7
15
|
*/
|
|
8
16
|
declare global {
|
|
9
17
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,14 +5,26 @@
|
|
|
5
5
|
* Provides soft_it() function that wraps Cypress tests to make all assertions soft.
|
|
6
6
|
* Assertions don't stop execution on failure - they continue and all failures are
|
|
7
7
|
* aggregated and reported at the end of the test.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Chai's Assertion.prototype.assert is patched to intercept assertion failures.
|
|
11
|
+
* - During retries, failures are rethrown so Cypress can retry.
|
|
12
|
+
* - Once the retry window expires, the error is swallowed (not rethrown).
|
|
13
|
+
* This lets Cypress consider the command "passed" and continue the queue.
|
|
14
|
+
* - A fail handler catches non-assertion errors (e.g. element-not-found timeouts).
|
|
15
|
+
* - An afterEach hook finalizes: aggregates all failures and reports them.
|
|
8
16
|
*/
|
|
9
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const utils_1 = require("./utils");
|
|
10
19
|
let softAssertionErrors = [];
|
|
11
20
|
let retryAssertionFailures = new Map();
|
|
12
21
|
let retryFirstSeen = new Map();
|
|
13
22
|
let isInSoftTest = false;
|
|
14
23
|
let activeFailHandler = null;
|
|
15
24
|
let originalChaiAssert = null;
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Module-level state
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
16
28
|
function getEffectiveTimeout() {
|
|
17
29
|
var _a;
|
|
18
30
|
try {
|
|
@@ -29,40 +41,13 @@ function getEffectiveTimeout() {
|
|
|
29
41
|
return 4000;
|
|
30
42
|
}
|
|
31
43
|
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Internal helpers (Cypress-dependent — not unit-testable in isolation)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
32
47
|
function captureSoftAssertion(error) {
|
|
33
48
|
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
34
49
|
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
35
|
-
|
|
36
|
-
if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
|
|
37
|
-
softAssertionErrors.push({ message, stack });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
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}`;
|
|
45
|
-
}
|
|
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);
|
|
62
|
-
}
|
|
63
|
-
catch (_a) {
|
|
64
|
-
return String(value);
|
|
65
|
-
}
|
|
50
|
+
(0, utils_1.appendUniqueError)(softAssertionErrors, message, stack);
|
|
66
51
|
}
|
|
67
52
|
function getRetryableCommandId() {
|
|
68
53
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -70,10 +55,6 @@ function getRetryableCommandId() {
|
|
|
70
55
|
const current = cy.state('current');
|
|
71
56
|
if (!current)
|
|
72
57
|
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
58
|
const assertionCmd = (_a = current.get) === null || _a === void 0 ? void 0 : _a.call(current, 'currentAssertionCommand');
|
|
78
59
|
if (assertionCmd) {
|
|
79
60
|
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 : '';
|
|
@@ -87,13 +68,6 @@ function getRetryableCommandId() {
|
|
|
87
68
|
catch ( /* ignore */_h) { /* ignore */ }
|
|
88
69
|
return '';
|
|
89
70
|
}
|
|
90
|
-
function getAssertionToken(assertionContext, args) {
|
|
91
|
-
const subjectKey = getSubjectKey(assertionContext);
|
|
92
|
-
if (!subjectKey)
|
|
93
|
-
return '';
|
|
94
|
-
const expected = args === null || args === void 0 ? void 0 : args[3];
|
|
95
|
-
return `${subjectKey}|${toTokenPart(expected)}`;
|
|
96
|
-
}
|
|
97
71
|
function patchChaiAssertions() {
|
|
98
72
|
var _a;
|
|
99
73
|
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
@@ -105,6 +79,20 @@ function patchChaiAssertions() {
|
|
|
105
79
|
return;
|
|
106
80
|
assertionProto.assert = patchedAssertionAssert;
|
|
107
81
|
}
|
|
82
|
+
function resolveStableToken(assertionContext, args) {
|
|
83
|
+
return (0, utils_1.resolveToken)((0, utils_1.getAssertionToken)(assertionContext, args), getRetryableCommandId(), args);
|
|
84
|
+
}
|
|
85
|
+
function isRunningHookContext() {
|
|
86
|
+
var _a;
|
|
87
|
+
try {
|
|
88
|
+
const runnable = cy.state('runnable');
|
|
89
|
+
const type = (_a = runnable === null || runnable === void 0 ? void 0 : runnable.type) !== null && _a !== void 0 ? _a : runnable === null || runnable === void 0 ? void 0 : runnable._type;
|
|
90
|
+
return type === 'hook';
|
|
91
|
+
}
|
|
92
|
+
catch (_b) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
108
96
|
function patchedAssertionAssert(...args) {
|
|
109
97
|
if (!originalChaiAssert)
|
|
110
98
|
return;
|
|
@@ -112,74 +100,72 @@ function patchedAssertionAssert(...args) {
|
|
|
112
100
|
const result = originalChaiAssert.apply(this, args);
|
|
113
101
|
// Assertion passed — clear any staged failure for this token.
|
|
114
102
|
if (isInSoftTest) {
|
|
115
|
-
const token =
|
|
103
|
+
const token = resolveStableToken(this, args);
|
|
116
104
|
if (token) {
|
|
117
105
|
retryAssertionFailures.delete(token);
|
|
118
106
|
retryFirstSeen.delete(token);
|
|
119
107
|
}
|
|
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);
|
|
126
|
-
}
|
|
127
108
|
}
|
|
128
109
|
return result;
|
|
129
110
|
}
|
|
130
111
|
catch (error) {
|
|
131
112
|
if (!isInSoftTest)
|
|
132
113
|
throw error;
|
|
133
|
-
|
|
114
|
+
if (isRunningHookContext())
|
|
115
|
+
throw error;
|
|
116
|
+
const errorEntry = {
|
|
117
|
+
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
118
|
+
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
119
|
+
};
|
|
120
|
+
const token = resolveStableToken(this, args);
|
|
134
121
|
if (token) {
|
|
135
|
-
//
|
|
136
|
-
// a later retry succeeds.
|
|
137
|
-
retryAssertionFailures.set(token, {
|
|
138
|
-
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
139
|
-
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
140
|
-
});
|
|
122
|
+
// Track when we first saw this failure.
|
|
141
123
|
if (!retryFirstSeen.has(token)) {
|
|
142
124
|
retryFirstSeen.set(token, Date.now());
|
|
143
125
|
}
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
157
|
-
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
158
|
-
});
|
|
159
|
-
if (!retryFirstSeen.has(fallbackToken)) {
|
|
160
|
-
retryFirstSeen.set(fallbackToken, Date.now());
|
|
126
|
+
// Stage the failure so it can be cleared if a later retry succeeds.
|
|
127
|
+
retryAssertionFailures.set(token, errorEntry);
|
|
128
|
+
// Check if the retry window has expired. Swallow slightly before
|
|
129
|
+
// Cypress's own timeout to prevent it from firing the fail event.
|
|
130
|
+
// Cypress retries every ~50ms, so subtracting 100ms ensures we
|
|
131
|
+
// catch at least 1-2 more retries before the deadline.
|
|
132
|
+
const elapsed = Date.now() - retryFirstSeen.get(token);
|
|
133
|
+
const timeout = getEffectiveTimeout();
|
|
134
|
+
const swallowAt = Math.max(timeout - 100, timeout * 0.9);
|
|
135
|
+
if (elapsed < swallowAt) {
|
|
136
|
+
// Still within the retry window — rethrow so Cypress retries.
|
|
137
|
+
throw error;
|
|
161
138
|
}
|
|
162
|
-
//
|
|
163
|
-
|
|
139
|
+
// Retry window expired. Swallow the error: Cypress considers the
|
|
140
|
+
// assertion "passed", the command resolves, and the queue continues.
|
|
141
|
+
// The failure is already staged in retryAssertionFailures and will
|
|
142
|
+
// be promoted to softAssertionErrors during finalization.
|
|
143
|
+
return;
|
|
164
144
|
}
|
|
165
|
-
//
|
|
145
|
+
// No stable token (e.g. bare expect() in .then() callbacks).
|
|
146
|
+
// Capture directly and swallow so the queue continues.
|
|
166
147
|
captureSoftAssertion(error);
|
|
167
148
|
}
|
|
168
149
|
}
|
|
169
150
|
function setupSoftAssertions() {
|
|
170
151
|
patchChaiAssertions();
|
|
152
|
+
// Fail handler for non-assertion errors (e.g. element-not-found after
|
|
153
|
+
// cy.get timeout). These don't go through Chai's assert at all.
|
|
171
154
|
if (!activeFailHandler) {
|
|
172
155
|
activeFailHandler = (error) => {
|
|
173
156
|
if (!isInSoftTest)
|
|
174
157
|
throw error;
|
|
158
|
+
if (isRunningHookContext())
|
|
159
|
+
throw error;
|
|
175
160
|
// Final aggregated error must propagate to fail the test.
|
|
176
161
|
if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
|
|
177
162
|
throw error;
|
|
178
|
-
//
|
|
179
|
-
//
|
|
163
|
+
// Check if this is an assertion error that was already handled by
|
|
164
|
+
// patchedAssertionAssert (swallowed after timeout). In that case
|
|
165
|
+
// the fail handler should NOT fire. But for non-assertion command
|
|
166
|
+
// failures (e.g. cy.get can't find element), capture as soft failure.
|
|
180
167
|
captureSoftAssertion(error);
|
|
181
|
-
// Clear any matching retry-tracked entry to prevent double-counting
|
|
182
|
-
// at finalization (the fail handler is now the authoritative capture).
|
|
168
|
+
// Clear any matching retry-tracked entry to prevent double-counting.
|
|
183
169
|
const errorMsg = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
184
170
|
for (const [token, entry] of retryAssertionFailures.entries()) {
|
|
185
171
|
if (entry.message === errorMsg) {
|
|
@@ -208,27 +194,11 @@ function restoreAssertions() {
|
|
|
208
194
|
function buildSoftAssertionError() {
|
|
209
195
|
// Promote any remaining retry-tracked failures that weren't already
|
|
210
196
|
// captured by the fail handler (dedup by message).
|
|
211
|
-
|
|
212
|
-
const isDuplicate = softAssertionErrors.some(e => e.message === entry.message);
|
|
213
|
-
if (!isDuplicate) {
|
|
214
|
-
softAssertionErrors.push(entry);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
197
|
+
softAssertionErrors = (0, utils_1.mergeRetryFailures)(softAssertionErrors, retryAssertionFailures);
|
|
217
198
|
retryAssertionFailures.clear();
|
|
218
199
|
retryFirstSeen.clear();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
222
|
-
.join('\n');
|
|
223
|
-
const finalMessage = [
|
|
224
|
-
'',
|
|
225
|
-
'='.repeat(80),
|
|
226
|
-
`SOFT ASSERTION FAILURES (${softAssertionErrors.length} failed):`,
|
|
227
|
-
'='.repeat(80),
|
|
228
|
-
errorMessages,
|
|
229
|
-
'='.repeat(80),
|
|
230
|
-
'',
|
|
231
|
-
].join('\n');
|
|
200
|
+
const finalMessage = (0, utils_1.formatSoftAssertionErrors)(softAssertionErrors);
|
|
201
|
+
if (finalMessage !== null) {
|
|
232
202
|
softAssertionErrors = [];
|
|
233
203
|
const error = new Error(finalMessage);
|
|
234
204
|
error.name = 'SoftAssertionError';
|
|
@@ -255,31 +225,13 @@ function createSoftIt(baseIt) {
|
|
|
255
225
|
retryAssertionFailures.clear();
|
|
256
226
|
retryFirstSeen.clear();
|
|
257
227
|
setupSoftAssertions();
|
|
258
|
-
let result;
|
|
259
228
|
try {
|
|
260
|
-
|
|
229
|
+
return fn.call(this);
|
|
261
230
|
}
|
|
262
231
|
catch (error) {
|
|
263
232
|
abortSoftTest();
|
|
264
233
|
throw error;
|
|
265
234
|
}
|
|
266
|
-
// Finalize from inside the test chain (not from hooks) so Cypress counts
|
|
267
|
-
// the failure on the test itself.
|
|
268
|
-
return cy.then(() => {
|
|
269
|
-
return Cypress.Promise.resolve(result)
|
|
270
|
-
.catch((error) => {
|
|
271
|
-
if (isInSoftTest) {
|
|
272
|
-
abortSoftTest();
|
|
273
|
-
}
|
|
274
|
-
throw error;
|
|
275
|
-
})
|
|
276
|
-
.then(() => {
|
|
277
|
-
const finalError = finalizeSoftTest();
|
|
278
|
-
if (finalError) {
|
|
279
|
-
throw finalError;
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
235
|
});
|
|
284
236
|
};
|
|
285
237
|
}
|
|
@@ -309,3 +261,33 @@ globalThis.soft_it.only = createSoftIt(it.only);
|
|
|
309
261
|
* soft_it.skip - Skip this soft test
|
|
310
262
|
*/
|
|
311
263
|
globalThis.soft_it.skip = it.skip;
|
|
264
|
+
// Global afterEach hook: finalize soft assertions after each test.
|
|
265
|
+
// Registered at the root level so it applies to all suites.
|
|
266
|
+
// For non-soft tests (isInSoftTest === false), this is a no-op.
|
|
267
|
+
afterEach(function () {
|
|
268
|
+
var _a, _b, _c;
|
|
269
|
+
if (!isInSoftTest)
|
|
270
|
+
return;
|
|
271
|
+
const finalError = finalizeSoftTest();
|
|
272
|
+
if (finalError) {
|
|
273
|
+
const test = this.currentTest;
|
|
274
|
+
const currentRetry = (_a = test === null || test === void 0 ? void 0 : test._currentRetry) !== null && _a !== void 0 ? _a : 0;
|
|
275
|
+
const maxRetries = (_b = test === null || test === void 0 ? void 0 : test._retries) !== null && _b !== void 0 ? _b : 0;
|
|
276
|
+
if (currentRetry < maxRetries) {
|
|
277
|
+
// Intermediate retry attempt — throw so Cypress triggers the next retry.
|
|
278
|
+
// Cypress's retry machinery intercepts hook failures during non-final
|
|
279
|
+
// attempts and does NOT abort the suite, so this is safe here.
|
|
280
|
+
throw finalError;
|
|
281
|
+
}
|
|
282
|
+
// Last (or only) attempt — use runner.fail() to mark the TEST as failed
|
|
283
|
+
// rather than the hook. Throwing from afterEach on the final attempt
|
|
284
|
+
// would skip remaining tests in the suite.
|
|
285
|
+
const runner = (_c = Cypress.mocha) === null || _c === void 0 ? void 0 : _c.getRunner();
|
|
286
|
+
if (runner && test) {
|
|
287
|
+
runner.fail(test, finalError);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw finalError;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for the soft assertions plugin.
|
|
3
|
+
*
|
|
4
|
+
* These are free of Cypress/Chai globals and module-level state, making them
|
|
5
|
+
* suitable for fast, isolated unit testing without a running Cypress instance.
|
|
6
|
+
*/
|
|
7
|
+
export interface ErrorEntry {
|
|
8
|
+
message: string;
|
|
9
|
+
stack?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Converts any value to a stable string token component.
|
|
13
|
+
* Used when building assertion identity tokens for retry-tracking.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toTokenPart(value: any): string;
|
|
16
|
+
/**
|
|
17
|
+
* Extracts a stable key from a Chai assertion context's subject.
|
|
18
|
+
* Returns '#<id>' for elements with an id, the jQuery selector string if available,
|
|
19
|
+
* or '' when nothing stable can be derived.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSubjectKey(assertionContext: any): string;
|
|
22
|
+
/**
|
|
23
|
+
* Builds a stable token that uniquely identifies a retryable assertion by
|
|
24
|
+
* combining the subject key with the expected value.
|
|
25
|
+
* Returns '' when no stable key can be derived (token-less assertion).
|
|
26
|
+
*/
|
|
27
|
+
export declare function getAssertionToken(assertionContext: any, args: any[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Appends an error to the list only if it is not a consecutive duplicate.
|
|
30
|
+
* "Duplicate" means the immediately preceding entry has an identical message
|
|
31
|
+
* AND identical stack. Non-consecutive duplicates are always appended.
|
|
32
|
+
*
|
|
33
|
+
* Mutates the provided array in place (matching the original behaviour of
|
|
34
|
+
* captureSoftAssertion).
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendUniqueError(errors: ErrorEntry[], message: string, stack?: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Promotes entries from retryFailures into softErrors, skipping any entry
|
|
39
|
+
* whose message already appears in softErrors (message-only dedup).
|
|
40
|
+
*
|
|
41
|
+
* Returns a new array — does not mutate either input. The retry map is also
|
|
42
|
+
* left untouched; callers are responsible for clearing it afterwards.
|
|
43
|
+
*/
|
|
44
|
+
export declare function mergeRetryFailures(softErrors: ErrorEntry[], retryFailures: Map<string, ErrorEntry>): ErrorEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the stable token that identifies a retryable assertion.
|
|
47
|
+
*
|
|
48
|
+
* Priority:
|
|
49
|
+
* 1. assertionToken (derived from subject id / selector + expected value)
|
|
50
|
+
* 2. commandId-based token (used when the subject has no stable id/selector)
|
|
51
|
+
* 3. '' (token-less — assertion will be captured immediately instead of tracked)
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveToken(assertionToken: string, commandId: string, args: any[]): string;
|
|
54
|
+
/**
|
|
55
|
+
* Formats a list of captured soft assertion errors into the final
|
|
56
|
+
* SoftAssertionError message string.
|
|
57
|
+
* Returns null when the list is empty (no failures to report).
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatSoftAssertionErrors(errors: ErrorEntry[]): string | null;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure utility functions for the soft assertions plugin.
|
|
4
|
+
*
|
|
5
|
+
* These are free of Cypress/Chai globals and module-level state, making them
|
|
6
|
+
* suitable for fast, isolated unit testing without a running Cypress instance.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.toTokenPart = toTokenPart;
|
|
10
|
+
exports.getSubjectKey = getSubjectKey;
|
|
11
|
+
exports.getAssertionToken = getAssertionToken;
|
|
12
|
+
exports.appendUniqueError = appendUniqueError;
|
|
13
|
+
exports.mergeRetryFailures = mergeRetryFailures;
|
|
14
|
+
exports.resolveToken = resolveToken;
|
|
15
|
+
exports.formatSoftAssertionErrors = formatSoftAssertionErrors;
|
|
16
|
+
/**
|
|
17
|
+
* Converts any value to a stable string token component.
|
|
18
|
+
* Used when building assertion identity tokens for retry-tracking.
|
|
19
|
+
*/
|
|
20
|
+
function toTokenPart(value) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return 'undefined';
|
|
23
|
+
if (value === null)
|
|
24
|
+
return 'null';
|
|
25
|
+
const kind = typeof value;
|
|
26
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
27
|
+
return String(value);
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extracts a stable key from a Chai assertion context's subject.
|
|
37
|
+
* Returns '#<id>' for elements with an id, the jQuery selector string if available,
|
|
38
|
+
* or '' when nothing stable can be derived.
|
|
39
|
+
*/
|
|
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}`;
|
|
45
|
+
}
|
|
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
|
+
/**
|
|
53
|
+
* Builds a stable token that uniquely identifies a retryable assertion by
|
|
54
|
+
* combining the subject key with the expected value.
|
|
55
|
+
* Returns '' when no stable key can be derived (token-less assertion).
|
|
56
|
+
*/
|
|
57
|
+
function getAssertionToken(assertionContext, args) {
|
|
58
|
+
const subjectKey = getSubjectKey(assertionContext);
|
|
59
|
+
if (!subjectKey)
|
|
60
|
+
return '';
|
|
61
|
+
const expected = args === null || args === void 0 ? void 0 : args[3];
|
|
62
|
+
return `${subjectKey}|${toTokenPart(expected)}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Appends an error to the list only if it is not a consecutive duplicate.
|
|
66
|
+
* "Duplicate" means the immediately preceding entry has an identical message
|
|
67
|
+
* AND identical stack. Non-consecutive duplicates are always appended.
|
|
68
|
+
*
|
|
69
|
+
* Mutates the provided array in place (matching the original behaviour of
|
|
70
|
+
* captureSoftAssertion).
|
|
71
|
+
*/
|
|
72
|
+
function appendUniqueError(errors, message, stack) {
|
|
73
|
+
const lastEntry = errors[errors.length - 1];
|
|
74
|
+
if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
|
|
75
|
+
errors.push({ message, stack });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Promotes entries from retryFailures into softErrors, skipping any entry
|
|
80
|
+
* whose message already appears in softErrors (message-only dedup).
|
|
81
|
+
*
|
|
82
|
+
* Returns a new array — does not mutate either input. The retry map is also
|
|
83
|
+
* left untouched; callers are responsible for clearing it afterwards.
|
|
84
|
+
*/
|
|
85
|
+
function mergeRetryFailures(softErrors, retryFailures) {
|
|
86
|
+
const result = [...softErrors];
|
|
87
|
+
for (const entry of retryFailures.values()) {
|
|
88
|
+
const isDuplicate = result.some(e => e.message === entry.message);
|
|
89
|
+
if (!isDuplicate) {
|
|
90
|
+
result.push(entry);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the stable token that identifies a retryable assertion.
|
|
97
|
+
*
|
|
98
|
+
* Priority:
|
|
99
|
+
* 1. assertionToken (derived from subject id / selector + expected value)
|
|
100
|
+
* 2. commandId-based token (used when the subject has no stable id/selector)
|
|
101
|
+
* 3. '' (token-less — assertion will be captured immediately instead of tracked)
|
|
102
|
+
*/
|
|
103
|
+
function resolveToken(assertionToken, commandId, args) {
|
|
104
|
+
if (assertionToken)
|
|
105
|
+
return assertionToken;
|
|
106
|
+
if (commandId)
|
|
107
|
+
return `__cmd__|${commandId}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Formats a list of captured soft assertion errors into the final
|
|
112
|
+
* SoftAssertionError message string.
|
|
113
|
+
* Returns null when the list is empty (no failures to report).
|
|
114
|
+
*/
|
|
115
|
+
function formatSoftAssertionErrors(errors) {
|
|
116
|
+
if (errors.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const errorMessages = errors
|
|
119
|
+
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
return [
|
|
122
|
+
'',
|
|
123
|
+
'='.repeat(80),
|
|
124
|
+
`SOFT ASSERTION FAILURES (${errors.length} failed):`,
|
|
125
|
+
'='.repeat(80),
|
|
126
|
+
errorMessages,
|
|
127
|
+
'='.repeat(80),
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koenvanbelle/cypress-soft-assertions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
+
"prepare": "npm run build",
|
|
9
10
|
"prepublishOnly": "npm run build",
|
|
10
11
|
"cy:open": "cypress open",
|
|
11
12
|
"cy:run": "cypress run",
|
|
12
|
-
"test": "npm run build && cypress run"
|
|
13
|
+
"test": "npm run build && npm run test:unit && cypress run && npm run test:runner",
|
|
14
|
+
"test:unit": "node --test test/unit.test.mjs",
|
|
15
|
+
"test:runner": "node --test test/runner.test.mjs"
|
|
13
16
|
},
|
|
14
17
|
"keywords": [
|
|
15
18
|
"cypress",
|
|
@@ -36,9 +39,10 @@
|
|
|
36
39
|
"cypress": ">=10.0.0"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
42
|
+
"@types/mocha": "^10.0.10",
|
|
39
43
|
"cypress": "^15.9.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
44
|
+
"cypress-translation-checker": "^1.3.4",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
44
48
|
"dist",
|