@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.
- package/dist/index.js +162 -96
- 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
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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",
|