@qualflare/cypress 0.1.0
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/LICENSE +190 -0
- package/README.md +101 -0
- package/dist/index.cjs +931 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +904 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/index.cjs +863 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +101 -0
- package/dist/plugin/index.d.ts +101 -0
- package/dist/plugin/index.js +825 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/types-BKTS9yBY.d.cts +5 -0
- package/dist/types-BKTS9yBY.d.ts +5 -0
- package/package.json +81 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
qualflare: () => qualflare
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(src_exports);
|
|
26
|
+
|
|
27
|
+
// src/browser/browser-integration-guard.ts
|
|
28
|
+
function initializeBrowserIntegration(register) {
|
|
29
|
+
const target = typeof Cypress === "undefined" ? void 0 : Cypress;
|
|
30
|
+
if (target?.__qualflareBrowserRegistered) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (target) {
|
|
34
|
+
target.__qualflareBrowserRegistered = true;
|
|
35
|
+
}
|
|
36
|
+
register();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/shared/constants.ts
|
|
40
|
+
var TASK_REPORT_CASE = "qualflareReportCase";
|
|
41
|
+
var TASK_MARK_TEST_PHASE_STARTED = "qualflareMarkTestPhaseStarted";
|
|
42
|
+
var MAX_PARAMETERS_PER_STEP = 50;
|
|
43
|
+
var MAX_ATTACHMENTS_PER_CASE = 50;
|
|
44
|
+
var MAX_LABELS_PER_CASE = 100;
|
|
45
|
+
var MAX_LINKS_PER_CASE = 20;
|
|
46
|
+
var MAX_TAGS_PER_CASE = 64;
|
|
47
|
+
var MAX_TAG_LENGTH = 255;
|
|
48
|
+
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
49
|
+
|
|
50
|
+
// src/shared/duration.ts
|
|
51
|
+
var NS_PER_MS = 1e6;
|
|
52
|
+
function msToNs(ms) {
|
|
53
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
return Math.round(ms * NS_PER_MS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/shared/logger.ts
|
|
60
|
+
var PREFIX = "[qualflare-cypress]";
|
|
61
|
+
var logger = {
|
|
62
|
+
debug(...args) {
|
|
63
|
+
console.debug(PREFIX, ...args);
|
|
64
|
+
},
|
|
65
|
+
info(...args) {
|
|
66
|
+
console.log(PREFIX, ...args);
|
|
67
|
+
},
|
|
68
|
+
warn(...args) {
|
|
69
|
+
console.warn(PREFIX, ...args);
|
|
70
|
+
},
|
|
71
|
+
error(...args) {
|
|
72
|
+
console.error(PREFIX, ...args);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/browser/console-props.ts
|
|
77
|
+
var MAX_VALUE_CHARS = 500;
|
|
78
|
+
var MAX_STRINGIFY_DEPTH = 3;
|
|
79
|
+
function safeStringify(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
80
|
+
if (value === null) return "null";
|
|
81
|
+
if (value === void 0) return "undefined";
|
|
82
|
+
const type = typeof value;
|
|
83
|
+
if (type === "string") {
|
|
84
|
+
return truncate(value);
|
|
85
|
+
}
|
|
86
|
+
if (type === "number" || type === "boolean" || type === "bigint") {
|
|
87
|
+
return truncate(String(value));
|
|
88
|
+
}
|
|
89
|
+
if (type === "function") {
|
|
90
|
+
return "[function]";
|
|
91
|
+
}
|
|
92
|
+
if (type === "symbol") {
|
|
93
|
+
return value.toString();
|
|
94
|
+
}
|
|
95
|
+
const obj = value;
|
|
96
|
+
if (seen.has(obj)) {
|
|
97
|
+
return "[circular]";
|
|
98
|
+
}
|
|
99
|
+
seen.add(obj);
|
|
100
|
+
try {
|
|
101
|
+
if (isDomElementLike(obj)) {
|
|
102
|
+
return describeDomElementLike(obj);
|
|
103
|
+
}
|
|
104
|
+
if (depth >= MAX_STRINGIFY_DEPTH) {
|
|
105
|
+
return Array.isArray(obj) ? "[array]" : "[object]";
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(obj)) {
|
|
108
|
+
const items = obj.slice(0, 20).map((item) => safeStringify(item, depth + 1, seen));
|
|
109
|
+
const suffix = obj.length > 20 ? `, \u2026(${obj.length - 20} more)` : "";
|
|
110
|
+
return truncate(`[${items.join(", ")}${suffix}]`);
|
|
111
|
+
}
|
|
112
|
+
if (obj instanceof Error) {
|
|
113
|
+
return truncate(obj.message ? `${obj.name}: ${obj.message}` : obj.name);
|
|
114
|
+
}
|
|
115
|
+
const entries = Object.entries(obj).slice(0, 20);
|
|
116
|
+
const rendered = entries.map(([key, val]) => `${key}: ${safeStringify(val, depth + 1, seen)}`);
|
|
117
|
+
return truncate(`{${rendered.join(", ")}}`);
|
|
118
|
+
} catch {
|
|
119
|
+
return "[unserializable]";
|
|
120
|
+
} finally {
|
|
121
|
+
seen.delete(obj);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function truncate(text) {
|
|
125
|
+
return text.length > MAX_VALUE_CHARS ? `${text.slice(0, MAX_VALUE_CHARS)}\u2026` : text;
|
|
126
|
+
}
|
|
127
|
+
function isDomElementLike(obj) {
|
|
128
|
+
return typeof obj.tagName === "string";
|
|
129
|
+
}
|
|
130
|
+
function describeDomElementLike(el) {
|
|
131
|
+
const tag = el.tagName.toLowerCase();
|
|
132
|
+
const id = el.id ? `#${el.id}` : "";
|
|
133
|
+
const cls = typeof el.className === "string" && el.className ? `.${el.className.split(/\s+/).join(".")}` : "";
|
|
134
|
+
return `<${tag}${id}${cls}>`;
|
|
135
|
+
}
|
|
136
|
+
function buildParametersFromConsoleProps(consoleProps) {
|
|
137
|
+
let resolved = consoleProps;
|
|
138
|
+
if (typeof resolved === "function") {
|
|
139
|
+
try {
|
|
140
|
+
resolved = resolved();
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (resolved === null || typeof resolved !== "object") {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const outer = resolved;
|
|
149
|
+
const nestedProps = outer.props;
|
|
150
|
+
const source = nestedProps !== null && typeof nestedProps === "object" && !Array.isArray(nestedProps) ? nestedProps : outer;
|
|
151
|
+
const parameters = [];
|
|
152
|
+
for (const [name, value] of Object.entries(source)) {
|
|
153
|
+
if (typeof value === "function") {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
parameters.push({ name, value: safeStringify(value) });
|
|
157
|
+
if (parameters.length >= MAX_PARAMETERS_PER_STEP) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return parameters;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/browser/command-log-listener.ts
|
|
165
|
+
var MAX_STEP_MESSAGE_CHARS = 200;
|
|
166
|
+
function describeMessage(message) {
|
|
167
|
+
if (typeof message !== "string") {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
const trimmed = message.trim();
|
|
171
|
+
if (trimmed.length === 0) {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
return trimmed.length > MAX_STEP_MESSAGE_CHARS ? `${trimmed.slice(0, MAX_STEP_MESSAGE_CHARS)}\u2026` : trimmed;
|
|
175
|
+
}
|
|
176
|
+
function formatLogError(err) {
|
|
177
|
+
if (err === void 0 || err === null) {
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
if (typeof err === "string") {
|
|
181
|
+
return err;
|
|
182
|
+
}
|
|
183
|
+
if (err instanceof Error) {
|
|
184
|
+
return err.stack ? `${err.message}
|
|
185
|
+
${err.stack}` : err.message;
|
|
186
|
+
}
|
|
187
|
+
return err.stack ? `${err.message ?? ""}
|
|
188
|
+
${err.stack}`.trim() : err.message;
|
|
189
|
+
}
|
|
190
|
+
function mapLogState(state) {
|
|
191
|
+
if (state === "failed") return "failed";
|
|
192
|
+
if (state === "passed") return "passed";
|
|
193
|
+
return "pending";
|
|
194
|
+
}
|
|
195
|
+
var CommandLogBuffer = class {
|
|
196
|
+
records = [];
|
|
197
|
+
indexById = /* @__PURE__ */ new Map();
|
|
198
|
+
lastParentIndex;
|
|
199
|
+
capWarned = false;
|
|
200
|
+
handleAdded(attrs, now = Date.now()) {
|
|
201
|
+
if (!attrs.name) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (this.records.length >= MAX_STEPS_PER_TEST_ATTEMPT) {
|
|
205
|
+
if (!this.capWarned) {
|
|
206
|
+
this.capWarned = true;
|
|
207
|
+
logger.warn(
|
|
208
|
+
`reached the ${MAX_STEPS_PER_TEST_ATTEMPT}-step-per-test soft cap \u2014 further command-log entries for this test attempt will not be recorded.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const index = this.records.length;
|
|
214
|
+
const parentIndex = attrs.type === "child" ? this.lastParentIndex : void 0;
|
|
215
|
+
if (attrs.type === "parent") {
|
|
216
|
+
this.lastParentIndex = index;
|
|
217
|
+
}
|
|
218
|
+
const message = describeMessage(attrs.message);
|
|
219
|
+
const name = message ? `${attrs.name} ${message}` : attrs.name;
|
|
220
|
+
this.records.push({
|
|
221
|
+
name,
|
|
222
|
+
keyword: attrs.displayName && attrs.displayName !== attrs.name ? attrs.displayName : void 0,
|
|
223
|
+
status: mapLogState(attrs.state),
|
|
224
|
+
error: formatLogError(attrs.err),
|
|
225
|
+
parentIndex,
|
|
226
|
+
startedAt: now,
|
|
227
|
+
lastSeenAt: now,
|
|
228
|
+
consoleProps: attrs.consoleProps
|
|
229
|
+
});
|
|
230
|
+
if (attrs.id) {
|
|
231
|
+
this.indexById.set(attrs.id, index);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
handleChanged(attrs, now = Date.now()) {
|
|
235
|
+
const index = attrs.id ? this.indexById.get(attrs.id) : void 0;
|
|
236
|
+
if (index === void 0) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const record = this.records[index];
|
|
240
|
+
if (!record) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
record.lastSeenAt = now;
|
|
244
|
+
if (attrs.state !== void 0) {
|
|
245
|
+
record.status = mapLogState(attrs.state);
|
|
246
|
+
}
|
|
247
|
+
if (attrs.err !== void 0) {
|
|
248
|
+
record.error = formatLogError(attrs.err);
|
|
249
|
+
}
|
|
250
|
+
if (attrs.consoleProps !== void 0) {
|
|
251
|
+
record.consoleProps = attrs.consoleProps;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Returns this attempt's steps as wire-shaped `Step[]` (durations already
|
|
255
|
+
* converted to nanoseconds — unlike `Case`-level duration, which stays in
|
|
256
|
+
* milliseconds until `queue.ts`, `Step` has no separate internal/ms-shaped
|
|
257
|
+
* representation elsewhere in this codebase, so there's no benefit to
|
|
258
|
+
* threading one through here only to convert it later) and clears the
|
|
259
|
+
* buffer for the next attempt. */
|
|
260
|
+
drain() {
|
|
261
|
+
const steps = this.records.map((record) => {
|
|
262
|
+
const step = {
|
|
263
|
+
name: record.name,
|
|
264
|
+
status: record.status,
|
|
265
|
+
duration: msToNs(Math.max(0, record.lastSeenAt - record.startedAt))
|
|
266
|
+
};
|
|
267
|
+
if (record.keyword) step.keyword = record.keyword;
|
|
268
|
+
if (record.error) step.error = record.error;
|
|
269
|
+
if (record.location) step.location = record.location;
|
|
270
|
+
if (record.parentIndex !== void 0) step.parentIndex = record.parentIndex;
|
|
271
|
+
const parameters = buildParametersFromConsoleProps(record.consoleProps);
|
|
272
|
+
if (parameters.length > 0) step.parameters = parameters;
|
|
273
|
+
return step;
|
|
274
|
+
});
|
|
275
|
+
this.reset();
|
|
276
|
+
return steps;
|
|
277
|
+
}
|
|
278
|
+
/** Starts a fresh attempt: clears all recorded steps and nesting state.
|
|
279
|
+
* Steps from an abandoned (retried) attempt are discarded, never merged
|
|
280
|
+
* into the next attempt's buffer — see `mocha-listener.ts`, which calls
|
|
281
|
+
* this on every `runner.on('test', ...)` (fired once per attempt,
|
|
282
|
+
* including retries) so only the FINAL attempt's steps ever reach
|
|
283
|
+
* `drain()`. */
|
|
284
|
+
reset() {
|
|
285
|
+
this.records = [];
|
|
286
|
+
this.indexById.clear();
|
|
287
|
+
this.lastParentIndex = void 0;
|
|
288
|
+
this.capWarned = false;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function registerCommandLogListener(buffer) {
|
|
292
|
+
Cypress.on("log:added", (attributes) => {
|
|
293
|
+
buffer.handleAdded(attributes);
|
|
294
|
+
});
|
|
295
|
+
Cypress.on("log:changed", (attributes) => {
|
|
296
|
+
buffer.handleChanged(attributes);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/browser/case-builder.ts
|
|
301
|
+
function combineSteps(autoSteps, manualSteps) {
|
|
302
|
+
const auto = autoSteps ?? [];
|
|
303
|
+
const manual = manualSteps ?? [];
|
|
304
|
+
if (auto.length === 0 && manual.length === 0) {
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
const offsetManual = manual.map((record) => {
|
|
308
|
+
const step = { name: record.name, status: record.status, duration: msToNs(record.durationMs ?? 0) };
|
|
309
|
+
if (record.error) step.error = record.error;
|
|
310
|
+
if (record.parentIndex !== void 0) step.parentIndex = record.parentIndex + auto.length;
|
|
311
|
+
if (record.parameters && record.parameters.length > 0) step.parameters = record.parameters;
|
|
312
|
+
return step;
|
|
313
|
+
});
|
|
314
|
+
return [...auto, ...offsetManual];
|
|
315
|
+
}
|
|
316
|
+
function collapseAttempts(attempts) {
|
|
317
|
+
if (attempts.length === 0) {
|
|
318
|
+
throw new Error("collapseAttempts: at least one attempt is required");
|
|
319
|
+
}
|
|
320
|
+
const final = attempts[attempts.length - 1];
|
|
321
|
+
const retryCount = attempts.length - 1;
|
|
322
|
+
const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
|
|
323
|
+
const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
|
|
324
|
+
return {
|
|
325
|
+
status: final.status,
|
|
326
|
+
duration,
|
|
327
|
+
retryCount,
|
|
328
|
+
isFlaky,
|
|
329
|
+
error: final.status === "passed" ? void 0 : final.error,
|
|
330
|
+
steps: combineSteps(final.steps, final.manualSteps),
|
|
331
|
+
labels: final.labels,
|
|
332
|
+
links: final.links,
|
|
333
|
+
tags: final.tags,
|
|
334
|
+
description: final.description,
|
|
335
|
+
priority: final.priority,
|
|
336
|
+
properties: final.properties,
|
|
337
|
+
attachments: final.attachments
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/browser/queue.ts
|
|
342
|
+
function flushCase(test, attempts) {
|
|
343
|
+
if (attempts.length === 0) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const collapsed = collapseAttempts(attempts);
|
|
347
|
+
const testCase = {
|
|
348
|
+
id: test.fullTitle(),
|
|
349
|
+
name: test.title,
|
|
350
|
+
className: test.parent?.fullTitle() || void 0,
|
|
351
|
+
status: collapsed.status,
|
|
352
|
+
duration: msToNs(collapsed.duration),
|
|
353
|
+
retryCount: collapsed.retryCount,
|
|
354
|
+
isFlaky: collapsed.isFlaky,
|
|
355
|
+
error: collapsed.error,
|
|
356
|
+
steps: collapsed.steps,
|
|
357
|
+
// qualflare.* author-facing metadata API calls (labels/links/tags/
|
|
358
|
+
// description/priority/properties from qualflare.label()/link()/tag()/
|
|
359
|
+
// description()/priority()/parameter(); attachments from
|
|
360
|
+
// qualflare.attachment()/attachmentFromFile() — the latter carry only
|
|
361
|
+
// a `path`, resolved into inline content Node-side by the existing
|
|
362
|
+
// screenshot-attachment pipeline, see tasks.ts/attachment-reader.ts)
|
|
363
|
+
// from the FINAL attempt only, same "abandoned attempts are discarded"
|
|
364
|
+
// rule as `steps`.
|
|
365
|
+
labels: collapsed.labels,
|
|
366
|
+
links: collapsed.links,
|
|
367
|
+
tags: collapsed.tags,
|
|
368
|
+
description: collapsed.description,
|
|
369
|
+
priority: collapsed.priority,
|
|
370
|
+
properties: collapsed.properties,
|
|
371
|
+
attachments: collapsed.attachments
|
|
372
|
+
};
|
|
373
|
+
cy.task(TASK_REPORT_CASE, testCase, { log: false });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/browser/test-metadata-buffer.ts
|
|
377
|
+
var TestMetadataBuffer = class {
|
|
378
|
+
active = false;
|
|
379
|
+
labels = [];
|
|
380
|
+
links = [];
|
|
381
|
+
tags = [];
|
|
382
|
+
descriptionText;
|
|
383
|
+
priorityValue;
|
|
384
|
+
properties = {};
|
|
385
|
+
attachments = [];
|
|
386
|
+
manualSteps = [];
|
|
387
|
+
manualStepStack = [];
|
|
388
|
+
cappedWarnings = /* @__PURE__ */ new Set();
|
|
389
|
+
isActive() {
|
|
390
|
+
return this.active;
|
|
391
|
+
}
|
|
392
|
+
/** Starts a fresh attempt: clears all accumulated data and marks the
|
|
393
|
+
* buffer active. Called from `mocha-listener.ts`'s `runner.on('test', ...)`
|
|
394
|
+
* — fired once per attempt, including retries. */
|
|
395
|
+
reset() {
|
|
396
|
+
this.active = true;
|
|
397
|
+
this.labels = [];
|
|
398
|
+
this.links = [];
|
|
399
|
+
this.tags = [];
|
|
400
|
+
this.descriptionText = void 0;
|
|
401
|
+
this.priorityValue = void 0;
|
|
402
|
+
this.properties = {};
|
|
403
|
+
this.attachments = [];
|
|
404
|
+
this.manualSteps = [];
|
|
405
|
+
this.manualStepStack = [];
|
|
406
|
+
this.cappedWarnings.clear();
|
|
407
|
+
}
|
|
408
|
+
/** Ends the current attempt: returns everything accumulated (undefined for
|
|
409
|
+
* any field with nothing recorded, matching this codebase's
|
|
410
|
+
* omit-rather-than-empty-array convention elsewhere), marks the buffer
|
|
411
|
+
* inactive, and clears state. */
|
|
412
|
+
drain() {
|
|
413
|
+
const snapshot = {
|
|
414
|
+
labels: this.labels.length > 0 ? this.labels : void 0,
|
|
415
|
+
links: this.links.length > 0 ? this.links : void 0,
|
|
416
|
+
tags: this.tags.length > 0 ? this.tags : void 0,
|
|
417
|
+
description: this.descriptionText,
|
|
418
|
+
priority: this.priorityValue,
|
|
419
|
+
properties: Object.keys(this.properties).length > 0 ? this.properties : void 0,
|
|
420
|
+
attachments: this.attachments.length > 0 ? this.attachments : void 0,
|
|
421
|
+
manualSteps: this.manualSteps.length > 0 ? this.manualSteps : void 0
|
|
422
|
+
};
|
|
423
|
+
this.active = false;
|
|
424
|
+
this.labels = [];
|
|
425
|
+
this.links = [];
|
|
426
|
+
this.tags = [];
|
|
427
|
+
this.descriptionText = void 0;
|
|
428
|
+
this.priorityValue = void 0;
|
|
429
|
+
this.properties = {};
|
|
430
|
+
this.attachments = [];
|
|
431
|
+
this.manualSteps = [];
|
|
432
|
+
this.manualStepStack = [];
|
|
433
|
+
return snapshot;
|
|
434
|
+
}
|
|
435
|
+
warnInactive(fnName) {
|
|
436
|
+
logger.warn(
|
|
437
|
+
`qualflare.${fnName}() was called while no test is currently running (e.g. from a before/after hook, or at module-load time) \u2014 this call has no effect.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
/** Warns at most once per (buffer lifetime, cap-name) pair, so a loop that
|
|
441
|
+
* blows through a cap doesn't spam the log once per iteration. */
|
|
442
|
+
warnCappedOnce(capName, message) {
|
|
443
|
+
if (this.cappedWarnings.has(capName)) return;
|
|
444
|
+
this.cappedWarnings.add(capName);
|
|
445
|
+
logger.warn(message);
|
|
446
|
+
}
|
|
447
|
+
label(name, value) {
|
|
448
|
+
if (!this.active) return this.warnInactive("label");
|
|
449
|
+
if (this.labels.length >= MAX_LABELS_PER_CASE) {
|
|
450
|
+
return this.warnCappedOnce(
|
|
451
|
+
"labels",
|
|
452
|
+
`reached the ${MAX_LABELS_PER_CASE}-label-per-case cap \u2014 further qualflare.label() calls this test will be dropped.`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
this.labels.push({ name, value });
|
|
456
|
+
}
|
|
457
|
+
link(url, opts) {
|
|
458
|
+
if (!this.active) return this.warnInactive("link");
|
|
459
|
+
if (this.links.length >= MAX_LINKS_PER_CASE) {
|
|
460
|
+
return this.warnCappedOnce(
|
|
461
|
+
"links",
|
|
462
|
+
`reached the ${MAX_LINKS_PER_CASE}-link-per-case cap \u2014 further qualflare.link() calls this test will be dropped.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const link = { type: opts?.type ?? "custom", url };
|
|
466
|
+
if (opts?.name) link.name = opts.name;
|
|
467
|
+
this.links.push(link);
|
|
468
|
+
}
|
|
469
|
+
/** `Case.tags` is a REJECT-not-truncate field server-side (`max=64` items,
|
|
470
|
+
* `max=255` chars each) — unlike most other caps in this file, exceeding
|
|
471
|
+
* it 400s the whole launch, not just this one test's tags. Count is
|
|
472
|
+
* enforced the same warn-and-drop-excess way as `label()`/`link()`; an
|
|
473
|
+
* individual over-length tag is truncated (not dropped) instead, since a
|
|
474
|
+
* single long string is a shortenable formatting issue, not a structural
|
|
475
|
+
* one — matching how other length-only limits elsewhere in this codebase
|
|
476
|
+
* (e.g. `console-props.ts`'s `truncate()`) are handled. */
|
|
477
|
+
tag(...tags) {
|
|
478
|
+
if (!this.active) return this.warnInactive("tag");
|
|
479
|
+
for (const rawTag of tags) {
|
|
480
|
+
if (this.tags.length >= MAX_TAGS_PER_CASE) {
|
|
481
|
+
this.warnCappedOnce(
|
|
482
|
+
"tags",
|
|
483
|
+
`reached the ${MAX_TAGS_PER_CASE}-tag-per-case cap \u2014 further qualflare.tag() calls this test will be dropped.`
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
let tag = rawTag;
|
|
488
|
+
if (tag.length > MAX_TAG_LENGTH) {
|
|
489
|
+
this.warnCappedOnce("tag-length", `a tag exceeded ${MAX_TAG_LENGTH} characters and was truncated.`);
|
|
490
|
+
tag = tag.slice(0, MAX_TAG_LENGTH);
|
|
491
|
+
}
|
|
492
|
+
this.tags.push(tag);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/** Last-write-wins if called more than once in one test — simpler than
|
|
496
|
+
* concatenation, and matches how most comparable metadata APIs (a single
|
|
497
|
+
* "set the description" call, not an accumulating log) behave. */
|
|
498
|
+
description(text) {
|
|
499
|
+
if (!this.active) return this.warnInactive("description");
|
|
500
|
+
this.descriptionText = text;
|
|
501
|
+
}
|
|
502
|
+
/** Last-write-wins if called more than once in one test, same as
|
|
503
|
+
* `description()`. Server-side, an unrecognized value is normalized/
|
|
504
|
+
* dropped rather than rejecting the request (`shared/types.ts`), so —
|
|
505
|
+
* like `link()`'s `type` option — this does no runtime validation of
|
|
506
|
+
* its own and simply takes the caller's word for it. */
|
|
507
|
+
priority(value) {
|
|
508
|
+
if (!this.active) return this.warnInactive("priority");
|
|
509
|
+
this.priorityValue = value;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Placement decision (the wire contract has no top-level `Parameter[]` on
|
|
513
|
+
* `Case` — only `Step.parameters` exists, see `shared/types.ts`): a call
|
|
514
|
+
* made while a `qualflare.step()` is currently open attaches to that
|
|
515
|
+
* step's `parameters[]` (capped at `MAX_PARAMETERS_PER_STEP`, shared with
|
|
516
|
+
* whatever `consoleProps`-derived parameters that step might separately
|
|
517
|
+
* accumulate — no, actually a manual step never has consoleProps, only
|
|
518
|
+
* auto-captured command-log steps do, so no sharing/collision is possible
|
|
519
|
+
* here). A call made OUTSIDE any step becomes a `Case.properties` entry
|
|
520
|
+
* instead, since that's the only test-level key/value bag the wire
|
|
521
|
+
* contract offers. `opts.masked` has no analog on `properties` (a plain
|
|
522
|
+
* `Record<string,string>`) and is silently ignored in that branch — real,
|
|
523
|
+
* documented limitation, not a bug: masking only has meaning for a
|
|
524
|
+
* step-level `Parameter`.
|
|
525
|
+
*/
|
|
526
|
+
parameter(name, value, opts) {
|
|
527
|
+
if (!this.active) return this.warnInactive("parameter");
|
|
528
|
+
const openStepIndex = this.manualStepStack[this.manualStepStack.length - 1];
|
|
529
|
+
if (openStepIndex !== void 0) {
|
|
530
|
+
const step = this.manualSteps[openStepIndex];
|
|
531
|
+
if (!step) return;
|
|
532
|
+
step.parameters ??= [];
|
|
533
|
+
if (step.parameters.length >= MAX_PARAMETERS_PER_STEP) {
|
|
534
|
+
return this.warnCappedOnce(
|
|
535
|
+
"step-parameters",
|
|
536
|
+
`reached the ${MAX_PARAMETERS_PER_STEP}-parameter-per-step cap \u2014 further qualflare.parameter() calls within this step will be dropped.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const parameter = { name };
|
|
540
|
+
if (value !== void 0) parameter.value = value;
|
|
541
|
+
if (opts?.masked) parameter.masked = true;
|
|
542
|
+
step.parameters.push(parameter);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.properties[name] = value ?? "";
|
|
546
|
+
}
|
|
547
|
+
/** `encoding` defaults to `'utf8'`: `content` is treated as plain text and
|
|
548
|
+
* base64-encoded before being placed into the wire format's
|
|
549
|
+
* always-base64 `Attachment.content` (see `shared/types.ts`). `'base64'`
|
|
550
|
+
* means the caller already has base64 text and it's passed through
|
|
551
|
+
* unencoded. Uses `TextEncoder`/`btoa` rather than Node's `Buffer` — this
|
|
552
|
+
* module runs in the browser (Cypress's actual browser context, not
|
|
553
|
+
* Node), where `Buffer` does not exist; both are standard Web APIs
|
|
554
|
+
* available in every browser Cypress supports. */
|
|
555
|
+
attachment(name, content, opts) {
|
|
556
|
+
if (!this.active) return this.warnInactive("attachment");
|
|
557
|
+
if (this.attachments.length >= MAX_ATTACHMENTS_PER_CASE) {
|
|
558
|
+
return this.warnCappedOnce(
|
|
559
|
+
"attachments",
|
|
560
|
+
`reached the ${MAX_ATTACHMENTS_PER_CASE}-attachment-per-case cap \u2014 further qualflare.attachment()/attachmentFromFile() calls this test will be dropped. Note this cap is enforced independently of any screenshots captured during the same test, which are merged in Node-side \u2014 the combined total is not currently capped.`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const base64 = opts?.encoding === "base64" ? content : utf8ToBase64(content);
|
|
564
|
+
const attachment = { name, content: base64 };
|
|
565
|
+
if (opts?.mimeType) attachment.mimeType = opts.mimeType;
|
|
566
|
+
this.attachments.push(attachment);
|
|
567
|
+
}
|
|
568
|
+
/** Mirrors the screenshot flow (`plugin/attachment-reader.ts`): the file's
|
|
569
|
+
* bytes are never read here (this runs browser-side, with no filesystem
|
|
570
|
+
* access) — a path-only `Attachment{name, path, mimeType}` is queued, and
|
|
571
|
+
* the EXISTING Node-side `resolveAttachments()` pipeline (already
|
|
572
|
+
* generic — it reads and size-guards any attachment that has a `path` but
|
|
573
|
+
* no `content`) resolves it once this test's `Case` reaches
|
|
574
|
+
* `TASK_REPORT_CASE`. No new Node-side code needed for this to work. */
|
|
575
|
+
attachmentFromFile(name, path, opts) {
|
|
576
|
+
if (!this.active) return this.warnInactive("attachmentFromFile");
|
|
577
|
+
if (this.attachments.length >= MAX_ATTACHMENTS_PER_CASE) {
|
|
578
|
+
return this.warnCappedOnce(
|
|
579
|
+
"attachments",
|
|
580
|
+
`reached the ${MAX_ATTACHMENTS_PER_CASE}-attachment-per-case cap \u2014 further qualflare.attachment()/attachmentFromFile() calls this test will be dropped.`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const attachment = { name, path };
|
|
584
|
+
if (opts?.mimeType) attachment.mimeType = opts.mimeType;
|
|
585
|
+
this.attachments.push(attachment);
|
|
586
|
+
}
|
|
587
|
+
/** Starts a manually-declared step, nested under whatever manual step (if
|
|
588
|
+
* any) is currently open — an independent nesting stack from
|
|
589
|
+
* `CommandLogBuffer`'s command-log-derived parent/child tracking (see this
|
|
590
|
+
* file's header comment for why the two aren't unified). Returns an index
|
|
591
|
+
* to pass back to `endStep()`. Soft-capped at `MAX_STEPS_PER_TEST_ATTEMPT`,
|
|
592
|
+
* same limit `CommandLogBuffer` uses (the two counts aren't combined
|
|
593
|
+
* against a single shared budget — a deliberate, documented simplification;
|
|
594
|
+
* either buffer alone can reach its own cap independently). Returns
|
|
595
|
+
* `undefined` if inactive or capped — `endStep(undefined, ...)` is a
|
|
596
|
+
* documented no-op, so callers don't need to branch on this themselves. */
|
|
597
|
+
beginStep(name, now = Date.now()) {
|
|
598
|
+
if (!this.active) {
|
|
599
|
+
this.warnInactive("step");
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
if (this.manualSteps.length >= MAX_STEPS_PER_TEST_ATTEMPT) {
|
|
603
|
+
this.warnCappedOnce(
|
|
604
|
+
"manual-steps",
|
|
605
|
+
`reached the ${MAX_STEPS_PER_TEST_ATTEMPT}-step-per-test soft cap \u2014 further qualflare.step() calls this test attempt will still run their wrapped commands, but will not be recorded as steps.`
|
|
606
|
+
);
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
const index = this.manualSteps.length;
|
|
610
|
+
const parentIndex = this.manualStepStack[this.manualStepStack.length - 1];
|
|
611
|
+
const record = { name, status: "pending", startedAt: now };
|
|
612
|
+
if (parentIndex !== void 0) record.parentIndex = parentIndex;
|
|
613
|
+
this.manualSteps.push(record);
|
|
614
|
+
this.manualStepStack.push(index);
|
|
615
|
+
return index;
|
|
616
|
+
}
|
|
617
|
+
/** Finalizes a step started by `beginStep()`, recording its real
|
|
618
|
+
* wall-clock duration. A no-op if `index` is `undefined` (the documented
|
|
619
|
+
* signal from `beginStep()` that nothing was actually recorded —
|
|
620
|
+
* inactive buffer or step-count cap reached). If a step's wrapped
|
|
621
|
+
* commands fail/throw, this never runs — see `metadata-api.ts`'s
|
|
622
|
+
* `step()` doc comment — so `durationMs` stays `undefined` and
|
|
623
|
+
* `combineSteps` (`case-builder.ts`) falls back to 0 for that step. */
|
|
624
|
+
endStep(index, status, error, now = Date.now()) {
|
|
625
|
+
if (index === void 0) return;
|
|
626
|
+
const record = this.manualSteps[index];
|
|
627
|
+
if (record) {
|
|
628
|
+
record.status = status;
|
|
629
|
+
if (error) record.error = error;
|
|
630
|
+
record.durationMs = Math.max(0, now - record.startedAt);
|
|
631
|
+
}
|
|
632
|
+
const stackPos = this.manualStepStack.lastIndexOf(index);
|
|
633
|
+
if (stackPos !== -1) {
|
|
634
|
+
this.manualStepStack.splice(stackPos, 1);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
function utf8ToBase64(text) {
|
|
639
|
+
const bytes = new TextEncoder().encode(text);
|
|
640
|
+
let binary = "";
|
|
641
|
+
for (const byte of bytes) {
|
|
642
|
+
binary += String.fromCharCode(byte);
|
|
643
|
+
}
|
|
644
|
+
return btoa(binary);
|
|
645
|
+
}
|
|
646
|
+
var fallbackBuffer;
|
|
647
|
+
function getDefaultMetadataBuffer() {
|
|
648
|
+
if (typeof Cypress === "undefined") {
|
|
649
|
+
fallbackBuffer ??= new TestMetadataBuffer();
|
|
650
|
+
return fallbackBuffer;
|
|
651
|
+
}
|
|
652
|
+
const target = Cypress;
|
|
653
|
+
target.__qualflareMetadataBuffer ??= new TestMetadataBuffer();
|
|
654
|
+
return target.__qualflareMetadataBuffer;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/browser/mocha-listener.ts
|
|
658
|
+
function retryIndex(test) {
|
|
659
|
+
return test._currentRetry ?? 0;
|
|
660
|
+
}
|
|
661
|
+
function formatError(err) {
|
|
662
|
+
if (err === void 0 || err === null) {
|
|
663
|
+
return void 0;
|
|
664
|
+
}
|
|
665
|
+
if (err instanceof Error) {
|
|
666
|
+
return err.stack ? `${err.message}
|
|
667
|
+
${err.stack}` : err.message;
|
|
668
|
+
}
|
|
669
|
+
return String(err);
|
|
670
|
+
}
|
|
671
|
+
var MochaAttemptTracker = class {
|
|
672
|
+
entries = /* @__PURE__ */ new Map();
|
|
673
|
+
entryFor(key, test) {
|
|
674
|
+
let entry = this.entries.get(key);
|
|
675
|
+
if (!entry) {
|
|
676
|
+
entry = { test, attempts: [], willRetry: false };
|
|
677
|
+
this.entries.set(key, entry);
|
|
678
|
+
}
|
|
679
|
+
return entry;
|
|
680
|
+
}
|
|
681
|
+
/** Records one attempt, deduped against only the immediately-preceding
|
|
682
|
+
* record for THIS test (never against any other test's history). More
|
|
683
|
+
* than one Mocha/Cypress event can legitimately fire for the same
|
|
684
|
+
* physical attempt (verified empirically: `runner.on('fail', ...)` AND
|
|
685
|
+
* `runner.on('retry', ...)` both fire for one failing-and-retried
|
|
686
|
+
* attempt) — a second call with the same `retryIndex` for the same key
|
|
687
|
+
* is a no-op rather than double-recording. */
|
|
688
|
+
record(key, retryIdx, test, snapshot) {
|
|
689
|
+
const entry = this.entryFor(key, test);
|
|
690
|
+
if (entry.lastRecordedRetryIndex === retryIdx) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
entry.lastRecordedRetryIndex = retryIdx;
|
|
694
|
+
entry.attempts.push(snapshot);
|
|
695
|
+
}
|
|
696
|
+
/** Marks this test's most-recently-recorded attempt as non-final — more
|
|
697
|
+
* attempts are coming, so `takeIfFinal`/`drainOrphaned` must not flush
|
|
698
|
+
* yet. Only meaningful immediately after a `record()` call for the same
|
|
699
|
+
* attempt (Cypress's retry mechanism always records the failing attempt
|
|
700
|
+
* before emitting `'retry'`). A no-op if no entry exists yet for `key`. */
|
|
701
|
+
markWillRetry(key) {
|
|
702
|
+
const entry = this.entries.get(key);
|
|
703
|
+
if (entry) {
|
|
704
|
+
entry.willRetry = true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** The `afterEach`-driven path for the CURRENTLY-ending test: returns its
|
|
708
|
+
* attempts and forgets them — UNLESS `markWillRetry` was called for the
|
|
709
|
+
* attempt just recorded, in which case this consumes that flag and
|
|
710
|
+
* returns `undefined`, leaving the entry in place so the next attempt
|
|
711
|
+
* appends to the same array instead of starting fresh. Returns
|
|
712
|
+
* `undefined` (nothing to do) if no entry exists for `key` at all. */
|
|
713
|
+
takeIfFinal(key) {
|
|
714
|
+
const entry = this.entries.get(key);
|
|
715
|
+
if (!entry) {
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
if (entry.willRetry) {
|
|
719
|
+
entry.willRetry = false;
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
this.entries.delete(key);
|
|
723
|
+
return entry.attempts;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Sweeps every OTHER finalized-but-never-collected entry (excluding
|
|
727
|
+
* `excludeKey`, the test this `afterEach` firing is already handling via
|
|
728
|
+
* `takeIfFinal`), skipping anything still mid-retry. This exists for
|
|
729
|
+
* exactly one real scenario: a statically-skipped test (`it.skip(...)` or
|
|
730
|
+
* an inherited `.skip`) fires `'pending'` and gets `record()`ed, but
|
|
731
|
+
* Mocha's skip path never runs `afterEach` for it at all — so nothing
|
|
732
|
+
* else will ever collect it.
|
|
733
|
+
*
|
|
734
|
+
* The obvious-looking alternative — flush a skipped test immediately,
|
|
735
|
+
* right in the `'pending'` handler — was tried and is UNSAFE: verified
|
|
736
|
+
* empirically (a real `cypress run` against a fixture spec containing
|
|
737
|
+
* `it.skip(...)`) that calling `cy.task()` synchronously from that
|
|
738
|
+
* handler doesn't just silently no-op, it HANGS the entire run
|
|
739
|
+
* indefinitely (Cypress's command-queue machinery for a test whose body
|
|
740
|
+
* never executes at all appears to never reach a state where a
|
|
741
|
+
* newly-enqueued command can be processed). `drainOrphaned` instead waits
|
|
742
|
+
* until the NEXT real `afterEach` fires (a call site independently
|
|
743
|
+
* proven safe for `cy.task()`, both here and by every other flush in this
|
|
744
|
+
* file) and sweeps anything left over at that point.
|
|
745
|
+
*
|
|
746
|
+
* Residual, honestly-documented limitation: if EVERY test in a spec file
|
|
747
|
+
* is statically skipped, no real `afterEach` ever fires at all in that
|
|
748
|
+
* spec, so a lingering skip entry is never swept — the same outcome as
|
|
749
|
+
* before this fix, for that one narrower sub-case. The common case (at
|
|
750
|
+
* least one non-skipped test in the spec) is fully fixed.
|
|
751
|
+
*/
|
|
752
|
+
drainOrphaned(excludeKey) {
|
|
753
|
+
const drained = [];
|
|
754
|
+
for (const [key, entry] of this.entries) {
|
|
755
|
+
if (key === excludeKey || entry.willRetry) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
drained.push({ test: entry.test, attempts: entry.attempts });
|
|
759
|
+
this.entries.delete(key);
|
|
760
|
+
}
|
|
761
|
+
return drained;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
function registerMochaListener() {
|
|
765
|
+
const runner = Cypress.mocha.getRunner();
|
|
766
|
+
const tracker = new MochaAttemptTracker();
|
|
767
|
+
const stepBuffer = new CommandLogBuffer();
|
|
768
|
+
registerCommandLogListener(stepBuffer);
|
|
769
|
+
function buildSnapshot(test, status, err) {
|
|
770
|
+
const steps = stepBuffer.drain();
|
|
771
|
+
const metadata = getDefaultMetadataBuffer().drain();
|
|
772
|
+
return {
|
|
773
|
+
status,
|
|
774
|
+
duration: test.duration ?? 0,
|
|
775
|
+
error: formatError(err),
|
|
776
|
+
steps: steps.length > 0 ? steps : void 0,
|
|
777
|
+
...metadata
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
runner.on("test", () => {
|
|
781
|
+
stepBuffer.reset();
|
|
782
|
+
getDefaultMetadataBuffer().reset();
|
|
783
|
+
});
|
|
784
|
+
runner.on("pass", (test) => {
|
|
785
|
+
tracker.record(test.fullTitle(), retryIndex(test), test, buildSnapshot(test, "passed"));
|
|
786
|
+
});
|
|
787
|
+
runner.on("fail", (test, err) => {
|
|
788
|
+
const runnable = test;
|
|
789
|
+
if (runnable.type === "test") {
|
|
790
|
+
tracker.record(test.fullTitle(), retryIndex(test), test, buildSnapshot(test, "failed", err));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const guardedTest = runnable.ctx?.currentTest;
|
|
794
|
+
const isBeforeEachHook = (runnable.originalTitle ?? runnable.title ?? "").startsWith('"before each" hook');
|
|
795
|
+
if (guardedTest && isBeforeEachHook) {
|
|
796
|
+
tracker.record(guardedTest.fullTitle(), retryIndex(guardedTest), guardedTest, buildSnapshot(guardedTest, "failed", err));
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
runner.on("retry", (test, err) => {
|
|
800
|
+
const status = err ? "failed" : "passed";
|
|
801
|
+
tracker.record(test.fullTitle(), retryIndex(test), test, buildSnapshot(test, status, err));
|
|
802
|
+
tracker.markWillRetry(test.fullTitle());
|
|
803
|
+
});
|
|
804
|
+
runner.on("pending", (test) => {
|
|
805
|
+
tracker.record(test.fullTitle(), retryIndex(test), test, buildSnapshot(test, "skipped"));
|
|
806
|
+
});
|
|
807
|
+
Cypress.on("fail", (err, runnable) => {
|
|
808
|
+
if (runnable.type !== "test") {
|
|
809
|
+
throw err;
|
|
810
|
+
}
|
|
811
|
+
const test = runnable;
|
|
812
|
+
tracker.record(test.fullTitle(), retryIndex(test), test, buildSnapshot(test, "failed", err));
|
|
813
|
+
throw err;
|
|
814
|
+
});
|
|
815
|
+
afterEach(function flushCurrentTest() {
|
|
816
|
+
const test = this.currentTest;
|
|
817
|
+
if (!test) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const key = test.fullTitle();
|
|
821
|
+
const attempts = tracker.takeIfFinal(key);
|
|
822
|
+
if (attempts) {
|
|
823
|
+
flushCase(test, attempts);
|
|
824
|
+
}
|
|
825
|
+
for (const orphan of tracker.drainOrphaned(key)) {
|
|
826
|
+
flushCase(orphan.test, orphan.attempts);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/browser/test-phase-signal.ts
|
|
832
|
+
function registerTestPhaseSignal() {
|
|
833
|
+
let signaled = false;
|
|
834
|
+
beforeEach(function qualflareMarkTestPhaseStarted() {
|
|
835
|
+
if (signaled) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
signaled = true;
|
|
839
|
+
cy.task(TASK_MARK_TEST_PHASE_STARTED, null, { log: false });
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/browser/index.ts
|
|
844
|
+
initializeBrowserIntegration(() => {
|
|
845
|
+
registerMochaListener();
|
|
846
|
+
registerTestPhaseSignal();
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// src/browser/metadata-api.ts
|
|
850
|
+
var qualflare = {
|
|
851
|
+
label(name, value) {
|
|
852
|
+
getDefaultMetadataBuffer().label(name, value);
|
|
853
|
+
},
|
|
854
|
+
link(url, opts) {
|
|
855
|
+
getDefaultMetadataBuffer().link(url, opts);
|
|
856
|
+
},
|
|
857
|
+
tag(...tags) {
|
|
858
|
+
getDefaultMetadataBuffer().tag(...tags);
|
|
859
|
+
},
|
|
860
|
+
description(text) {
|
|
861
|
+
getDefaultMetadataBuffer().description(text);
|
|
862
|
+
},
|
|
863
|
+
priority(value) {
|
|
864
|
+
getDefaultMetadataBuffer().priority(value);
|
|
865
|
+
},
|
|
866
|
+
parameter(name, value, opts) {
|
|
867
|
+
getDefaultMetadataBuffer().parameter(name, value, opts);
|
|
868
|
+
},
|
|
869
|
+
attachment(name, content, opts) {
|
|
870
|
+
getDefaultMetadataBuffer().attachment(name, content, opts);
|
|
871
|
+
},
|
|
872
|
+
attachmentFromFile(name, path, opts) {
|
|
873
|
+
getDefaultMetadataBuffer().attachmentFromFile(name, path, opts);
|
|
874
|
+
},
|
|
875
|
+
/**
|
|
876
|
+
* Wraps `fn` as a named, reportable step. Unlike every other function on
|
|
877
|
+
* this object, a step's start/end only has meaning relative to when its
|
|
878
|
+
* wrapped `cy.*()` commands actually EXECUTE — not when `step()` is
|
|
879
|
+
* textually called, which happens synchronously, before any queued
|
|
880
|
+
* command has run. So this interleaves `beginStep`/`endStep` INTO the
|
|
881
|
+
* command queue itself via `cy.then()`, at the exact point the wrapped
|
|
882
|
+
* commands run, rather than calling them eagerly.
|
|
883
|
+
*
|
|
884
|
+
* Verified directly against `node_modules/cypress/types/cypress.d.ts`
|
|
885
|
+
* (Cypress 14.5.4) before relying on any of this, rather than assumed:
|
|
886
|
+
* - `Cypress.isCy(obj: any): obj is Chainable` exists exactly as the
|
|
887
|
+
* plan's sketch expected.
|
|
888
|
+
* - `cy.wrap<S>(object: S, options?: Partial<Loggable & Timeoutable>)`
|
|
889
|
+
* accepts `{ log: false }` — this is properly typed.
|
|
890
|
+
* - `cy.then<S>(options: Partial<Timeoutable>, fn): ...` does NOT accept
|
|
891
|
+
* `Loggable` in its options type (only `wrap()` does) — passing
|
|
892
|
+
* `{ log: false }` to `.then()` is a genuine type error under this
|
|
893
|
+
* version's declarations. Cypress's own runtime DOES honor `log: false`
|
|
894
|
+
* on `.then()` in practice (a long-documented, widely-relied-upon
|
|
895
|
+
* behavior across the Cypress plugin ecosystem — every reporter that
|
|
896
|
+
* injects bookkeeping commands into the queue uses this), so `.then()`
|
|
897
|
+
* calls below pass it via a narrow, explicitly-commented type
|
|
898
|
+
* assertion rather than omitting it and cluttering every test's
|
|
899
|
+
* Command Log with reporter-internal entries.
|
|
900
|
+
*/
|
|
901
|
+
step(name, fn) {
|
|
902
|
+
const stepIndex = getDefaultMetadataBuffer().beginStep(name);
|
|
903
|
+
let result;
|
|
904
|
+
try {
|
|
905
|
+
result = fn();
|
|
906
|
+
} catch (err) {
|
|
907
|
+
getDefaultMetadataBuffer().endStep(stepIndex, "failed", formatSyncStepError(err));
|
|
908
|
+
throw err;
|
|
909
|
+
}
|
|
910
|
+
const chained = Cypress.isCy(result) ? result : cy.wrap(result, { log: false });
|
|
911
|
+
return chained.then(withoutLog(), (value) => {
|
|
912
|
+
getDefaultMetadataBuffer().endStep(stepIndex, "passed");
|
|
913
|
+
return value;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
function formatSyncStepError(err) {
|
|
918
|
+
if (err instanceof Error) {
|
|
919
|
+
return err.stack ? `${err.message}
|
|
920
|
+
${err.stack}` : err.message;
|
|
921
|
+
}
|
|
922
|
+
return String(err);
|
|
923
|
+
}
|
|
924
|
+
function withoutLog() {
|
|
925
|
+
return { log: false };
|
|
926
|
+
}
|
|
927
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
928
|
+
0 && (module.exports = {
|
|
929
|
+
qualflare
|
|
930
|
+
});
|
|
931
|
+
//# sourceMappingURL=index.cjs.map
|