@koenvanbelle/cypress-soft-assertions 2.2.1 → 2.3.2
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 +111 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +85 -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,117 @@ 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
|
+
|
|
252
|
+
## Force-Fail Hook (strict mode)
|
|
253
|
+
|
|
254
|
+
If your project (or another plugin) mutates test state in hooks (for example in `test:after:run`) and soft-failed tests appear as passed, enable strict mode per test:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
soft_it.strict('fails hard when soft assertions are captured', () => {
|
|
258
|
+
cy.visit('/');
|
|
259
|
+
cy.get('#title').should('have.text', 'Wrong Title');
|
|
260
|
+
cy.get('#status').should('have.text', 'Ready');
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
You can also use `soft_it.strict.only(...)` and `soft_it.strict.skip(...)`.
|
|
265
|
+
|
|
266
|
+
To force strict mode for all soft tests in a run, use env flags:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
cypress run --env softAssertForceFail=true
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
You can also use:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
cypress run --env SOFT_ASSERT_FORCE_FAIL=true
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
When strict mode is enabled (per test or via env), the plugin throws the final `SoftAssertionError` from `afterEach` on the last attempt, preventing downstream hook-based recovery from turning the test green.
|
|
279
|
+
|
|
280
|
+
Note: strict mode may abort remaining tests in the same suite after the first forced soft failure (standard Cypress hook-failure behavior).
|
|
281
|
+
|
|
171
282
|
## License
|
|
172
283
|
|
|
173
284
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,16 @@ declare global {
|
|
|
39
39
|
* Run only this test (like it.only)
|
|
40
40
|
*/
|
|
41
41
|
function only(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
|
|
42
|
+
/**
|
|
43
|
+
* Run this soft test in strict mode (force unrecoverable failure)
|
|
44
|
+
*/
|
|
45
|
+
function strict(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
|
|
46
|
+
namespace strict {
|
|
47
|
+
/** Run only this strict soft test */
|
|
48
|
+
function only(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
|
|
49
|
+
/** Skip this strict soft test */
|
|
50
|
+
function skip(title: string, fn: Mocha.Func | Mocha.AsyncFunc): void;
|
|
51
|
+
}
|
|
42
52
|
/**
|
|
43
53
|
* Skip this test (like it.skip)
|
|
44
54
|
*/
|
package/dist/index.js
CHANGED
|
@@ -15,12 +15,17 @@
|
|
|
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;
|
|
23
|
+
let forceFailCurrentSoftTest = false;
|
|
22
24
|
let activeFailHandler = null;
|
|
23
25
|
let originalChaiAssert = null;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Module-level state
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
24
29
|
function getEffectiveTimeout() {
|
|
25
30
|
var _a;
|
|
26
31
|
try {
|
|
@@ -37,41 +42,34 @@ function getEffectiveTimeout() {
|
|
|
37
42
|
return 4000;
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 '';
|
|
45
|
+
function isTruthy(value) {
|
|
46
|
+
if (typeof value === 'boolean')
|
|
47
|
+
return value;
|
|
48
|
+
if (typeof value === 'number')
|
|
49
|
+
return value !== 0;
|
|
50
|
+
const normalized = String(value !== null && value !== void 0 ? value : '').trim().toLowerCase();
|
|
51
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
59
52
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
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);
|
|
53
|
+
function shouldForceFailSoftAssertions() {
|
|
54
|
+
var _a;
|
|
68
55
|
try {
|
|
69
|
-
|
|
56
|
+
const env = (_a = Cypress.env) === null || _a === void 0 ? void 0 : _a.bind(Cypress);
|
|
57
|
+
if (!env)
|
|
58
|
+
return false;
|
|
59
|
+
return isTruthy(env('softAssertForceFail')) || isTruthy(env('SOFT_ASSERT_FORCE_FAIL'));
|
|
70
60
|
}
|
|
71
|
-
catch (
|
|
72
|
-
return
|
|
61
|
+
catch (_b) {
|
|
62
|
+
return false;
|
|
73
63
|
}
|
|
74
64
|
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Internal helpers (Cypress-dependent — not unit-testable in isolation)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function captureSoftAssertion(error) {
|
|
69
|
+
const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
70
|
+
const stack = error === null || error === void 0 ? void 0 : error.stack;
|
|
71
|
+
(0, utils_1.appendUniqueError)(softAssertionErrors, message, stack);
|
|
72
|
+
}
|
|
75
73
|
function getRetryableCommandId() {
|
|
76
74
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
77
75
|
try {
|
|
@@ -91,13 +89,6 @@ function getRetryableCommandId() {
|
|
|
91
89
|
catch ( /* ignore */_h) { /* ignore */ }
|
|
92
90
|
return '';
|
|
93
91
|
}
|
|
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
92
|
function patchChaiAssertions() {
|
|
102
93
|
var _a;
|
|
103
94
|
const assertionProto = (_a = chai === null || chai === void 0 ? void 0 : chai.Assertion) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
@@ -110,13 +101,18 @@ function patchChaiAssertions() {
|
|
|
110
101
|
assertionProto.assert = patchedAssertionAssert;
|
|
111
102
|
}
|
|
112
103
|
function resolveStableToken(assertionContext, args) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
return (0, utils_1.resolveToken)((0, utils_1.getAssertionToken)(assertionContext, args), getRetryableCommandId(), args);
|
|
105
|
+
}
|
|
106
|
+
function isRunningHookContext() {
|
|
107
|
+
var _a;
|
|
108
|
+
try {
|
|
109
|
+
const runnable = cy.state('runnable');
|
|
110
|
+
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;
|
|
111
|
+
return type === 'hook';
|
|
112
|
+
}
|
|
113
|
+
catch (_b) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
120
116
|
}
|
|
121
117
|
function patchedAssertionAssert(...args) {
|
|
122
118
|
if (!originalChaiAssert)
|
|
@@ -136,6 +132,8 @@ function patchedAssertionAssert(...args) {
|
|
|
136
132
|
catch (error) {
|
|
137
133
|
if (!isInSoftTest)
|
|
138
134
|
throw error;
|
|
135
|
+
if (isRunningHookContext())
|
|
136
|
+
throw error;
|
|
139
137
|
const errorEntry = {
|
|
140
138
|
message: String((error === null || error === void 0 ? void 0 : error.message) || error),
|
|
141
139
|
stack: error === null || error === void 0 ? void 0 : error.stack,
|
|
@@ -178,6 +176,8 @@ function setupSoftAssertions() {
|
|
|
178
176
|
activeFailHandler = (error) => {
|
|
179
177
|
if (!isInSoftTest)
|
|
180
178
|
throw error;
|
|
179
|
+
if (isRunningHookContext())
|
|
180
|
+
throw error;
|
|
181
181
|
// Final aggregated error must propagate to fail the test.
|
|
182
182
|
if (String((error === null || error === void 0 ? void 0 : error.name) || '') === 'SoftAssertionError')
|
|
183
183
|
throw error;
|
|
@@ -215,27 +215,11 @@ function restoreAssertions() {
|
|
|
215
215
|
function buildSoftAssertionError() {
|
|
216
216
|
// Promote any remaining retry-tracked failures that weren't already
|
|
217
217
|
// 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
|
-
}
|
|
218
|
+
softAssertionErrors = (0, utils_1.mergeRetryFailures)(softAssertionErrors, retryAssertionFailures);
|
|
224
219
|
retryAssertionFailures.clear();
|
|
225
220
|
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');
|
|
221
|
+
const finalMessage = (0, utils_1.formatSoftAssertionErrors)(softAssertionErrors);
|
|
222
|
+
if (finalMessage !== null) {
|
|
239
223
|
softAssertionErrors = [];
|
|
240
224
|
const error = new Error(finalMessage);
|
|
241
225
|
error.name = 'SoftAssertionError';
|
|
@@ -245,19 +229,22 @@ function buildSoftAssertionError() {
|
|
|
245
229
|
}
|
|
246
230
|
function finalizeSoftTest() {
|
|
247
231
|
isInSoftTest = false;
|
|
232
|
+
forceFailCurrentSoftTest = false;
|
|
248
233
|
restoreAssertions();
|
|
249
234
|
return buildSoftAssertionError();
|
|
250
235
|
}
|
|
251
236
|
function abortSoftTest() {
|
|
252
237
|
isInSoftTest = false;
|
|
238
|
+
forceFailCurrentSoftTest = false;
|
|
253
239
|
restoreAssertions();
|
|
254
240
|
retryAssertionFailures.clear();
|
|
255
241
|
retryFirstSeen.clear();
|
|
256
242
|
}
|
|
257
|
-
function createSoftIt(baseIt) {
|
|
243
|
+
function createSoftIt(baseIt, options) {
|
|
258
244
|
return function (title, fn) {
|
|
259
245
|
return baseIt(title, function () {
|
|
260
246
|
isInSoftTest = true;
|
|
247
|
+
forceFailCurrentSoftTest = Boolean(options === null || options === void 0 ? void 0 : options.strict) || shouldForceFailSoftAssertions();
|
|
261
248
|
softAssertionErrors = [];
|
|
262
249
|
retryAssertionFailures.clear();
|
|
263
250
|
retryFirstSeen.clear();
|
|
@@ -294,23 +281,53 @@ globalThis.soft_it = createSoftIt(it);
|
|
|
294
281
|
* soft_it.only - Run only this soft test
|
|
295
282
|
*/
|
|
296
283
|
globalThis.soft_it.only = createSoftIt(it.only);
|
|
284
|
+
/**
|
|
285
|
+
* soft_it.strict - Run this soft test in strict mode.
|
|
286
|
+
*
|
|
287
|
+
* Strict mode throws the final SoftAssertionError from afterEach on the
|
|
288
|
+
* last attempt, making the failure unrecoverable by downstream hooks.
|
|
289
|
+
*/
|
|
290
|
+
globalThis.soft_it.strict = createSoftIt(it, { strict: true });
|
|
291
|
+
/**
|
|
292
|
+
* soft_it.strict.only - Run only this strict soft test
|
|
293
|
+
*/
|
|
294
|
+
globalThis.soft_it.strict.only = createSoftIt(it.only, { strict: true });
|
|
297
295
|
/**
|
|
298
296
|
* soft_it.skip - Skip this soft test
|
|
299
297
|
*/
|
|
300
298
|
globalThis.soft_it.skip = it.skip;
|
|
299
|
+
/**
|
|
300
|
+
* soft_it.strict.skip - Skip this strict soft test
|
|
301
|
+
*/
|
|
302
|
+
globalThis.soft_it.strict.skip = it.skip;
|
|
301
303
|
// Global afterEach hook: finalize soft assertions after each test.
|
|
302
304
|
// Registered at the root level so it applies to all suites.
|
|
303
305
|
// For non-soft tests (isInSoftTest === false), this is a no-op.
|
|
304
306
|
afterEach(function () {
|
|
305
|
-
var _a;
|
|
307
|
+
var _a, _b, _c;
|
|
306
308
|
if (!isInSoftTest)
|
|
307
309
|
return;
|
|
308
310
|
const finalError = finalizeSoftTest();
|
|
309
311
|
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
312
|
const test = this.currentTest;
|
|
313
|
+
const currentRetry = (_a = test === null || test === void 0 ? void 0 : test._currentRetry) !== null && _a !== void 0 ? _a : 0;
|
|
314
|
+
const maxRetries = (_b = test === null || test === void 0 ? void 0 : test._retries) !== null && _b !== void 0 ? _b : 0;
|
|
315
|
+
if (currentRetry < maxRetries) {
|
|
316
|
+
// Intermediate retry attempt — throw so Cypress triggers the next retry.
|
|
317
|
+
// Cypress's retry machinery intercepts hook failures during non-final
|
|
318
|
+
// attempts and does NOT abort the suite, so this is safe here.
|
|
319
|
+
throw finalError;
|
|
320
|
+
}
|
|
321
|
+
// Last (or only) attempt — use runner.fail() to mark the TEST as failed
|
|
322
|
+
// rather than the hook. Throwing from afterEach on the final attempt
|
|
323
|
+
// would skip remaining tests in the suite.
|
|
324
|
+
if (forceFailCurrentSoftTest) {
|
|
325
|
+
// Strict mode: force an unrecoverable hook failure.
|
|
326
|
+
// This guarantees a failed result even when other plugins mutate
|
|
327
|
+
// test state in test:after:run.
|
|
328
|
+
throw finalError;
|
|
329
|
+
}
|
|
330
|
+
const runner = (_c = Cypress.mocha) === null || _c === void 0 ? void 0 : _c.getRunner();
|
|
314
331
|
if (runner && test) {
|
|
315
332
|
runner.fail(test, finalError);
|
|
316
333
|
}
|
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.2
|
|
3
|
+
"version": "2.3.2",
|
|
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",
|