@qualflare/cucumberjs 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./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, real retry counts, screenshots, videos,
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
 
@@ -53,6 +53,11 @@ npx cucumber-js
53
53
  qf <your-project-identifier> collect ./qualflare-results
54
54
  ```
55
55
 
56
+ > **Videos are opt-in from `@qualflare/cli` v0.1.20.** `collect` uploads the report itself
57
+ > always, but a video only when asked: `--upload-artifacts=video` (or `QF_UPLOAD_ARTIFACTS=video`).
58
+ > Earlier CLI versions uploaded every video automatically. Nothing is dropped silently — `collect`
59
+ > prints how many it skipped and the exact flag to include them.
60
+
56
61
  That's it — Feature/Scenario results, retries, and any screenshots you already attach arrive as one
57
62
  Launch. See [`examples/basic/`](./examples/basic) for a complete runnable project.
58
63
 
@@ -125,6 +130,21 @@ wrong value cannot fail at run time — this package makes no network calls —
125
130
  (workaround, not a first-class rendering).
126
131
  - **`BeforeStep`/`AfterStep` hooks are off by default** (`includeStepHooks`) — noisy for suites with
127
132
  global per-step instrumentation.
133
+ - **`BeforeAll`/`AfterAll` attachments need the hook to fail** — a failed global hook becomes a
134
+ synthetic Case and its attachments land there; a passing one produces no Case, so they are
135
+ dropped.
136
+ - **`parameter()` outside a step is not masked** — `masked` is a display hint for the UI; the
137
+ server never redacts the value, so never put a real secret in one. See
138
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#qualflareparameter-outside-a-step-has-no-masking).
139
+ - **Attachment caps are two budgets, not one pool** — `maxAttachmentBytes` bounds a single
140
+ attachment and `maxTotalAttachmentBytes` the whole run; anything over either is dropped
141
+ outright rather than truncated. Raising them is the easiest way to push a request past
142
+ `/collect`'s body limit. See
143
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#per-caseper-attachment-caps-are-independent-not-pooled).
144
+ - **Retries carry per-attempt errors, but everything else is the final attempt** — `Case.attempts`
145
+ records each attempt's status, duration and error; steps, labels, links, tags, priority,
146
+ properties and attachments come from the last attempt only, so an abandoned attempt's step trace
147
+ is discarded rather than replayed alongside the final one.
128
148
 
129
149
  Full details in [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md).
130
150
 
@@ -1038,7 +1038,7 @@ function timestampMs(ts) {
1038
1038
  var os = __toESM(require("os"), 1);
1039
1039
 
1040
1040
  // src/config/version.ts
1041
- var PACKAGE_VERSION = "0.4.0";
1041
+ var PACKAGE_VERSION = "0.5.1";
1042
1042
 
1043
1043
  // src/formatter/collect-builder.ts
1044
1044
  function resolveOs(config) {
@@ -1165,23 +1165,52 @@ function buildHookIndex(supportCodeLibrary) {
1165
1165
 
1166
1166
  // src/formatter/run-hook-tracker.ts
1167
1167
  var RunHookTracker = class {
1168
+ constructor(config, budget) {
1169
+ this.config = config;
1170
+ this.budget = budget;
1171
+ }
1172
+ config;
1173
+ budget;
1168
1174
  /** `testRunHookStartedId` -> the hook it started, so `finish()` can look
1169
1175
  * up its kind/name. `TestRunHookFinished.result.duration` already gives
1170
1176
  * an accurate duration directly — no start/finish timestamp delta needed. */
1171
1177
  started = /* @__PURE__ */ new Map();
1172
1178
  failed = [];
1179
+ /** `testRunHookStartedId` -> attachments made during that hook, unresolved. */
1180
+ pending = /* @__PURE__ */ new Map();
1173
1181
  start(e) {
1174
1182
  this.started.set(e.id, e.hookId);
1175
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
+ }
1176
1203
  finish(e, hookIndex) {
1177
1204
  const hookId = this.started.get(e.testRunHookStartedId);
1178
1205
  this.started.delete(e.testRunHookStartedId);
1179
1206
  const status = mapStatus(e.result.status);
1180
1207
  if (status === "passed" || status === "skipped") {
1208
+ this.pending.delete(e.testRunHookStartedId);
1181
1209
  return;
1182
1210
  }
1183
1211
  const hook = hookId ? hookIndex.get(hookId) : void 0;
1184
1212
  const label = hook?.kind === "afterAll" ? "AfterAll hook" : "BeforeAll hook";
1213
+ const attachments = this.takeAttachments(e.testRunHookStartedId);
1185
1214
  this.failed.push({
1186
1215
  id: `global-hook:${e.testRunHookStartedId}`,
1187
1216
  name: hook?.name || label,
@@ -1190,9 +1219,17 @@ var RunHookTracker = class {
1190
1219
  // `result.message` first — see `step-mapper.ts`'s `formatError()` doc
1191
1220
  // comment for why (verified empirically to be the version-safe field
1192
1221
  // across the peer-dependency range; `exception.stackTrace` is not).
1193
- 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 } : {}
1194
1224
  });
1195
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
+ }
1196
1233
  /** Returns `undefined` if no run-hook failed — see the class doc comment
1197
1234
  * for why a passing BeforeAll/AfterAll produces no Case at all. */
1198
1235
  buildSuite() {
@@ -1264,7 +1301,7 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
1264
1301
  testCaseIndex = /* @__PURE__ */ new Map();
1265
1302
  attachmentBudget;
1266
1303
  attemptTracker;
1267
- runHookTracker = new RunHookTracker();
1304
+ runHookTracker;
1268
1305
  finishedCases = [];
1269
1306
  /** One promise per `testCaseFinished` envelope, resolving once that
1270
1307
  * scenario's `AttemptTracker.finish()` (which itself awaits any pending
@@ -1279,6 +1316,7 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
1279
1316
  this.config = resolveConfig(options.parsedArgvOptions);
1280
1317
  this.hookIndex = buildHookIndex(options.supportCodeLibrary);
1281
1318
  this.attachmentBudget = new AttachmentBudget(this.config.maxTotalAttachmentBytes);
1319
+ this.runHookTracker = new RunHookTracker(this.config, this.attachmentBudget);
1282
1320
  this.attemptTracker = new AttemptTracker(
1283
1321
  this.hookIndex,
1284
1322
  this.gherkin,