@koenvanbelle/cypress-soft-assertions 2.0.1 → 2.1.1

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 +67 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -64,6 +64,29 @@ function toTokenPart(value) {
64
64
  return String(value);
65
65
  }
66
66
  }
67
+ function getRetryableCommandId() {
68
+ var _a, _b, _c, _d, _e, _f, _g;
69
+ try {
70
+ const current = cy.state('current');
71
+ if (!current)
72
+ return '';
73
+ // In Cypress 15, inside a .should()/.and() callback, cy.state('current')
74
+ // points to the parent command (e.g. 'wrap', 'window', 'get'). The
75
+ // 'followedByShouldCallback' attribute is set to true, and
76
+ // 'currentAssertionCommand' points to the should/and command object.
77
+ const assertionCmd = (_a = current.get) === null || _a === void 0 ? void 0 : _a.call(current, 'currentAssertionCommand');
78
+ if (assertionCmd) {
79
+ const id = (_d = (_c = (_b = assertionCmd.get) === null || _b === void 0 ? void 0 : _b.call(assertionCmd, 'id')) !== null && _c !== void 0 ? _c : assertionCmd.id) !== null && _d !== void 0 ? _d : '';
80
+ return id ? String(id) : '';
81
+ }
82
+ if ((_e = current.get) === null || _e === void 0 ? void 0 : _e.call(current, 'followedByShouldCallback')) {
83
+ const id = (_g = (_f = current.get) === null || _f === void 0 ? void 0 : _f.call(current, 'id')) !== null && _g !== void 0 ? _g : '';
84
+ return id ? String(id) : '';
85
+ }
86
+ }
87
+ catch ( /* ignore */_h) { /* ignore */ }
88
+ return '';
89
+ }
67
90
  function getAssertionToken(assertionContext, args) {
68
91
  const subjectKey = getSubjectKey(assertionContext);
69
92
  if (!subjectKey)
@@ -94,6 +117,13 @@ function patchedAssertionAssert(...args) {
94
117
  retryAssertionFailures.delete(token);
95
118
  retryFirstSeen.delete(token);
96
119
  }
120
+ // Also clear command-based fallback tokens for this command
121
+ const retryableCid = getRetryableCommandId();
122
+ if (retryableCid) {
123
+ const fallbackToken = `__cmd__|${retryableCid}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
124
+ retryAssertionFailures.delete(fallbackToken);
125
+ retryFirstSeen.delete(fallbackToken);
126
+ }
97
127
  }
98
128
  return result;
99
129
  }
@@ -108,27 +138,31 @@ function patchedAssertionAssert(...args) {
108
138
  message: String((error === null || error === void 0 ? void 0 : error.message) || error),
109
139
  stack: error === null || error === void 0 ? void 0 : error.stack,
110
140
  });
111
- const now = Date.now();
112
141
  if (!retryFirstSeen.has(token)) {
113
- retryFirstSeen.set(token, now);
142
+ retryFirstSeen.set(token, Date.now());
114
143
  }
115
- const elapsed = now - retryFirstSeen.get(token);
116
- const timeout = getEffectiveTimeout();
117
- // Use 75% of the timeout as the retry window. This ensures the plugin
118
- // swallows the error before Cypress's own timeout fires (which would
119
- // route through the fail handler and prevent finalization).
120
- if (elapsed < timeout * 0.75) {
121
- // Still within the command timeout window rethrow to let Cypress
122
- // retry the assertion. If it eventually passes, the token is cleared.
123
- throw error;
144
+ // Always rethrow to let Cypress use its full retry/timeout window.
145
+ // When the timeout expires, Cypress fires the fail event; our fail
146
+ // handler captures it and clears the staged entry to avoid duplicates.
147
+ throw error;
148
+ }
149
+ // No identifiable subject from the DOM. If we're inside a retryable
150
+ // command (.should() / .and()), derive a token from the command ID so the
151
+ // retry-window logic still applies (e.g. window property assertions).
152
+ const retryableCid = getRetryableCommandId();
153
+ if (retryableCid) {
154
+ const fallbackToken = `__cmd__|${retryableCid}|${toTokenPart(args === null || args === void 0 ? void 0 : args[3])}`;
155
+ retryAssertionFailures.set(fallbackToken, {
156
+ message: String((error === null || error === void 0 ? void 0 : error.message) || error),
157
+ stack: error === null || error === void 0 ? void 0 : error.stack,
158
+ });
159
+ if (!retryFirstSeen.has(fallbackToken)) {
160
+ retryFirstSeen.set(fallbackToken, Date.now());
124
161
  }
125
- // Past the timeout budget swallow so the command "succeeds" and
126
- // Cypress moves on to the next queued command. The token stays in the
127
- // Map and will be promoted to softAssertionErrors at finalization.
128
- return;
162
+ // Always rethrow to let Cypress use its full retry/timeout window.
163
+ throw error;
129
164
  }
130
- // No identifiable subject — capture directly (e.g. bare expect() calls
131
- // in .then() callbacks).
165
+ // Bare expect() in .then() callbacks — capture directly.
132
166
  captureSoftAssertion(error);
133
167
  }
134
168
  }
@@ -144,6 +178,16 @@ function setupSoftAssertions() {
144
178
  // Non-assertion command failures (e.g. element not found timeouts)
145
179
  // are captured as soft failures.
146
180
  captureSoftAssertion(error);
181
+ // Clear any matching retry-tracked entry to prevent double-counting
182
+ // at finalization (the fail handler is now the authoritative capture).
183
+ const errorMsg = (error === null || error === void 0 ? void 0 : error.message) || String(error);
184
+ for (const [token, entry] of retryAssertionFailures.entries()) {
185
+ if (entry.message === errorMsg) {
186
+ retryAssertionFailures.delete(token);
187
+ retryFirstSeen.delete(token);
188
+ break;
189
+ }
190
+ }
147
191
  return false;
148
192
  };
149
193
  Cypress.on('fail', activeFailHandler);
@@ -162,9 +206,13 @@ function restoreAssertions() {
162
206
  }
163
207
  }
164
208
  function buildSoftAssertionError() {
165
- // Promote any remaining retry-tracked failures.
209
+ // Promote any remaining retry-tracked failures that weren't already
210
+ // captured by the fail handler (dedup by message).
166
211
  for (const entry of retryAssertionFailures.values()) {
167
- captureSoftAssertion(entry);
212
+ const isDuplicate = softAssertionErrors.some(e => e.message === entry.message);
213
+ if (!isDuplicate) {
214
+ softAssertionErrors.push(entry);
215
+ }
168
216
  }
169
217
  retryAssertionFailures.clear();
170
218
  retryFirstSeen.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koenvanbelle/cypress-soft-assertions",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
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",