@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 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
- 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 '';
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
- 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 '';
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
- 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
- }
197
+ softAssertionErrors = (0, utils_1.mergeRetryFailures)(softAssertionErrors, retryAssertionFailures);
224
198
  retryAssertionFailures.clear();
225
199
  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');
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
  }
@@ -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.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
- "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",