@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.
@@ -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.3.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 = new 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,