@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/dist/formatter/index.js
CHANGED
|
@@ -12,6 +12,8 @@ var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
|
12
12
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
13
13
|
var MAX_CASES_PER_SUITE = 5e3;
|
|
14
14
|
var MAX_TAGS_PER_CASE = 64;
|
|
15
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
16
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
15
17
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
16
18
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
17
19
|
|
|
@@ -492,6 +494,18 @@ function messageDurationToNs(duration) {
|
|
|
492
494
|
return total > 0 ? Math.round(total) : 0;
|
|
493
495
|
}
|
|
494
496
|
|
|
497
|
+
// src/shared/text.ts
|
|
498
|
+
function truncateRunes(value, maxRunes) {
|
|
499
|
+
if (value.length <= maxRunes) {
|
|
500
|
+
return value;
|
|
501
|
+
}
|
|
502
|
+
const runes = Array.from(value);
|
|
503
|
+
if (runes.length <= maxRunes) {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
return runes.slice(0, maxRunes).join("");
|
|
507
|
+
}
|
|
508
|
+
|
|
495
509
|
// src/formatter/case-builder.ts
|
|
496
510
|
function combineSteps(realSteps, manualSteps) {
|
|
497
511
|
if (realSteps.length === 0 && manualSteps.length === 0) {
|
|
@@ -510,6 +524,26 @@ function combineSteps(realSteps, manualSteps) {
|
|
|
510
524
|
});
|
|
511
525
|
return [...realSteps, ...offsetManual];
|
|
512
526
|
}
|
|
527
|
+
function buildAttempts(attempts) {
|
|
528
|
+
if (attempts.length < 2) {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
let kept = attempts;
|
|
532
|
+
if (attempts.length > MAX_ATTEMPTS_PER_CASE) {
|
|
533
|
+
kept = [...attempts.slice(0, MAX_ATTEMPTS_PER_CASE - 1), attempts[attempts.length - 1]];
|
|
534
|
+
}
|
|
535
|
+
return kept.map((a, i) => {
|
|
536
|
+
const attempt = {
|
|
537
|
+
attempt: i + 1,
|
|
538
|
+
status: a.status,
|
|
539
|
+
duration: a.duration
|
|
540
|
+
};
|
|
541
|
+
if (a.error) {
|
|
542
|
+
attempt.message = truncateRunes(a.error, MAX_ATTEMPT_MESSAGE_RUNES);
|
|
543
|
+
}
|
|
544
|
+
return attempt;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
513
547
|
function collapseAttempts(attempts) {
|
|
514
548
|
if (attempts.length === 0) {
|
|
515
549
|
throw new Error("collapseAttempts: at least one attempt is required");
|
|
@@ -518,11 +552,13 @@ function collapseAttempts(attempts) {
|
|
|
518
552
|
const retryCount = attempts.length - 1;
|
|
519
553
|
const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
|
|
520
554
|
const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
|
|
555
|
+
const attemptHistory = buildAttempts(attempts);
|
|
521
556
|
return {
|
|
522
557
|
status: final.status,
|
|
523
558
|
duration,
|
|
524
559
|
retryCount,
|
|
525
560
|
isFlaky,
|
|
561
|
+
...attemptHistory ? { attempts: attemptHistory } : {},
|
|
526
562
|
error: final.status === "passed" ? void 0 : final.error,
|
|
527
563
|
steps: combineSteps(final.steps, final.manualSteps),
|
|
528
564
|
labels: final.labels,
|
|
@@ -562,6 +598,7 @@ function buildCase(uri, pickle, collapsed, gherkin) {
|
|
|
562
598
|
duration: collapsed.duration,
|
|
563
599
|
retryCount: collapsed.retryCount || void 0,
|
|
564
600
|
isFlaky: collapsed.isFlaky || void 0,
|
|
601
|
+
attempts: collapsed.attempts,
|
|
565
602
|
error: collapsed.error,
|
|
566
603
|
tags: tags.length > 0 ? tags : void 0,
|
|
567
604
|
steps: collapsed.steps,
|
|
@@ -967,7 +1004,7 @@ function timestampMs(ts) {
|
|
|
967
1004
|
import * as os from "os";
|
|
968
1005
|
|
|
969
1006
|
// src/config/version.ts
|
|
970
|
-
var PACKAGE_VERSION = "0.
|
|
1007
|
+
var PACKAGE_VERSION = "0.5.0";
|
|
971
1008
|
|
|
972
1009
|
// src/formatter/collect-builder.ts
|
|
973
1010
|
function resolveOs(config) {
|
|
@@ -1094,23 +1131,52 @@ function buildHookIndex(supportCodeLibrary) {
|
|
|
1094
1131
|
|
|
1095
1132
|
// src/formatter/run-hook-tracker.ts
|
|
1096
1133
|
var RunHookTracker = class {
|
|
1134
|
+
constructor(config, budget) {
|
|
1135
|
+
this.config = config;
|
|
1136
|
+
this.budget = budget;
|
|
1137
|
+
}
|
|
1138
|
+
config;
|
|
1139
|
+
budget;
|
|
1097
1140
|
/** `testRunHookStartedId` -> the hook it started, so `finish()` can look
|
|
1098
1141
|
* up its kind/name. `TestRunHookFinished.result.duration` already gives
|
|
1099
1142
|
* an accurate duration directly — no start/finish timestamp delta needed. */
|
|
1100
1143
|
started = /* @__PURE__ */ new Map();
|
|
1101
1144
|
failed = [];
|
|
1145
|
+
/** `testRunHookStartedId` -> attachments made during that hook, unresolved. */
|
|
1146
|
+
pending = /* @__PURE__ */ new Map();
|
|
1102
1147
|
start(e) {
|
|
1103
1148
|
this.started.set(e.id, e.hookId);
|
|
1104
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Buffers one attachment made from inside a `BeforeAll`/`AfterAll`. Returns
|
|
1152
|
+
* whether it was claimed, so the caller can tell a run-hook attachment from
|
|
1153
|
+
* one that simply has no home.
|
|
1154
|
+
*
|
|
1155
|
+
* `testRunHookStartedId` is optional in the message schema and only populated
|
|
1156
|
+
* by newer cucumber-js; when it is absent this returns false and the
|
|
1157
|
+
* attachment is dropped exactly as it was before.
|
|
1158
|
+
*/
|
|
1159
|
+
attachment(e) {
|
|
1160
|
+
if (!e.testRunHookStartedId) {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
const content = e.contentEncoding === "BASE64" ? e.body : Buffer.from(e.body, "utf8").toString("base64");
|
|
1164
|
+
const list = this.pending.get(e.testRunHookStartedId) ?? [];
|
|
1165
|
+
list.push({ name: e.fileName || "attachment", mimeType: e.mediaType, content });
|
|
1166
|
+
this.pending.set(e.testRunHookStartedId, list);
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1105
1169
|
finish(e, hookIndex) {
|
|
1106
1170
|
const hookId = this.started.get(e.testRunHookStartedId);
|
|
1107
1171
|
this.started.delete(e.testRunHookStartedId);
|
|
1108
1172
|
const status = mapStatus(e.result.status);
|
|
1109
1173
|
if (status === "passed" || status === "skipped") {
|
|
1174
|
+
this.pending.delete(e.testRunHookStartedId);
|
|
1110
1175
|
return;
|
|
1111
1176
|
}
|
|
1112
1177
|
const hook = hookId ? hookIndex.get(hookId) : void 0;
|
|
1113
1178
|
const label = hook?.kind === "afterAll" ? "AfterAll hook" : "BeforeAll hook";
|
|
1179
|
+
const attachments = this.takeAttachments(e.testRunHookStartedId);
|
|
1114
1180
|
this.failed.push({
|
|
1115
1181
|
id: `global-hook:${e.testRunHookStartedId}`,
|
|
1116
1182
|
name: hook?.name || label,
|
|
@@ -1119,9 +1185,17 @@ var RunHookTracker = class {
|
|
|
1119
1185
|
// `result.message` first — see `step-mapper.ts`'s `formatError()` doc
|
|
1120
1186
|
// comment for why (verified empirically to be the version-safe field
|
|
1121
1187
|
// across the peer-dependency range; `exception.stackTrace` is not).
|
|
1122
|
-
error: e.result.message || e.result.exception?.stackTrace || e.result.exception?.message
|
|
1188
|
+
error: e.result.message || e.result.exception?.stackTrace || e.result.exception?.message,
|
|
1189
|
+
...attachments.length > 0 ? { attachments } : {}
|
|
1123
1190
|
});
|
|
1124
1191
|
}
|
|
1192
|
+
/** Resolves and clears the buffer for one hook. Called only for a FAILED
|
|
1193
|
+
* hook, so the budget is never spent on a passing one's attachments. */
|
|
1194
|
+
takeAttachments(testRunHookStartedId) {
|
|
1195
|
+
const buffered = this.pending.get(testRunHookStartedId) ?? [];
|
|
1196
|
+
this.pending.delete(testRunHookStartedId);
|
|
1197
|
+
return buffered.map((pending) => resolvePendingAttachment(pending, this.config, this.budget)).filter((a) => a !== void 0);
|
|
1198
|
+
}
|
|
1125
1199
|
/** Returns `undefined` if no run-hook failed — see the class doc comment
|
|
1126
1200
|
* for why a passing BeforeAll/AfterAll produces no Case at all. */
|
|
1127
1201
|
buildSuite() {
|
|
@@ -1193,7 +1267,7 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1193
1267
|
testCaseIndex = /* @__PURE__ */ new Map();
|
|
1194
1268
|
attachmentBudget;
|
|
1195
1269
|
attemptTracker;
|
|
1196
|
-
runHookTracker
|
|
1270
|
+
runHookTracker;
|
|
1197
1271
|
finishedCases = [];
|
|
1198
1272
|
/** One promise per `testCaseFinished` envelope, resolving once that
|
|
1199
1273
|
* scenario's `AttemptTracker.finish()` (which itself awaits any pending
|
|
@@ -1208,6 +1282,7 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1208
1282
|
this.config = resolveConfig(options.parsedArgvOptions);
|
|
1209
1283
|
this.hookIndex = buildHookIndex(options.supportCodeLibrary);
|
|
1210
1284
|
this.attachmentBudget = new AttachmentBudget(this.config.maxTotalAttachmentBytes);
|
|
1285
|
+
this.runHookTracker = new RunHookTracker(this.config, this.attachmentBudget);
|
|
1211
1286
|
this.attemptTracker = new AttemptTracker(
|
|
1212
1287
|
this.hookIndex,
|
|
1213
1288
|
this.gherkin,
|