@koenvanbelle/cypress-soft-assertions 1.0.6 → 1.0.7

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.
Files changed (2) hide show
  1. package/dist/index.js +162 -96
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,40 +10,112 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  let softAssertionErrors = [];
11
11
  let isInSoftTest = false;
12
12
  let originalAssert = null;
13
+ const retryWindows = new Map();
13
14
  /**
14
- * Intercept Chai assertions to capture failures instead of throwing
15
+ * Track a soft assertion failure so it can be reported at test end.
16
+ */
17
+ function captureSoftAssertion(error) {
18
+ const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
19
+ const stack = error === null || error === void 0 ? void 0 : error.stack;
20
+ const lastEntry = softAssertionErrors[softAssertionErrors.length - 1];
21
+ if (lastEntry && lastEntry.message === message && lastEntry.stack === stack) {
22
+ return;
23
+ }
24
+ softAssertionErrors.push({
25
+ message,
26
+ stack
27
+ });
28
+ }
29
+ /**
30
+ * Read command metadata from Cypress internals in a defensive way.
31
+ */
32
+ function getCurrentCommand() {
33
+ const state = cy === null || cy === void 0 ? void 0 : cy.state;
34
+ if (typeof state !== 'function') {
35
+ return null;
36
+ }
37
+ return state('current') || null;
38
+ }
39
+ function getCommandProp(command, prop) {
40
+ if (!command) {
41
+ return undefined;
42
+ }
43
+ if (typeof command.get === 'function') {
44
+ return command.get(prop);
45
+ }
46
+ if (command.attributes && prop in command.attributes) {
47
+ return command.attributes[prop];
48
+ }
49
+ return command[prop];
50
+ }
51
+ /**
52
+ * Build retry context for the currently running Cypress command.
53
+ */
54
+ function getAssertionContext() {
55
+ const command = getCurrentCommand();
56
+ const commandId = String(getCommandProp(command, 'id') || getCommandProp(command, 'chainerId') || '');
57
+ const commandName = String(getCommandProp(command, 'name') || '');
58
+ const timeout = Number(getCommandProp(command, 'timeout')) || Number(Cypress.config('defaultCommandTimeout'));
59
+ const isRetriable = commandName === 'should' || commandName === 'and';
60
+ return {
61
+ commandId,
62
+ timeout,
63
+ isRetriable,
64
+ };
65
+ }
66
+ function formatSoftAssertionError(error, timeout, isRetriable) {
67
+ const message = String((error === null || error === void 0 ? void 0 : error.message) || error);
68
+ if (isRetriable && !/Timed out retrying after/i.test(message)) {
69
+ return {
70
+ ...error,
71
+ message: `Timed out retrying after ${timeout}ms: ${message}`,
72
+ };
73
+ }
74
+ return error;
75
+ }
76
+ /**
77
+ * Intercept Chai assertions and make them soft in soft_it() tests.
15
78
  */
16
79
  function setupSoftAssertions() {
17
80
  if (!originalAssert) {
18
- // Store the original assert function
19
81
  originalAssert = chai.Assertion.prototype.assert;
20
82
  }
21
- // Override the assert function to capture errors
22
83
  chai.Assertion.prototype.assert = function (...args) {
23
- if (isInSoftTest) {
24
- try {
25
- // Call the original assert
26
- originalAssert.apply(this, args);
27
- }
28
- catch (error) {
29
- // Capture the error instead of throwing
30
- softAssertionErrors.push({
31
- message: error.message,
32
- stack: error.stack
33
- });
34
- // Don't throw - let the test continue
84
+ if (!isInSoftTest) {
85
+ return originalAssert.apply(this, args);
86
+ }
87
+ const context = getAssertionContext();
88
+ try {
89
+ const result = originalAssert.apply(this, args);
90
+ if (context.commandId) {
91
+ retryWindows.delete(context.commandId);
35
92
  }
93
+ return result;
36
94
  }
37
- else {
38
- // Normal behavior for regular tests
39
- originalAssert.apply(this, args);
95
+ catch (rawError) {
96
+ const error = formatSoftAssertionError(rawError, context.timeout, context.isRetriable);
97
+ if (context.isRetriable && context.commandId) {
98
+ const currentWindow = retryWindows.get(context.commandId) || {
99
+ startedAt: Date.now(),
100
+ timeout: context.timeout,
101
+ };
102
+ retryWindows.set(context.commandId, currentWindow);
103
+ const elapsed = Date.now() - currentWindow.startedAt;
104
+ if (elapsed < currentWindow.timeout) {
105
+ throw rawError;
106
+ }
107
+ retryWindows.delete(context.commandId);
108
+ }
109
+ captureSoftAssertion(error);
110
+ return;
40
111
  }
41
112
  };
42
113
  }
43
114
  /**
44
- * Restore original Chai assertion behavior
115
+ * Restore original Chai assertion behavior.
45
116
  */
46
117
  function restoreAssertions() {
118
+ retryWindows.clear();
47
119
  if (originalAssert) {
48
120
  chai.Assertion.prototype.assert = originalAssert;
49
121
  }
@@ -51,7 +123,7 @@ function restoreAssertions() {
51
123
  /**
52
124
  * Report all collected soft assertion failures
53
125
  */
54
- function reportSoftAssertionFailures() {
126
+ function buildSoftAssertionError() {
55
127
  if (softAssertionErrors.length > 0) {
56
128
  const errorMessages = softAssertionErrors
57
129
  .map((entry, index) => ` ${index + 1}. ${entry.message}`)
@@ -66,13 +138,78 @@ function reportSoftAssertionFailures() {
66
138
  ''
67
139
  ].join('\n');
68
140
  // Clear errors
69
- const errorCount = softAssertionErrors.length;
70
141
  softAssertionErrors = [];
71
- // Throw the aggregated error
72
142
  const error = new Error(finalMessage);
73
143
  error.name = 'SoftAssertionError';
74
- throw error;
144
+ return error;
75
145
  }
146
+ return null;
147
+ }
148
+ /**
149
+ * Cleanup soft assertion state and report accumulated failures.
150
+ */
151
+ function finalizeSoftTest() {
152
+ isInSoftTest = false;
153
+ restoreAssertions();
154
+ return buildSoftAssertionError();
155
+ }
156
+ /**
157
+ * Cleanup soft assertion state without reporting (used on hard failures).
158
+ */
159
+ function abortSoftTest() {
160
+ isInSoftTest = false;
161
+ restoreAssertions();
162
+ }
163
+ /**
164
+ * Create a soft_it variant from a Mocha it function.
165
+ */
166
+ function createSoftIt(baseIt) {
167
+ return function (title, fn) {
168
+ return baseIt(title, function () {
169
+ isInSoftTest = true;
170
+ softAssertionErrors = [];
171
+ setupSoftAssertions();
172
+ try {
173
+ const result = fn.call(this);
174
+ if (result && typeof result.then === 'function') {
175
+ return result
176
+ .catch((error) => {
177
+ if ((error === null || error === void 0 ? void 0 : error.name) === 'AssertionError') {
178
+ captureSoftAssertion(error);
179
+ return;
180
+ }
181
+ abortSoftTest();
182
+ throw error;
183
+ })
184
+ .then(() => cy.wrap(null).then(() => {
185
+ const finalError = finalizeSoftTest();
186
+ if (finalError) {
187
+ throw finalError;
188
+ }
189
+ }));
190
+ }
191
+ return cy.wrap(null).then(() => {
192
+ const finalError = finalizeSoftTest();
193
+ if (finalError) {
194
+ throw finalError;
195
+ }
196
+ });
197
+ }
198
+ catch (error) {
199
+ if ((error === null || error === void 0 ? void 0 : error.name) === 'AssertionError') {
200
+ captureSoftAssertion(error);
201
+ return cy.wrap(null).then(() => {
202
+ const finalError = finalizeSoftTest();
203
+ if (finalError) {
204
+ throw finalError;
205
+ }
206
+ });
207
+ }
208
+ abortSoftTest();
209
+ throw error;
210
+ }
211
+ });
212
+ };
76
213
  }
77
214
  /**
78
215
  * soft_it - Define a test where all assertions are soft (non-blocking)
@@ -91,82 +228,11 @@ function reportSoftAssertionFailures() {
91
228
  * cy.get('.city').should('have.text', 'NYC'); // Won't stop if fails
92
229
  * });
93
230
  */
94
- globalThis.soft_it = function (title, fn) {
95
- return it(title, function () {
96
- // Setup soft assertion mode
97
- isInSoftTest = true;
98
- softAssertionErrors = [];
99
- setupSoftAssertions();
100
- // Wrap the test function execution
101
- const executeTest = () => {
102
- try {
103
- const result = fn.call(this);
104
- // Handle async tests (Cypress tests return undefined, but we need to check after cy commands)
105
- if (result && typeof result.then === 'function') {
106
- return result.then(() => {
107
- // Test completed - check for errors at the end
108
- return cy.wrap(null).then(() => {
109
- isInSoftTest = false;
110
- reportSoftAssertionFailures();
111
- });
112
- }, (err) => {
113
- isInSoftTest = false;
114
- restoreAssertions();
115
- throw err;
116
- });
117
- }
118
- // For Cypress tests, we need to hook into the end of the command chain
119
- return cy.wrap(null).then(() => {
120
- isInSoftTest = false;
121
- reportSoftAssertionFailures();
122
- });
123
- }
124
- catch (err) {
125
- isInSoftTest = false;
126
- restoreAssertions();
127
- throw err;
128
- }
129
- };
130
- return executeTest();
131
- });
132
- };
231
+ globalThis.soft_it = createSoftIt(it);
133
232
  /**
134
233
  * soft_it.only - Run only this soft test
135
234
  */
136
- globalThis.soft_it.only = function (title, fn) {
137
- return it.only(title, function () {
138
- isInSoftTest = true;
139
- softAssertionErrors = [];
140
- setupSoftAssertions();
141
- const executeTest = () => {
142
- try {
143
- const result = fn.call(this);
144
- if (result && typeof result.then === 'function') {
145
- return result.then(() => {
146
- return cy.wrap(null).then(() => {
147
- isInSoftTest = false;
148
- reportSoftAssertionFailures();
149
- });
150
- }, (err) => {
151
- isInSoftTest = false;
152
- restoreAssertions();
153
- throw err;
154
- });
155
- }
156
- return cy.wrap(null).then(() => {
157
- isInSoftTest = false;
158
- reportSoftAssertionFailures();
159
- });
160
- }
161
- catch (err) {
162
- isInSoftTest = false;
163
- restoreAssertions();
164
- throw err;
165
- }
166
- };
167
- return executeTest();
168
- });
169
- };
235
+ globalThis.soft_it.only = createSoftIt(it.only);
170
236
  /**
171
237
  * soft_it.skip - Skip this soft test
172
238
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koenvanbelle/cypress-soft-assertions",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",