@koenvanbelle/cypress-soft-assertions 2.3.1 → 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 }
package/dist/index.d.ts CHANGED
@@ -39,6 +39,14 @@ declare global {
39
39
  * Run only this test (like it.only)
40
40
  */
41
41
  function only(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
42
+ /**
43
+ * Run a soft test that is expected to finish with a SoftAssertionError.
44
+ */
45
+ function expectFailure(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
46
+ namespace expectFailure {
47
+ /** Run only this expected-failure soft test */
48
+ function only(title: string, fn: Mocha.Func | Mocha.AsyncFunc): Mocha.Test;
49
+ }
42
50
  /**
43
51
  * Skip this test (like it.skip)
44
52
  */
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ let softAssertionErrors = [];
20
20
  let retryAssertionFailures = new Map();
21
21
  let retryFirstSeen = new Map();
22
22
  let isInSoftTest = false;
23
+ let expectSoftFailureCurrentSoftTest = false;
23
24
  let activeFailHandler = null;
24
25
  let originalChaiAssert = null;
25
26
  // ---------------------------------------------------------------------------
@@ -208,19 +209,22 @@ function buildSoftAssertionError() {
208
209
  }
209
210
  function finalizeSoftTest() {
210
211
  isInSoftTest = false;
212
+ expectSoftFailureCurrentSoftTest = false;
211
213
  restoreAssertions();
212
214
  return buildSoftAssertionError();
213
215
  }
214
216
  function abortSoftTest() {
215
217
  isInSoftTest = false;
218
+ expectSoftFailureCurrentSoftTest = false;
216
219
  restoreAssertions();
217
220
  retryAssertionFailures.clear();
218
221
  retryFirstSeen.clear();
219
222
  }
220
- function createSoftIt(baseIt) {
223
+ function createSoftIt(baseIt, options) {
221
224
  return function (title, fn) {
222
225
  return baseIt(title, function () {
223
226
  isInSoftTest = true;
227
+ expectSoftFailureCurrentSoftTest = Boolean(options === null || options === void 0 ? void 0 : options.expectFailure);
224
228
  softAssertionErrors = [];
225
229
  retryAssertionFailures.clear();
226
230
  retryFirstSeen.clear();
@@ -257,6 +261,15 @@ globalThis.soft_it = createSoftIt(it);
257
261
  * soft_it.only - Run only this soft test
258
262
  */
259
263
  globalThis.soft_it.only = createSoftIt(it.only);
264
+ /**
265
+ * soft_it.expectFailure - Run a soft test that is expected to aggregate
266
+ * into a final SoftAssertionError without failing the enclosing behavior spec.
267
+ */
268
+ globalThis.soft_it.expectFailure = createSoftIt(it, { expectFailure: true });
269
+ /**
270
+ * soft_it.expectFailure.only - Run only this expected-failure soft test.
271
+ */
272
+ globalThis.soft_it.expectFailure.only = createSoftIt(it.only, { expectFailure: true });
260
273
  /**
261
274
  * soft_it.skip - Skip this soft test
262
275
  */
@@ -265,29 +278,34 @@ globalThis.soft_it.skip = it.skip;
265
278
  // Registered at the root level so it applies to all suites.
266
279
  // For non-soft tests (isInSoftTest === false), this is a no-op.
267
280
  afterEach(function () {
268
- var _a, _b, _c;
269
281
  if (!isInSoftTest)
270
282
  return;
283
+ const expectsSoftFailure = expectSoftFailureCurrentSoftTest;
271
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
+ }
272
291
  if (finalError) {
273
292
  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();
286
- if (runner && test) {
287
- runner.fail(test, finalError);
288
- }
289
- else {
290
- 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;
291
309
  }
292
310
  }
293
311
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koenvanbelle/cypress-soft-assertions",
3
- "version": "2.3.1",
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",