@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 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 for up to 75% of the effective timeout before being captured as soft failures. For slow applications, increase the timeout to give assertions more time to pass:
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 this soft test in strict mode (force unrecoverable failure)
43
+ * Run a soft test that is expected to finish with a SoftAssertionError.
44
44
  */
45
- function strict(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
46
- namespace strict {
47
- /** Run only this strict soft test */
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 forceFailCurrentSoftTest = false;
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
- forceFailCurrentSoftTest = false;
212
+ expectSoftFailureCurrentSoftTest = false;
233
213
  restoreAssertions();
234
214
  return buildSoftAssertionError();
235
215
  }
236
216
  function abortSoftTest() {
237
217
  isInSoftTest = false;
238
- forceFailCurrentSoftTest = false;
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
- forceFailCurrentSoftTest = Boolean(options === null || options === void 0 ? void 0 : options.strict) || shouldForceFailSoftAssertions();
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.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.
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.strict = createSoftIt(it, { strict: true });
268
+ globalThis.soft_it.expectFailure = createSoftIt(it, { expectFailure: true });
291
269
  /**
292
- * soft_it.strict.only - Run only this strict soft test
270
+ * soft_it.expectFailure.only - Run only this expected-failure soft test.
293
271
  */
294
- globalThis.soft_it.strict.only = createSoftIt(it.only, { strict: true });
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
- 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();
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.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",