@koenvanbelle/cypress-soft-assertions 2.3.2 → 2.4.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 +18 -31
- package/dist/index.d.ts +4 -6
- package/dist/index.js +32 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,23 @@ describe('Test Suite', () => {
|
|
|
130
130
|
});
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
### `soft_it.expectFailure`
|
|
134
|
+
|
|
135
|
+
Use `soft_it.expectFailure()` for browser behavior specs that intentionally trigger
|
|
136
|
+
soft failures but should still finish green overall.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
soft_it.expectFailure('captures a known soft failure without failing the spec', () => {
|
|
140
|
+
cy.get('#title').should('have.text', 'Wrong Title');
|
|
141
|
+
cy.get('#status').should('have.text', 'Ready');
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This mode still verifies that a final `SoftAssertionError` was produced. If the
|
|
146
|
+
test completes without any soft failure, the test fails.
|
|
147
|
+
|
|
148
|
+
`soft_it.expectFailure.only(...)` is also available.
|
|
149
|
+
|
|
133
150
|
## How it works
|
|
134
151
|
|
|
135
152
|
1. `soft_it()` patches Chai's assertion mechanism for the duration of the test.
|
|
@@ -158,7 +175,7 @@ SOFT ASSERTION FAILURES (3 failed):
|
|
|
158
175
|
- **Retriable assertions are retried**: `.should()` and `.and()` assertions are retried by Cypress before being captured as soft failures. Assertions that eventually pass are not reported.
|
|
159
176
|
- **Non-assertion errors still stop execution**: Network errors, visit timeouts, and other command errors are captured as soft failures but may prevent subsequent commands from running.
|
|
160
177
|
- **State resets between tests**: Each `soft_it()` test starts with a clean slate — failures from one test never leak into another.
|
|
161
|
-
- **Timeout-aware retries**: The plugin respects Cypress's `defaultCommandTimeout` and per-command `{ timeout }` overrides. Retriable assertions (`.should()`, `.and()`) are retried
|
|
178
|
+
- **Timeout-aware retries**: The plugin respects Cypress's `defaultCommandTimeout` and per-command `{ timeout }` overrides. Retriable assertions (`.should()`, `.and()`) are retried until just before the effective timeout expires, using `max(timeout - 100ms, timeout * 0.9)` as the swallow threshold. For slow applications, increase the timeout to give assertions more time to pass:
|
|
162
179
|
```typescript
|
|
163
180
|
// Global: set in cypress.config.ts
|
|
164
181
|
e2e: { defaultCommandTimeout: 10000 }
|
|
@@ -249,36 +266,6 @@ soft_it('works with automatic translation checks', () => {
|
|
|
249
266
|
- Both plugins register hooks/events, but they can run together safely with the setup above.
|
|
250
267
|
- Keep translation-checker in non-invasive mode (`failOnError: false` in auto mode) so functional assertions remain the source of test pass/fail behavior.
|
|
251
268
|
|
|
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
|
-
|
|
282
269
|
## License
|
|
283
270
|
|
|
284
271
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -40,14 +40,12 @@ declare global {
|
|
|
40
40
|
*/
|
|
41
41
|
function only(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
|
|
42
42
|
/**
|
|
43
|
-
* Run
|
|
43
|
+
* Run a soft test that is expected to finish with a SoftAssertionError.
|
|
44
44
|
*/
|
|
45
|
-
function
|
|
46
|
-
namespace
|
|
47
|
-
/** Run only this
|
|
45
|
+
function expectFailure(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
|
|
46
|
+
namespace expectFailure {
|
|
47
|
+
/** Run only this expected-failure soft test */
|
|
48
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
49
|
}
|
|
52
50
|
/**
|
|
53
51
|
* Skip this test (like it.skip)
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ let softAssertionErrors = [];
|
|
|
20
20
|
let retryAssertionFailures = new Map();
|
|
21
21
|
let retryFirstSeen = new Map();
|
|
22
22
|
let isInSoftTest = false;
|
|
23
|
-
let
|
|
23
|
+
let expectSoftFailureCurrentSoftTest = false;
|
|
24
24
|
let activeFailHandler = null;
|
|
25
25
|
let originalChaiAssert = null;
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -42,26 +42,6 @@ function getEffectiveTimeout() {
|
|
|
42
42
|
return 4000;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
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';
|
|
52
|
-
}
|
|
53
|
-
function shouldForceFailSoftAssertions() {
|
|
54
|
-
var _a;
|
|
55
|
-
try {
|
|
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'));
|
|
60
|
-
}
|
|
61
|
-
catch (_b) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
45
|
// ---------------------------------------------------------------------------
|
|
66
46
|
// Internal helpers (Cypress-dependent — not unit-testable in isolation)
|
|
67
47
|
// ---------------------------------------------------------------------------
|
|
@@ -229,13 +209,13 @@ function buildSoftAssertionError() {
|
|
|
229
209
|
}
|
|
230
210
|
function finalizeSoftTest() {
|
|
231
211
|
isInSoftTest = false;
|
|
232
|
-
|
|
212
|
+
expectSoftFailureCurrentSoftTest = false;
|
|
233
213
|
restoreAssertions();
|
|
234
214
|
return buildSoftAssertionError();
|
|
235
215
|
}
|
|
236
216
|
function abortSoftTest() {
|
|
237
217
|
isInSoftTest = false;
|
|
238
|
-
|
|
218
|
+
expectSoftFailureCurrentSoftTest = false;
|
|
239
219
|
restoreAssertions();
|
|
240
220
|
retryAssertionFailures.clear();
|
|
241
221
|
retryFirstSeen.clear();
|
|
@@ -244,7 +224,7 @@ function createSoftIt(baseIt, options) {
|
|
|
244
224
|
return function (title, fn) {
|
|
245
225
|
return baseIt(title, function () {
|
|
246
226
|
isInSoftTest = true;
|
|
247
|
-
|
|
227
|
+
expectSoftFailureCurrentSoftTest = Boolean(options === null || options === void 0 ? void 0 : options.expectFailure);
|
|
248
228
|
softAssertionErrors = [];
|
|
249
229
|
retryAssertionFailures.clear();
|
|
250
230
|
retryFirstSeen.clear();
|
|
@@ -282,57 +262,50 @@ globalThis.soft_it = createSoftIt(it);
|
|
|
282
262
|
*/
|
|
283
263
|
globalThis.soft_it.only = createSoftIt(it.only);
|
|
284
264
|
/**
|
|
285
|
-
* soft_it.
|
|
286
|
-
*
|
|
287
|
-
* Strict mode throws the final SoftAssertionError from afterEach on the
|
|
288
|
-
* last attempt, making the failure unrecoverable by downstream hooks.
|
|
265
|
+
* soft_it.expectFailure - Run a soft test that is expected to aggregate
|
|
266
|
+
* into a final SoftAssertionError without failing the enclosing behavior spec.
|
|
289
267
|
*/
|
|
290
|
-
globalThis.soft_it.
|
|
268
|
+
globalThis.soft_it.expectFailure = createSoftIt(it, { expectFailure: true });
|
|
291
269
|
/**
|
|
292
|
-
* soft_it.
|
|
270
|
+
* soft_it.expectFailure.only - Run only this expected-failure soft test.
|
|
293
271
|
*/
|
|
294
|
-
globalThis.soft_it.
|
|
272
|
+
globalThis.soft_it.expectFailure.only = createSoftIt(it.only, { expectFailure: true });
|
|
295
273
|
/**
|
|
296
274
|
* soft_it.skip - Skip this soft test
|
|
297
275
|
*/
|
|
298
276
|
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;
|
|
303
277
|
// Global afterEach hook: finalize soft assertions after each test.
|
|
304
278
|
// Registered at the root level so it applies to all suites.
|
|
305
279
|
// For non-soft tests (isInSoftTest === false), this is a no-op.
|
|
306
280
|
afterEach(function () {
|
|
307
|
-
var _a, _b, _c;
|
|
308
281
|
if (!isInSoftTest)
|
|
309
282
|
return;
|
|
283
|
+
const expectsSoftFailure = expectSoftFailureCurrentSoftTest;
|
|
310
284
|
const finalError = finalizeSoftTest();
|
|
285
|
+
if (expectsSoftFailure) {
|
|
286
|
+
if (!finalError) {
|
|
287
|
+
throw new Error('Expected SoftAssertionError but soft_it.expectFailure test completed without one.');
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
311
291
|
if (finalError) {
|
|
312
292
|
const test = this.currentTest;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
const runner = (_c = Cypress.mocha) === null || _c === void 0 ? void 0 : _c.getRunner();
|
|
331
|
-
if (runner && test) {
|
|
332
|
-
runner.fail(test, finalError);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
throw finalError;
|
|
293
|
+
if (test) {
|
|
294
|
+
test.err = finalError;
|
|
295
|
+
test._cypressTestStatusInfo = {
|
|
296
|
+
outerStatus: 'failed',
|
|
297
|
+
shouldAttemptsContinue: false,
|
|
298
|
+
attempts: typeof test.currentRetry === 'function' ? test.currentRetry() + 1 : 1,
|
|
299
|
+
strategy: 'detect-flake-and-pass-on-threshold',
|
|
300
|
+
};
|
|
301
|
+
const prevAttempts = Array.isArray(test.prevAttempts) ? test.prevAttempts : [];
|
|
302
|
+
if (!prevAttempts.some((attempt) => (attempt === null || attempt === void 0 ? void 0 : attempt.state) === 'failed' && (attempt === null || attempt === void 0 ? void 0 : attempt.err))) {
|
|
303
|
+
prevAttempts.unshift({
|
|
304
|
+
state: 'failed',
|
|
305
|
+
err: finalError,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
test.prevAttempts = prevAttempts;
|
|
336
309
|
}
|
|
337
310
|
}
|
|
338
311
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koenvanbelle/cypress-soft-assertions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|