@qualflare/cucumberjs 0.2.0 → 0.4.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 +9 -3
- package/dist/formatter/index.cjs +59 -7
- package/dist/formatter/index.cjs.map +1 -1
- package/dist/formatter/index.js +59 -7
- package/dist/formatter/index.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,11 +106,17 @@ Every option can be set either as a `formatOptions` entry or via a `QUALFLARE_*`
|
|
|
106
106
|
variable. Full table, precedence rules, and auto-detection behavior (git branch/commit, CI
|
|
107
107
|
provider/build/PR) in [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md).
|
|
108
108
|
|
|
109
|
+
One option is worth calling out because it fails late: `environment` is matched against the
|
|
110
|
+
environment's **uid (slug)**, not its display name, so **Staging** in the UI is `staging` here. A
|
|
111
|
+
wrong value cannot fail at run time — this package makes no network calls — so the run succeeds and
|
|
112
|
+
`collect` 404s afterwards. See
|
|
113
|
+
[the note in the configuration docs](./docs/CONFIGURATION.md#environment-is-matched-by-uid-not-display-name).
|
|
114
|
+
|
|
109
115
|
## Known limitations
|
|
110
116
|
|
|
111
|
-
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
- **A stale `outputDir` is refused, not merged** — each report carries a `runId`, and `qf collect`
|
|
118
|
+
errors rather than merging files from two different runs. Needs `@qualflare/cli` v0.1.19+; older
|
|
119
|
+
CLIs merge as before.
|
|
114
120
|
- **`shardIndex` is best-effort** — cucumber-js routes its own `--shard` flag somewhere a formatter
|
|
115
121
|
cannot read, so it is recovered from `QUALFLARE_SHARD_INDEX` or by scanning `process.argv`. It is
|
|
116
122
|
only an attribution label; merging never depends on it. See
|
package/dist/formatter/index.cjs
CHANGED
|
@@ -35,16 +35,21 @@ __export(formatter_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(formatter_exports);
|
|
36
36
|
|
|
37
37
|
// src/formatter/formatter.ts
|
|
38
|
-
var
|
|
38
|
+
var import_node_crypto3 = require("crypto");
|
|
39
39
|
var fs3 = __toESM(require("fs"), 1);
|
|
40
40
|
var path4 = __toESM(require("path"), 1);
|
|
41
41
|
var import_cucumber = require("@cucumber/cucumber");
|
|
42
42
|
|
|
43
|
+
// src/config/resolve-config.ts
|
|
44
|
+
var import_node_crypto = require("crypto");
|
|
45
|
+
|
|
43
46
|
// src/shared/constants.ts
|
|
44
47
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
45
48
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
46
49
|
var MAX_CASES_PER_SUITE = 5e3;
|
|
47
50
|
var MAX_TAGS_PER_CASE = 64;
|
|
51
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
52
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
48
53
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
49
54
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
50
55
|
|
|
@@ -65,6 +70,7 @@ var PROVIDERS = [
|
|
|
65
70
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
66
71
|
providerName: "GitHub Actions",
|
|
67
72
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
73
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
68
74
|
runUrl: (env) => env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0,
|
|
69
75
|
prNumber: (env) => {
|
|
70
76
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -75,6 +81,7 @@ var PROVIDERS = [
|
|
|
75
81
|
detect: (env) => env.GITLAB_CI === "true",
|
|
76
82
|
providerName: "GitLab CI",
|
|
77
83
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
84
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
78
85
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
79
86
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
80
87
|
},
|
|
@@ -82,6 +89,7 @@ var PROVIDERS = [
|
|
|
82
89
|
detect: (env) => env.CIRCLECI === "true",
|
|
83
90
|
providerName: "CircleCI",
|
|
84
91
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
92
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
85
93
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
86
94
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
87
95
|
},
|
|
@@ -89,6 +97,7 @@ var PROVIDERS = [
|
|
|
89
97
|
detect: (env) => env.BUILDKITE === "true",
|
|
90
98
|
providerName: "Buildkite",
|
|
91
99
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
100
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
92
101
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
93
102
|
prNumber: (env) => {
|
|
94
103
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -104,6 +113,7 @@ var PROVIDERS = [
|
|
|
104
113
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
105
114
|
providerName: "Jenkins",
|
|
106
115
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
116
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
107
117
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
108
118
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
109
119
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -113,6 +123,7 @@ var PROVIDERS = [
|
|
|
113
123
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
114
124
|
providerName: "Azure Pipelines",
|
|
115
125
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
126
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
116
127
|
runUrl: (env) => {
|
|
117
128
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
118
129
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -128,6 +139,7 @@ var PROVIDERS = [
|
|
|
128
139
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
129
140
|
providerName: "Bitbucket Pipelines",
|
|
130
141
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
142
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
131
143
|
runUrl: (env) => {
|
|
132
144
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
133
145
|
if (!origin) {
|
|
@@ -149,6 +161,8 @@ function detectCi(env = process.env) {
|
|
|
149
161
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
150
162
|
const prNumber = provider.prNumber?.(env);
|
|
151
163
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
164
|
+
const runId = provider.runId?.(env);
|
|
165
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
152
166
|
return result;
|
|
153
167
|
}
|
|
154
168
|
if (ciInfo.name) {
|
|
@@ -256,6 +270,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
256
270
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
257
271
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
258
272
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
273
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? (0, import_node_crypto.randomUUID)();
|
|
259
274
|
return {
|
|
260
275
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
261
276
|
// fields — an explicit `''` option must not silently win over the
|
|
@@ -275,6 +290,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
275
290
|
ciBuildNumber,
|
|
276
291
|
ciRunUrl,
|
|
277
292
|
ciPrNumber,
|
|
293
|
+
runId,
|
|
278
294
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
279
295
|
includeStepHooks: options.includeStepHooks ?? envBool("QUALFLARE_INCLUDE_STEP_HOOKS") ?? false,
|
|
280
296
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
@@ -308,8 +324,8 @@ var logger = {
|
|
|
308
324
|
var fs2 = __toESM(require("fs"), 1);
|
|
309
325
|
var path2 = __toESM(require("path"), 1);
|
|
310
326
|
|
|
311
|
-
// src/formatter/video-
|
|
312
|
-
var
|
|
327
|
+
// src/formatter/video-writer.ts
|
|
328
|
+
var import_node_crypto2 = require("crypto");
|
|
313
329
|
var fs = __toESM(require("fs"), 1);
|
|
314
330
|
var path = __toESM(require("path"), 1);
|
|
315
331
|
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
@@ -338,7 +354,7 @@ function writeVideoAttachment(pending, outputDir, maxVideoBytes) {
|
|
|
338
354
|
logger.warn(`skipping video attachment "${pending.name}": unsupported video format.`);
|
|
339
355
|
return void 0;
|
|
340
356
|
}
|
|
341
|
-
const localVideoPath = `${(0,
|
|
357
|
+
const localVideoPath = `${(0, import_node_crypto2.randomUUID)()}${resolved.extension}`;
|
|
342
358
|
const destination = path.join(outputDir, localVideoPath);
|
|
343
359
|
if (pending.path !== void 0) {
|
|
344
360
|
let fileSize;
|
|
@@ -512,6 +528,18 @@ function messageDurationToNs(duration) {
|
|
|
512
528
|
return total > 0 ? Math.round(total) : 0;
|
|
513
529
|
}
|
|
514
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
|
+
|
|
515
543
|
// src/formatter/case-builder.ts
|
|
516
544
|
function combineSteps(realSteps, manualSteps) {
|
|
517
545
|
if (realSteps.length === 0 && manualSteps.length === 0) {
|
|
@@ -530,6 +558,26 @@ function combineSteps(realSteps, manualSteps) {
|
|
|
530
558
|
});
|
|
531
559
|
return [...realSteps, ...offsetManual];
|
|
532
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
|
+
}
|
|
533
581
|
function collapseAttempts(attempts) {
|
|
534
582
|
if (attempts.length === 0) {
|
|
535
583
|
throw new Error("collapseAttempts: at least one attempt is required");
|
|
@@ -538,11 +586,13 @@ function collapseAttempts(attempts) {
|
|
|
538
586
|
const retryCount = attempts.length - 1;
|
|
539
587
|
const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
|
|
540
588
|
const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
|
|
589
|
+
const attemptHistory = buildAttempts(attempts);
|
|
541
590
|
return {
|
|
542
591
|
status: final.status,
|
|
543
592
|
duration,
|
|
544
593
|
retryCount,
|
|
545
594
|
isFlaky,
|
|
595
|
+
...attemptHistory ? { attempts: attemptHistory } : {},
|
|
546
596
|
error: final.status === "passed" ? void 0 : final.error,
|
|
547
597
|
steps: combineSteps(final.steps, final.manualSteps),
|
|
548
598
|
labels: final.labels,
|
|
@@ -582,6 +632,7 @@ function buildCase(uri, pickle, collapsed, gherkin) {
|
|
|
582
632
|
duration: collapsed.duration,
|
|
583
633
|
retryCount: collapsed.retryCount || void 0,
|
|
584
634
|
isFlaky: collapsed.isFlaky || void 0,
|
|
635
|
+
attempts: collapsed.attempts,
|
|
585
636
|
error: collapsed.error,
|
|
586
637
|
tags: tags.length > 0 ? tags : void 0,
|
|
587
638
|
steps: collapsed.steps,
|
|
@@ -987,7 +1038,7 @@ function timestampMs(ts) {
|
|
|
987
1038
|
var os = __toESM(require("os"), 1);
|
|
988
1039
|
|
|
989
1040
|
// src/config/version.ts
|
|
990
|
-
var PACKAGE_VERSION = "0.
|
|
1041
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
991
1042
|
|
|
992
1043
|
// src/formatter/collect-builder.ts
|
|
993
1044
|
function resolveOs(config) {
|
|
@@ -1010,7 +1061,8 @@ function buildCollectPayload(suites, config) {
|
|
|
1010
1061
|
metadata: {
|
|
1011
1062
|
version: PACKAGE_VERSION,
|
|
1012
1063
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1013
|
-
cliName: "qualflare-cucumberjs"
|
|
1064
|
+
cliName: "qualflare-cucumberjs",
|
|
1065
|
+
runId: config.runId
|
|
1014
1066
|
},
|
|
1015
1067
|
properties: config.properties,
|
|
1016
1068
|
suites,
|
|
@@ -1343,7 +1395,7 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1343
1395
|
}
|
|
1344
1396
|
}
|
|
1345
1397
|
fs3.mkdirSync(this.config.outputDir, { recursive: true });
|
|
1346
|
-
const outputPath = path4.join(this.config.outputDir, `${(0,
|
|
1398
|
+
const outputPath = path4.join(this.config.outputDir, `${(0, import_node_crypto3.randomUUID)()}.json`);
|
|
1347
1399
|
fs3.writeFileSync(outputPath, JSON.stringify(payload));
|
|
1348
1400
|
logger.info(
|
|
1349
1401
|
`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${this.config.outputDir}\` to upload it.`
|