@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 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 captureSoftAssertion(error) {
41
- const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
42
- const stack = error === null || error === void 0 ? void 0 : error.stack;
43
- const lastEntry = softAssertionErrors[softAssertionErrors.length - 1];
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 '';
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 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);
53
+ function shouldForceFailSoftAssertions() {
54
+ var _a;
68
55
  try {
69
- return JSON.stringify(value);
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 (_a) {
72
- return String(value);
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
- const token = getAssertionToken(assertionContext, args);
114
- if (token)
115
- return token;
116
- const retryableCid = getRetryableCommandId();
117
- if (retryableCid)
118
- return `__cmd__|${retryableCid}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
119
- return '';
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
- for (const entry of retryAssertionFailures.values()) {
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
- if (softAssertionErrors.length > 0) {
227
- const errorMessages = softAssertionErrors
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
  }
@@ -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.1",
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
- "typescript": "^5.9.3",
42
- "@types/mocha": "^10.0.10"
44
+ "cypress-translation-checker": "^1.3.4",
45
+ "typescript": "^5.9.3"
43
46
  },
44
47
  "files": [
45
48
  "dist",