@qualflare/cucumberjs 0.3.0 → 0.5.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/README.md +16 -1
- package/dist/formatter/index.cjs +78 -3
- package/dist/formatter/index.cjs.map +1 -1
- package/dist/formatter/index.js +78 -3
- package/dist/formatter/index.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
7
|
A native CucumberJS reporter for [Qualflare](https://qualflare.com) — captures test results directly
|
|
8
|
-
from your `cucumber-js` run: Feature/Scenario status,
|
|
8
|
+
from your `cucumber-js` run: Feature/Scenario status, per-attempt retry history, screenshots, videos,
|
|
9
9
|
Given/When/Then step traces, Scenario Outline rows, and author-facing metadata (labels, links, tags,
|
|
10
10
|
custom attachments).
|
|
11
11
|
|
|
@@ -125,6 +125,21 @@ wrong value cannot fail at run time — this package makes no network calls —
|
|
|
125
125
|
(workaround, not a first-class rendering).
|
|
126
126
|
- **`BeforeStep`/`AfterStep` hooks are off by default** (`includeStepHooks`) — noisy for suites with
|
|
127
127
|
global per-step instrumentation.
|
|
128
|
+
- **`BeforeAll`/`AfterAll` attachments need the hook to fail** — a failed global hook becomes a
|
|
129
|
+
synthetic Case and its attachments land there; a passing one produces no Case, so they are
|
|
130
|
+
dropped.
|
|
131
|
+
- **`parameter()` outside a step is not masked** — `masked` is a display hint for the UI; the
|
|
132
|
+
server never redacts the value, so never put a real secret in one. See
|
|
133
|
+
[`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#qualflareparameter-outside-a-step-has-no-masking).
|
|
134
|
+
- **Attachment caps are two budgets, not one pool** — `maxAttachmentBytes` bounds a single
|
|
135
|
+
attachment and `maxTotalAttachmentBytes` the whole run; anything over either is dropped
|
|
136
|
+
outright rather than truncated. Raising them is the easiest way to push a request past
|
|
137
|
+
`/collect`'s body limit. See
|
|
138
|
+
[`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#per-caseper-attachment-caps-are-independent-not-pooled).
|
|
139
|
+
- **Retries carry per-attempt errors, but everything else is the final attempt** — `Case.attempts`
|
|
140
|
+
records each attempt's status, duration and error; steps, labels, links, tags, priority,
|
|
141
|
+
properties and attachments come from the last attempt only, so an abandoned attempt's step trace
|
|
142
|
+
is discarded rather than replayed alongside the final one.
|
|
128
143
|
|
|
129
144
|
Full details in [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md).
|
|
130
145
|
|
package/dist/formatter/index.cjs
CHANGED
|
@@ -48,6 +48,8 @@ var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
|
48
48
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
49
49
|
var MAX_CASES_PER_SUITE = 5e3;
|
|
50
50
|
var MAX_TAGS_PER_CASE = 64;
|
|
51
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
52
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
51
53
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
52
54
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
53
55
|
|
|
@@ -526,6 +528,18 @@ function messageDurationToNs(duration) {
|
|
|
526
528
|
return total > 0 ? Math.round(total) : 0;
|
|
527
529
|
}
|
|
528
530
|
|
|
531
|
+
// src/shared/text.ts
|
|
532
|
+
function truncateRunes(value, maxRunes) {
|
|
533
|
+
if (value.length <= maxRunes) {
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
const runes = Array.from(value);
|
|
537
|
+
if (runes.length <= maxRunes) {
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
return runes.slice(0, maxRunes).join("");
|
|
541
|
+
}
|
|
542
|
+
|
|
529
543
|
// src/formatter/case-builder.ts
|
|
530
544
|
function combineSteps(realSteps, manualSteps) {
|
|
531
545
|
if (realSteps.length === 0 && manualSteps.length === 0) {
|
|
@@ -544,6 +558,26 @@ function combineSteps(realSteps, manualSteps) {
|
|
|
544
558
|
});
|
|
545
559
|
return [...realSteps, ...offsetManual];
|
|
546
560
|
}
|
|
561
|
+
function buildAttempts(attempts) {
|
|
562
|
+
if (attempts.length < 2) {
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
let kept = attempts;
|
|
566
|
+
if (attempts.length > MAX_ATTEMPTS_PER_CASE) {
|
|
567
|
+
kept = [...attempts.slice(0, MAX_ATTEMPTS_PER_CASE - 1), attempts[attempts.length - 1]];
|
|
568
|
+
}
|
|
569
|
+
return kept.map((a, i) => {
|
|
570
|
+
const attempt = {
|
|
571
|
+
attempt: i + 1,
|
|
572
|
+
status: a.status,
|
|
573
|
+
duration: a.duration
|
|
574
|
+
};
|
|
575
|
+
if (a.error) {
|
|
576
|
+
attempt.message = truncateRunes(a.error, MAX_ATTEMPT_MESSAGE_RUNES);
|
|
577
|
+
}
|
|
578
|
+
return attempt;
|
|
579
|
+
});
|
|
580
|
+
}
|
|
547
581
|
function collapseAttempts(attempts) {
|
|
548
582
|
if (attempts.length === 0) {
|
|
549
583
|
throw new Error("collapseAttempts: at least one attempt is required");
|
|
@@ -552,11 +586,13 @@ function collapseAttempts(attempts) {
|
|
|
552
586
|
const retryCount = attempts.length - 1;
|
|
553
587
|
const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
|
|
554
588
|
const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
|
|
589
|
+
const attemptHistory = buildAttempts(attempts);
|
|
555
590
|
return {
|
|
556
591
|
status: final.status,
|
|
557
592
|
duration,
|
|
558
593
|
retryCount,
|
|
559
594
|
isFlaky,
|
|
595
|
+
...attemptHistory ? { attempts: attemptHistory } : {},
|
|
560
596
|
error: final.status === "passed" ? void 0 : final.error,
|
|
561
597
|
steps: combineSteps(final.steps, final.manualSteps),
|
|
562
598
|
labels: final.labels,
|
|
@@ -596,6 +632,7 @@ function buildCase(uri, pickle, collapsed, gherkin) {
|
|
|
596
632
|
duration: collapsed.duration,
|
|
597
633
|
retryCount: collapsed.retryCount || void 0,
|
|
598
634
|
isFlaky: collapsed.isFlaky || void 0,
|
|
635
|
+
attempts: collapsed.attempts,
|
|
599
636
|
error: collapsed.error,
|
|
600
637
|
tags: tags.length > 0 ? tags : void 0,
|
|
601
638
|
steps: collapsed.steps,
|
|
@@ -1001,7 +1038,7 @@ function timestampMs(ts) {
|
|
|
1001
1038
|
var os = __toESM(require("os"), 1);
|
|
1002
1039
|
|
|
1003
1040
|
// src/config/version.ts
|
|
1004
|
-
var PACKAGE_VERSION = "0.
|
|
1041
|
+
var PACKAGE_VERSION = "0.5.0";
|
|
1005
1042
|
|
|
1006
1043
|
// src/formatter/collect-builder.ts
|
|
1007
1044
|
function resolveOs(config) {
|
|
@@ -1128,23 +1165,52 @@ function buildHookIndex(supportCodeLibrary) {
|
|
|
1128
1165
|
|
|
1129
1166
|
// src/formatter/run-hook-tracker.ts
|
|
1130
1167
|
var RunHookTracker = class {
|
|
1168
|
+
constructor(config, budget) {
|
|
1169
|
+
this.config = config;
|
|
1170
|
+
this.budget = budget;
|
|
1171
|
+
}
|
|
1172
|
+
config;
|
|
1173
|
+
budget;
|
|
1131
1174
|
/** `testRunHookStartedId` -> the hook it started, so `finish()` can look
|
|
1132
1175
|
* up its kind/name. `TestRunHookFinished.result.duration` already gives
|
|
1133
1176
|
* an accurate duration directly — no start/finish timestamp delta needed. */
|
|
1134
1177
|
started = /* @__PURE__ */ new Map();
|
|
1135
1178
|
failed = [];
|
|
1179
|
+
/** `testRunHookStartedId` -> attachments made during that hook, unresolved. */
|
|
1180
|
+
pending = /* @__PURE__ */ new Map();
|
|
1136
1181
|
start(e) {
|
|
1137
1182
|
this.started.set(e.id, e.hookId);
|
|
1138
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Buffers one attachment made from inside a `BeforeAll`/`AfterAll`. Returns
|
|
1186
|
+
* whether it was claimed, so the caller can tell a run-hook attachment from
|
|
1187
|
+
* one that simply has no home.
|
|
1188
|
+
*
|
|
1189
|
+
* `testRunHookStartedId` is optional in the message schema and only populated
|
|
1190
|
+
* by newer cucumber-js; when it is absent this returns false and the
|
|
1191
|
+
* attachment is dropped exactly as it was before.
|
|
1192
|
+
*/
|
|
1193
|
+
attachment(e) {
|
|
1194
|
+
if (!e.testRunHookStartedId) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
const content = e.contentEncoding === "BASE64" ? e.body : Buffer.from(e.body, "utf8").toString("base64");
|
|
1198
|
+
const list = this.pending.get(e.testRunHookStartedId) ?? [];
|
|
1199
|
+
list.push({ name: e.fileName || "attachment", mimeType: e.mediaType, content });
|
|
1200
|
+
this.pending.set(e.testRunHookStartedId, list);
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1139
1203
|
finish(e, hookIndex) {
|
|
1140
1204
|
const hookId = this.started.get(e.testRunHookStartedId);
|
|
1141
1205
|
this.started.delete(e.testRunHookStartedId);
|
|
1142
1206
|
const status = mapStatus(e.result.status);
|
|
1143
1207
|
if (status === "passed" || status === "skipped") {
|
|
1208
|
+
this.pending.delete(e.testRunHookStartedId);
|
|
1144
1209
|
return;
|
|
1145
1210
|
}
|
|
1146
1211
|
const hook = hookId ? hookIndex.get(hookId) : void 0;
|
|
1147
1212
|
const label = hook?.kind === "afterAll" ? "AfterAll hook" : "BeforeAll hook";
|
|
1213
|
+
const attachments = this.takeAttachments(e.testRunHookStartedId);
|
|
1148
1214
|
this.failed.push({
|
|
1149
1215
|
id: `global-hook:${e.testRunHookStartedId}`,
|
|
1150
1216
|
name: hook?.name || label,
|
|
@@ -1153,9 +1219,17 @@ var RunHookTracker = class {
|
|
|
1153
1219
|
// `result.message` first — see `step-mapper.ts`'s `formatError()` doc
|
|
1154
1220
|
// comment for why (verified empirically to be the version-safe field
|
|
1155
1221
|
// across the peer-dependency range; `exception.stackTrace` is not).
|
|
1156
|
-
error: e.result.message || e.result.exception?.stackTrace || e.result.exception?.message
|
|
1222
|
+
error: e.result.message || e.result.exception?.stackTrace || e.result.exception?.message,
|
|
1223
|
+
...attachments.length > 0 ? { attachments } : {}
|
|
1157
1224
|
});
|
|
1158
1225
|
}
|
|
1226
|
+
/** Resolves and clears the buffer for one hook. Called only for a FAILED
|
|
1227
|
+
* hook, so the budget is never spent on a passing one's attachments. */
|
|
1228
|
+
takeAttachments(testRunHookStartedId) {
|
|
1229
|
+
const buffered = this.pending.get(testRunHookStartedId) ?? [];
|
|
1230
|
+
this.pending.delete(testRunHookStartedId);
|
|
1231
|
+
return buffered.map((pending) => resolvePendingAttachment(pending, this.config, this.budget)).filter((a) => a !== void 0);
|
|
1232
|
+
}
|
|
1159
1233
|
/** Returns `undefined` if no run-hook failed — see the class doc comment
|
|
1160
1234
|
* for why a passing BeforeAll/AfterAll produces no Case at all. */
|
|
1161
1235
|
buildSuite() {
|
|
@@ -1227,7 +1301,7 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1227
1301
|
testCaseIndex = /* @__PURE__ */ new Map();
|
|
1228
1302
|
attachmentBudget;
|
|
1229
1303
|
attemptTracker;
|
|
1230
|
-
runHookTracker
|
|
1304
|
+
runHookTracker;
|
|
1231
1305
|
finishedCases = [];
|
|
1232
1306
|
/** One promise per `testCaseFinished` envelope, resolving once that
|
|
1233
1307
|
* scenario's `AttemptTracker.finish()` (which itself awaits any pending
|
|
@@ -1242,6 +1316,7 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1242
1316
|
this.config = resolveConfig(options.parsedArgvOptions);
|
|
1243
1317
|
this.hookIndex = buildHookIndex(options.supportCodeLibrary);
|
|
1244
1318
|
this.attachmentBudget = new AttachmentBudget(this.config.maxTotalAttachmentBytes);
|
|
1319
|
+
this.runHookTracker = new RunHookTracker(this.config, this.attachmentBudget);
|
|
1245
1320
|
this.attemptTracker = new AttemptTracker(
|
|
1246
1321
|
this.hookIndex,
|
|
1247
1322
|
this.gherkin,
|