@koenvanbelle/cypress-soft-assertions 2.2.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.js +40 -68
- package/dist/utils.d.ts +59 -0
- package/dist/utils.js +130 -0
- package/package.json +7 -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.js
CHANGED
|
@@ -15,12 +15,16 @@
|
|
|
15
15
|
* - An afterEach hook finalizes: aggregates all failures and reports them.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const utils_1 = require("./utils");
|
|
18
19
|
let softAssertionErrors = [];
|
|
19
20
|
let retryAssertionFailures = new Map();
|
|
20
21
|
let retryFirstSeen = new Map();
|
|
21
22
|
let isInSoftTest = false;
|
|
22
23
|
let activeFailHandler = null;
|
|
23
24
|
let originalChaiAssert = null;
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Module-level state
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
24
28
|
function getEffectiveTimeout() {
|
|
25
29
|
var _a;
|
|
26
30
|
try {
|
|
@@ -37,40 +41,13 @@ function getEffectiveTimeout() {
|
|
|
37
41
|
return 4000;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Internal helpers (Cypress-dependent — not unit-testable in isolation)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
40
47
|
function captureSoftAssertion(error) {
|
|
41
48
|
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
42
49
|
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
43
|
-
|
|
44
|
-
if (!lastEntry || lastEntry.message !== message || lastEntry.stack !== stack) {
|
|
45
|
-
softAssertionErrors.push({ message, stack });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function getSubjectKey(assertionContext) {
|
|
49
|
-
const obj = assertionContext === null || assertionContext === void 0 ? void 0 : assertionContext._obj;
|
|
50
|
-
const first = Array.isArray(obj) ? obj[0] : obj === null || obj === void 0 ? void 0 : obj[0];
|
|
51
|
-
if (first && typeof first.id === 'string' && first.id.length > 0) {
|
|
52
|
-
return `#${first.id}`;
|
|
53
|
-
}
|
|
54
|
-
const selector = obj === null || obj === void 0 ? void 0 : obj.selector;
|
|
55
|
-
if (typeof selector === 'string' && selector.length > 0) {
|
|
56
|
-
return selector;
|
|
57
|
-
}
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
function toTokenPart(value) {
|
|
61
|
-
if (value === undefined)
|
|
62
|
-
return 'undefined';
|
|
63
|
-
if (value === null)
|
|
64
|
-
return 'null';
|
|
65
|
-
const kind = typeof value;
|
|
66
|
-
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
67
|
-
return String(value);
|
|
68
|
-
try {
|
|
69
|
-
return JSON.stringify(value);
|
|
70
|
-
}
|
|
71
|
-
catch (_a) {
|
|
72
|
-
return String(value);
|
|
73
|
-
}
|
|
50
|
+
(0, utils_1.appendUniqueError)(softAssertionErrors, message, stack);
|
|
74
51
|
}
|
|
75
52
|
function getRetryableCommandId() {
|
|
76
53
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -91,13 +68,6 @@ function getRetryableCommandId() {
|
|
|
91
68
|
catch ( /* ignore */_h) { /* ignore */ }
|
|
92
69
|
return '';
|
|
93
70
|
}
|
|
94
|
-
function getAssertionToken(assertionContext, args) {
|
|
95
|
-
const subjectKey = getSubjectKey(assertionContext);
|
|
96
|
-
if (!subjectKey)
|
|
97
|
-
return '';
|
|
98
|
-
const expected = args === null || args === void 0 ? void 0 : args[3];
|
|
99
|
-
return `${subjectKey}|${toTokenPart(expected)}`;
|
|
100
|
-
}
|
|
101
71
|
function patchChaiAssertions() {
|
|
102
72
|
var _a;
|
|
103
73
|
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
@@ -110,13 +80,18 @@ function patchChaiAssertions() {
|
|
|
110
80
|
assertionProto.assert = patchedAssertionAssert;
|
|
111
81
|
}
|
|
112
82
|
function resolveStableToken(assertionContext, args) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
95
|
}
|
|
121
96
|
function patchedAssertionAssert(...args) {
|
|
122
97
|
if (!originalChaiAssert)
|
|
@@ -136,6 +111,8 @@ function patchedAssertionAssert(...args) {
|
|
|
136
111
|
catch (error) {
|
|
137
112
|
if (!isInSoftTest)
|
|
138
113
|
throw error;
|
|
114
|
+
if (isRunningHookContext())
|
|
115
|
+
throw error;
|
|
139
116
|
const errorEntry = {
|
|
140
117
|
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
141
118
|
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
@@ -178,6 +155,8 @@ function setupSoftAssertions() {
|
|
|
178
155
|
activeFailHandler = (error) => {
|
|
179
156
|
if (!isInSoftTest)
|
|
180
157
|
throw error;
|
|
158
|
+
if (isRunningHookContext())
|
|
159
|
+
throw error;
|
|
181
160
|
// Final aggregated error must propagate to fail the test.
|
|
182
161
|
if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
|
|
183
162
|
throw error;
|
|
@@ -215,27 +194,11 @@ function restoreAssertions() {
|
|
|
215
194
|
function buildSoftAssertionError() {
|
|
216
195
|
// Promote any remaining retry-tracked failures that weren't already
|
|
217
196
|
// captured by the fail handler (dedup by message).
|
|
218
|
-
|
|
219
|
-
const isDuplicate = softAssertionErrors.some(e => e.message === entry.message);
|
|
220
|
-
if (!isDuplicate) {
|
|
221
|
-
softAssertionErrors.push(entry);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
197
|
+
softAssertionErrors = (0, utils_1.mergeRetryFailures)(softAssertionErrors, retryAssertionFailures);
|
|
224
198
|
retryAssertionFailures.clear();
|
|
225
199
|
retryFirstSeen.clear();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.map((entry, index) => ` ${index + 1}. ${entry.message}`)
|
|
229
|
-
.join('\n');
|
|
230
|
-
const finalMessage = [
|
|
231
|
-
'',
|
|
232
|
-
'='.repeat(80),
|
|
233
|
-
`SOFT ASSERTION FAILURES (${softAssertionErrors.length} failed):`,
|
|
234
|
-
'='.repeat(80),
|
|
235
|
-
errorMessages,
|
|
236
|
-
'='.repeat(80),
|
|
237
|
-
'',
|
|
238
|
-
].join('\n');
|
|
200
|
+
const finalMessage = (0, utils_1.formatSoftAssertionErrors)(softAssertionErrors);
|
|
201
|
+
if (finalMessage !== null) {
|
|
239
202
|
softAssertionErrors = [];
|
|
240
203
|
const error = new Error(finalMessage);
|
|
241
204
|
error.name = 'SoftAssertionError';
|
|
@@ -302,15 +265,24 @@ globalThis.soft_it.skip = it.skip;
|
|
|
302
265
|
// Registered at the root level so it applies to all suites.
|
|
303
266
|
// For non-soft tests (isInSoftTest === false), this is a no-op.
|
|
304
267
|
afterEach(function () {
|
|
305
|
-
var _a;
|
|
268
|
+
var _a, _b, _c;
|
|
306
269
|
if (!isInSoftTest)
|
|
307
270
|
return;
|
|
308
271
|
const finalError = finalizeSoftTest();
|
|
309
272
|
if (finalError) {
|
|
310
|
-
// Use runner.fail() to mark the TEST as failed rather than the hook.
|
|
311
|
-
// Throwing from afterEach would skip remaining tests in the suite.
|
|
312
|
-
const runner = (_a = Cypress.mocha) === null || _a === void 0 ? void 0 : _a.getRunner();
|
|
313
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();
|
|
314
286
|
if (runner && test) {
|
|
315
287
|
runner.fail(test, finalError);
|
|
316
288
|
}
|
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,17 @@
|
|
|
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 && npm run test:runner",
|
|
13
|
+
"test": "npm run build && npm run test:unit && cypress run && npm run test:runner",
|
|
14
|
+
"test:unit": "node --test test/unit.test.mjs",
|
|
13
15
|
"test:runner": "node --test test/runner.test.mjs"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
@@ -37,9 +39,10 @@
|
|
|
37
39
|
"cypress": ">=10.0.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
42
|
+
"@types/mocha": "^10.0.10",
|
|
40
43
|
"cypress": "^15.9.0",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
44
|
+
"cypress-translation-checker": "^1.3.4",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
43
46
|
},
|
|
44
47
|
"files": [
|
|
45
48
|
"dist",
|