@qualflare/cucumberjs 0.1.0 → 0.3.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 +55 -14
- package/dist/formatter/index.cjs +230 -303
- package/dist/formatter/index.cjs.map +1 -1
- package/dist/formatter/index.d.cts +14 -1
- package/dist/formatter/index.d.ts +14 -1
- package/dist/formatter/index.js +230 -303
- package/dist/formatter/index.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/formatter/index.cjs
CHANGED
|
@@ -35,8 +35,22 @@ __export(formatter_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(formatter_exports);
|
|
36
36
|
|
|
37
37
|
// src/formatter/formatter.ts
|
|
38
|
+
var import_node_crypto3 = require("crypto");
|
|
39
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
40
|
+
var path4 = __toESM(require("path"), 1);
|
|
38
41
|
var import_cucumber = require("@cucumber/cucumber");
|
|
39
42
|
|
|
43
|
+
// src/config/resolve-config.ts
|
|
44
|
+
var import_node_crypto = require("crypto");
|
|
45
|
+
|
|
46
|
+
// src/shared/constants.ts
|
|
47
|
+
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
48
|
+
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
49
|
+
var MAX_CASES_PER_SUITE = 5e3;
|
|
50
|
+
var MAX_TAGS_PER_CASE = 64;
|
|
51
|
+
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
52
|
+
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
53
|
+
|
|
40
54
|
// src/config/ci-detect.ts
|
|
41
55
|
var ciInfo = __toESM(require("ci-info"), 1);
|
|
42
56
|
function parsePositiveInt(raw) {
|
|
@@ -54,6 +68,7 @@ var PROVIDERS = [
|
|
|
54
68
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
55
69
|
providerName: "GitHub Actions",
|
|
56
70
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
71
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
57
72
|
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,
|
|
58
73
|
prNumber: (env) => {
|
|
59
74
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -64,6 +79,7 @@ var PROVIDERS = [
|
|
|
64
79
|
detect: (env) => env.GITLAB_CI === "true",
|
|
65
80
|
providerName: "GitLab CI",
|
|
66
81
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
82
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
67
83
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
68
84
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
69
85
|
},
|
|
@@ -71,6 +87,7 @@ var PROVIDERS = [
|
|
|
71
87
|
detect: (env) => env.CIRCLECI === "true",
|
|
72
88
|
providerName: "CircleCI",
|
|
73
89
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
90
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
74
91
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
75
92
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
76
93
|
},
|
|
@@ -78,6 +95,7 @@ var PROVIDERS = [
|
|
|
78
95
|
detect: (env) => env.BUILDKITE === "true",
|
|
79
96
|
providerName: "Buildkite",
|
|
80
97
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
98
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
81
99
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
82
100
|
prNumber: (env) => {
|
|
83
101
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -93,6 +111,7 @@ var PROVIDERS = [
|
|
|
93
111
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
94
112
|
providerName: "Jenkins",
|
|
95
113
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
114
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
96
115
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
97
116
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
98
117
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -102,6 +121,7 @@ var PROVIDERS = [
|
|
|
102
121
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
103
122
|
providerName: "Azure Pipelines",
|
|
104
123
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
124
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
105
125
|
runUrl: (env) => {
|
|
106
126
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
107
127
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -117,6 +137,7 @@ var PROVIDERS = [
|
|
|
117
137
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
118
138
|
providerName: "Bitbucket Pipelines",
|
|
119
139
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
140
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
120
141
|
runUrl: (env) => {
|
|
121
142
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
122
143
|
if (!origin) {
|
|
@@ -138,6 +159,8 @@ function detectCi(env = process.env) {
|
|
|
138
159
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
139
160
|
const prNumber = provider.prNumber?.(env);
|
|
140
161
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
162
|
+
const runId = provider.runId?.(env);
|
|
163
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
141
164
|
return result;
|
|
142
165
|
}
|
|
143
166
|
if (ciInfo.name) {
|
|
@@ -208,22 +231,30 @@ function envInt(...names) {
|
|
|
208
231
|
const parsed = Number.parseInt(raw, 10);
|
|
209
232
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
210
233
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
function argvShardIndex(argv = process.argv) {
|
|
235
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
236
|
+
const arg = argv[i];
|
|
237
|
+
if (arg === void 0) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const raw = arg === "--shard" ? argv[i + 1] : arg.startsWith("--shard=") ? arg.slice("--shard=".length) : void 0;
|
|
241
|
+
if (raw === void 0) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!/^\d+\/\d+$/.test(raw)) {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
const oneBased = Number.parseInt(raw.split("/")[0] ?? "", 10);
|
|
248
|
+
return Number.isFinite(oneBased) && oneBased >= 1 ? oneBased - 1 : void 0;
|
|
215
249
|
}
|
|
216
|
-
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
217
252
|
function resolveConfig(options, deps = {}) {
|
|
218
253
|
const doDetectGit = deps.detectGit ?? detectGit;
|
|
219
254
|
const doDetectCi = deps.detectCi ?? detectCi;
|
|
220
255
|
const enabled = options.enabled ?? envBool("QUALFLARE_ENABLED") ?? true;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
throw new QualflareConfigError(
|
|
224
|
-
"qualflare-cucumberjs: no token configured. Set the `token` format option or the QUALFLARE_TOKEN (or QF_TOKEN) environment variable, or pass `enabled: false` to disable this formatter."
|
|
225
|
-
);
|
|
226
|
-
}
|
|
256
|
+
const outputDir = options.outputDir || firstEnv2("QUALFLARE_OUTPUT_DIR") || "./qualflare-results";
|
|
257
|
+
const shardIndex = options.shardIndex ?? envInt("QUALFLARE_SHARD_INDEX") ?? argvShardIndex();
|
|
227
258
|
const milestoneRaw = options.milestone !== void 0 ? options.milestone : envInt("QUALFLARE_MILESTONE", "QF_MILESTONE");
|
|
228
259
|
const milestone = milestoneRaw !== void 0 && milestoneRaw !== null && milestoneRaw >= 1 ? milestoneRaw : null;
|
|
229
260
|
const envBranch = firstEnv2("QUALFLARE_BRANCH", "QF_BRANCH");
|
|
@@ -237,14 +268,11 @@ function resolveConfig(options, deps = {}) {
|
|
|
237
268
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
238
269
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
239
270
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
271
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? (0, import_node_crypto.randomUUID)();
|
|
240
272
|
return {
|
|
241
|
-
token,
|
|
242
|
-
apiEndpoint: options.apiEndpoint ?? firstEnv2("QUALFLARE_API_ENDPOINT") ?? "https://api.qualflare.com",
|
|
243
273
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
244
274
|
// fields — an explicit `''` option must not silently win over the
|
|
245
|
-
// default (the server rejects an empty `environment
|
|
246
|
-
// `failOnUploadError` defaults `false`, that would fail the entire
|
|
247
|
-
// upload with no visible error by default). Ported verbatim from
|
|
275
|
+
// default (the server rejects an empty `environment`). Ported verbatim from
|
|
248
276
|
// qualflare-cypress, where this was found via deep adversarial review.
|
|
249
277
|
environment: (options.environment || void 0) ?? firstEnv2("QUALFLARE_ENVIRONMENT", "QF_ENVIRONMENT") ?? "development",
|
|
250
278
|
language: (options.language || void 0) ?? firstEnv2("QUALFLARE_LANGUAGE", "QF_LANGUAGE") ?? "en-US",
|
|
@@ -260,38 +288,19 @@ function resolveConfig(options, deps = {}) {
|
|
|
260
288
|
ciBuildNumber,
|
|
261
289
|
ciRunUrl,
|
|
262
290
|
ciPrNumber,
|
|
263
|
-
|
|
264
|
-
retry: {
|
|
265
|
-
max: options.retry?.max ?? envInt("QUALFLARE_RETRY_MAX", "QF_RETRY_MAX") ?? 3,
|
|
266
|
-
baseDelayMs: options.retry?.baseDelayMs ?? envInt("QUALFLARE_RETRY_BASE_DELAY_MS") ?? 1e3,
|
|
267
|
-
maxDelayMs: options.retry?.maxDelayMs ?? envInt("QUALFLARE_RETRY_MAX_DELAY_MS") ?? 3e4
|
|
268
|
-
},
|
|
269
|
-
failOnUploadError: options.failOnUploadError ?? envBool("QUALFLARE_FAIL_ON_UPLOAD_ERROR") ?? false,
|
|
291
|
+
runId,
|
|
270
292
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
271
293
|
includeStepHooks: options.includeStepHooks ?? envBool("QUALFLARE_INCLUDE_STEP_HOOKS") ?? false,
|
|
272
294
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
273
295
|
maxTotalAttachmentBytes: options.maxTotalAttachmentBytes ?? envInt("QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES") ?? 75e4,
|
|
296
|
+
maxVideoBytes: options.maxVideoBytes ?? envInt("QUALFLARE_MAX_VIDEO_BYTES") ?? MAX_VIDEO_UPLOAD_BYTES,
|
|
274
297
|
debug: options.debug ?? envBool("QUALFLARE_DEBUG", "QF_DEBUG") ?? false,
|
|
275
|
-
enabled
|
|
298
|
+
enabled,
|
|
299
|
+
outputDir,
|
|
300
|
+
shardIndex
|
|
276
301
|
};
|
|
277
302
|
}
|
|
278
303
|
|
|
279
|
-
// src/http/client.ts
|
|
280
|
-
var import_undici = require("undici");
|
|
281
|
-
|
|
282
|
-
// src/shared/constants.ts
|
|
283
|
-
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
284
|
-
var HEADER_TOKEN = "QF_TOKEN";
|
|
285
|
-
var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
|
|
286
|
-
var HEADER_CONTENT_TYPE = "Content-Type";
|
|
287
|
-
var HEADER_ACCEPT = "Accept";
|
|
288
|
-
var HEADER_USER_AGENT = "User-Agent";
|
|
289
|
-
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
290
|
-
var MAX_CASES_PER_SUITE = 5e3;
|
|
291
|
-
var MAX_TAGS_PER_CASE = 64;
|
|
292
|
-
var MAX_IDEMPOTENCY_KEY_CHARS = 255;
|
|
293
|
-
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
294
|
-
|
|
295
304
|
// src/shared/logger.ts
|
|
296
305
|
var PREFIX = "[qualflare-cucumberjs]";
|
|
297
306
|
var logger = {
|
|
@@ -309,220 +318,86 @@ var logger = {
|
|
|
309
318
|
}
|
|
310
319
|
};
|
|
311
320
|
|
|
312
|
-
// src/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const jittered = Math.random() * Math.min(exponential, maxDelayMs);
|
|
316
|
-
const floor = retryAfterMs !== void 0 ? retryAfterMs : 0;
|
|
317
|
-
return Math.min(Math.max(jittered, floor), maxDelayMs);
|
|
318
|
-
}
|
|
321
|
+
// src/formatter/attachment-budget.ts
|
|
322
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
323
|
+
var path2 = __toESM(require("path"), 1);
|
|
319
324
|
|
|
320
|
-
// src/
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
325
|
+
// src/formatter/video-writer.ts
|
|
326
|
+
var import_node_crypto2 = require("crypto");
|
|
327
|
+
var fs = __toESM(require("fs"), 1);
|
|
328
|
+
var path = __toESM(require("path"), 1);
|
|
329
|
+
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
330
|
+
".mp4": "video/mp4",
|
|
331
|
+
".webm": "video/webm",
|
|
332
|
+
".mov": "video/quicktime"
|
|
333
|
+
};
|
|
334
|
+
var EXTENSION_BY_VIDEO_MIME_TYPE = {
|
|
335
|
+
"video/mp4": ".mp4",
|
|
336
|
+
"video/webm": ".webm",
|
|
337
|
+
"video/quicktime": ".mov"
|
|
338
|
+
};
|
|
339
|
+
function resolveVideoMimeType(mimeType, filePath) {
|
|
340
|
+
if (filePath) {
|
|
341
|
+
const extension2 = path.extname(filePath).toLowerCase();
|
|
342
|
+
const resolvedMimeType = VIDEO_MIME_TYPES_BY_EXTENSION[extension2];
|
|
343
|
+
return resolvedMimeType ? { mimeType: resolvedMimeType, extension: extension2 } : void 0;
|
|
344
|
+
}
|
|
345
|
+
const normalized = mimeType?.toLowerCase();
|
|
346
|
+
const extension = normalized ? EXTENSION_BY_VIDEO_MIME_TYPE[normalized] : void 0;
|
|
347
|
+
return normalized && extension ? { mimeType: normalized, extension } : void 0;
|
|
344
348
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
349
|
+
function writeVideoAttachment(pending, outputDir, maxVideoBytes) {
|
|
350
|
+
const resolved = resolveVideoMimeType(pending.mimeType, pending.path);
|
|
351
|
+
if (!resolved) {
|
|
352
|
+
logger.warn(`skipping video attachment "${pending.name}": unsupported video format.`);
|
|
347
353
|
return void 0;
|
|
348
354
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
requestId;
|
|
359
|
-
fields;
|
|
360
|
-
constructor(init) {
|
|
361
|
-
const parts = [init.message];
|
|
362
|
-
const fieldsRendered = renderFields(init.fields);
|
|
363
|
-
if (fieldsRendered) {
|
|
364
|
-
parts.push(`fields: ${fieldsRendered}`);
|
|
355
|
+
const localVideoPath = `${(0, import_node_crypto2.randomUUID)()}${resolved.extension}`;
|
|
356
|
+
const destination = path.join(outputDir, localVideoPath);
|
|
357
|
+
if (pending.path !== void 0) {
|
|
358
|
+
let fileSize;
|
|
359
|
+
try {
|
|
360
|
+
fileSize = fs.statSync(pending.path).size;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.warn(`skipping video attachment "${pending.path}": could not stat file: ${err.message}`);
|
|
363
|
+
return void 0;
|
|
365
364
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
if (fileSize > maxVideoBytes) {
|
|
366
|
+
logger.warn(
|
|
367
|
+
`skipping video attachment "${pending.path}": ${fileSize} bytes exceeds the configured maxVideoBytes cap of ${maxVideoBytes} bytes.`
|
|
368
|
+
);
|
|
369
|
+
return void 0;
|
|
369
370
|
}
|
|
370
|
-
|
|
371
|
-
|
|
371
|
+
try {
|
|
372
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
373
|
+
fs.copyFileSync(pending.path, destination);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
logger.warn(`skipping video attachment "${pending.path}": could not copy file: ${err.message}`);
|
|
376
|
+
return void 0;
|
|
372
377
|
}
|
|
373
|
-
|
|
374
|
-
this.name = "QualflareApiError";
|
|
375
|
-
this.code = init.code;
|
|
376
|
-
this.statusCode = init.statusCode;
|
|
377
|
-
this.requestId = init.requestId;
|
|
378
|
-
this.fields = init.fields;
|
|
378
|
+
return { localVideoPath, fileSize, mimeType: resolved.mimeType };
|
|
379
379
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
message,
|
|
386
|
-
code,
|
|
387
|
-
statusCode,
|
|
388
|
-
requestId: body?.request_id,
|
|
389
|
-
fields: body?.fields
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// src/http/idempotency.ts
|
|
394
|
-
var import_node_crypto = require("crypto");
|
|
395
|
-
function newIdempotencyKey() {
|
|
396
|
-
const key = (0, import_node_crypto.randomUUID)();
|
|
397
|
-
if (key.length > MAX_IDEMPOTENCY_KEY_CHARS) {
|
|
398
|
-
return key.slice(0, MAX_IDEMPOTENCY_KEY_CHARS);
|
|
399
|
-
}
|
|
400
|
-
return key;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// src/http/client.ts
|
|
404
|
-
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
405
|
-
function redactToken(token) {
|
|
406
|
-
return token.length > 0 ? "***REDACTED***" : "(none)";
|
|
407
|
-
}
|
|
408
|
-
var QualflareHttpClient = class {
|
|
409
|
-
constructor(opts) {
|
|
410
|
-
this.opts = opts;
|
|
411
|
-
}
|
|
412
|
-
opts;
|
|
413
|
-
async send(collect) {
|
|
414
|
-
const url = `${this.opts.endpoint.replace(/\/+$/, "")}/api/v1/collect`;
|
|
415
|
-
const idempotencyKey = newIdempotencyKey();
|
|
416
|
-
const body = JSON.stringify(collect);
|
|
417
|
-
const maxAttempts = Math.max(1, this.opts.retry.max + 1);
|
|
418
|
-
let lastError;
|
|
419
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
420
|
-
if (this.opts.debug) {
|
|
421
|
-
logger.debug(
|
|
422
|
-
`POST ${url} (attempt ${attempt}/${maxAttempts}, QF_TOKEN: ${redactToken(this.opts.token)})`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
let statusCode;
|
|
426
|
-
let responseBody;
|
|
427
|
-
let responseHeaders;
|
|
428
|
-
try {
|
|
429
|
-
const res = await (0, import_undici.request)(url, {
|
|
430
|
-
method: "POST",
|
|
431
|
-
headers: {
|
|
432
|
-
[HEADER_TOKEN]: this.opts.token,
|
|
433
|
-
[HEADER_CONTENT_TYPE]: "application/json",
|
|
434
|
-
[HEADER_ACCEPT]: "application/json",
|
|
435
|
-
[HEADER_USER_AGENT]: this.opts.userAgent,
|
|
436
|
-
[HEADER_IDEMPOTENCY_KEY]: idempotencyKey
|
|
437
|
-
},
|
|
438
|
-
body,
|
|
439
|
-
maxRedirections: 0,
|
|
440
|
-
signal: AbortSignal.timeout(this.opts.timeoutMs)
|
|
441
|
-
});
|
|
442
|
-
statusCode = res.statusCode;
|
|
443
|
-
responseHeaders = res.headers;
|
|
444
|
-
responseBody = await res.body.text();
|
|
445
|
-
} catch (err) {
|
|
446
|
-
lastError = err;
|
|
447
|
-
if (this.opts.debug) {
|
|
448
|
-
logger.debug(`attempt ${attempt} transport error: ${err?.message ?? err}`);
|
|
449
|
-
}
|
|
450
|
-
if (attempt < maxAttempts) {
|
|
451
|
-
await sleep(computeDelay(attempt, this.opts.retry.baseDelayMs, this.opts.retry.maxDelayMs));
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
throw new QualflareApiError({
|
|
455
|
-
message: `failed to send request to ${url}`,
|
|
456
|
-
cause: err
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
if (this.opts.debug) {
|
|
460
|
-
logger.debug(`attempt ${attempt} response: ${statusCode}`);
|
|
461
|
-
}
|
|
462
|
-
if (statusCode >= 200 && statusCode < 300) {
|
|
463
|
-
return parseSuccess(responseBody);
|
|
464
|
-
}
|
|
465
|
-
const parsedError = parseErrorBody(responseBody);
|
|
466
|
-
if (!RETRYABLE_STATUS_CODES.has(statusCode) || attempt === maxAttempts) {
|
|
467
|
-
throw buildApiError(statusCode, parsedError);
|
|
468
|
-
}
|
|
469
|
-
const retryAfterMs = parseRetryAfter(responseHeaders["retry-after"]);
|
|
470
|
-
const delay = computeDelay(
|
|
471
|
-
attempt,
|
|
472
|
-
this.opts.retry.baseDelayMs,
|
|
473
|
-
this.opts.retry.maxDelayMs,
|
|
474
|
-
retryAfterMs
|
|
380
|
+
if (pending.content !== void 0) {
|
|
381
|
+
const fileSize = Buffer.byteLength(pending.content, "base64");
|
|
382
|
+
if (fileSize > maxVideoBytes) {
|
|
383
|
+
logger.warn(
|
|
384
|
+
`skipping video attachment "${pending.name}": ${fileSize} bytes exceeds the configured maxVideoBytes cap of ${maxVideoBytes} bytes.`
|
|
475
385
|
);
|
|
476
|
-
|
|
477
|
-
logger.debug(`retrying after ${Math.round(delay)}ms (status ${statusCode})`);
|
|
478
|
-
}
|
|
479
|
-
await sleep(delay);
|
|
386
|
+
return void 0;
|
|
480
387
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (typeof parsed.seq !== "number") {
|
|
488
|
-
throw new Error('response body missing numeric "seq"');
|
|
388
|
+
try {
|
|
389
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
390
|
+
fs.writeFileSync(destination, Buffer.from(pending.content, "base64"));
|
|
391
|
+
} catch (err) {
|
|
392
|
+
logger.warn(`skipping video attachment "${pending.name}": could not write file: ${err.message}`);
|
|
393
|
+
return void 0;
|
|
489
394
|
}
|
|
490
|
-
return
|
|
491
|
-
} catch (err) {
|
|
492
|
-
throw new QualflareApiError({
|
|
493
|
-
message: "server returned a success status but an unparseable body",
|
|
494
|
-
cause: err
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
function parseErrorBody(responseBody) {
|
|
499
|
-
if (!responseBody) {
|
|
500
|
-
return void 0;
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
return JSON.parse(responseBody);
|
|
504
|
-
} catch {
|
|
505
|
-
return void 0;
|
|
395
|
+
return { localVideoPath, fileSize, mimeType: resolved.mimeType };
|
|
506
396
|
}
|
|
397
|
+
return void 0;
|
|
507
398
|
}
|
|
508
|
-
function parseRetryAfter(value) {
|
|
509
|
-
const raw = Array.isArray(value) ? value[0] : value;
|
|
510
|
-
if (!raw) {
|
|
511
|
-
return void 0;
|
|
512
|
-
}
|
|
513
|
-
const seconds = Number(raw);
|
|
514
|
-
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
|
|
515
|
-
}
|
|
516
|
-
function sleep(ms) {
|
|
517
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// src/config/version.ts
|
|
521
|
-
var PACKAGE_VERSION = "0.1.0";
|
|
522
399
|
|
|
523
400
|
// src/formatter/attachment-budget.ts
|
|
524
|
-
var fs = __toESM(require("fs"), 1);
|
|
525
|
-
var path = __toESM(require("path"), 1);
|
|
526
401
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".mkv"]);
|
|
527
402
|
var AttachmentBudget = class {
|
|
528
403
|
constructor(maxTotalBytes) {
|
|
@@ -547,7 +422,7 @@ function isVideoLike(mimeType, filePath) {
|
|
|
547
422
|
if (mimeType?.toLowerCase().startsWith("video/")) {
|
|
548
423
|
return true;
|
|
549
424
|
}
|
|
550
|
-
if (filePath && VIDEO_EXTENSIONS.has(
|
|
425
|
+
if (filePath && VIDEO_EXTENSIONS.has(path2.extname(filePath).toLowerCase())) {
|
|
551
426
|
return true;
|
|
552
427
|
}
|
|
553
428
|
return false;
|
|
@@ -555,7 +430,7 @@ function isVideoLike(mimeType, filePath) {
|
|
|
555
430
|
function readAttachmentFile(filePath, maxAttachmentBytes, budget) {
|
|
556
431
|
let size;
|
|
557
432
|
try {
|
|
558
|
-
size =
|
|
433
|
+
size = fs2.statSync(filePath).size;
|
|
559
434
|
} catch (err) {
|
|
560
435
|
return { skipped: true, reason: `could not stat file: ${err.message}` };
|
|
561
436
|
}
|
|
@@ -572,7 +447,7 @@ function readAttachmentFile(filePath, maxAttachmentBytes, budget) {
|
|
|
572
447
|
};
|
|
573
448
|
}
|
|
574
449
|
try {
|
|
575
|
-
const content =
|
|
450
|
+
const content = fs2.readFileSync(filePath).toString("base64");
|
|
576
451
|
return { skipped: false, content };
|
|
577
452
|
} catch (err) {
|
|
578
453
|
return { skipped: true, reason: `could not read file: ${err.message}` };
|
|
@@ -582,12 +457,6 @@ function resolvePendingAttachment(pending, config, budget) {
|
|
|
582
457
|
if (!config.attachScreenshots) {
|
|
583
458
|
return void 0;
|
|
584
459
|
}
|
|
585
|
-
if (isVideoLike(pending.mimeType, pending.path)) {
|
|
586
|
-
logger.warn(
|
|
587
|
-
`refusing to attach "${pending.name}": video attachments are not supported yet (${pending.path ?? pending.mimeType ?? "unknown"}).`
|
|
588
|
-
);
|
|
589
|
-
return void 0;
|
|
590
|
-
}
|
|
591
460
|
if (pending.content !== void 0) {
|
|
592
461
|
const bytes = Buffer.byteLength(pending.content, "base64");
|
|
593
462
|
if (bytes > config.maxAttachmentBytes) {
|
|
@@ -620,6 +489,22 @@ function resolvePendingAttachment(pending, config, budget) {
|
|
|
620
489
|
}
|
|
621
490
|
return void 0;
|
|
622
491
|
}
|
|
492
|
+
async function resolveVideoAttachment(pending, config) {
|
|
493
|
+
if (!config.attachScreenshots) {
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
const written = writeVideoAttachment(pending, config.outputDir, config.maxVideoBytes);
|
|
497
|
+
if (!written) {
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name: pending.name,
|
|
502
|
+
mimeType: written.mimeType,
|
|
503
|
+
localVideoPath: written.localVideoPath,
|
|
504
|
+
fileSize: written.fileSize,
|
|
505
|
+
stepIndex: pending.stepIndex
|
|
506
|
+
};
|
|
507
|
+
}
|
|
623
508
|
|
|
624
509
|
// src/formatter/attempt-tracker.ts
|
|
625
510
|
var import_messages2 = require("@cucumber/messages");
|
|
@@ -866,7 +751,8 @@ var AttemptTracker = class {
|
|
|
866
751
|
tags: [],
|
|
867
752
|
properties: {},
|
|
868
753
|
attachments: [],
|
|
869
|
-
stepCapWarned: false
|
|
754
|
+
stepCapWarned: false,
|
|
755
|
+
pendingVideoWrites: []
|
|
870
756
|
};
|
|
871
757
|
this.byTestCaseStartedId.set(e.id, record);
|
|
872
758
|
const attempts = this.byTestCaseId.get(testCase.id) ?? [];
|
|
@@ -944,11 +830,25 @@ var AttemptTracker = class {
|
|
|
944
830
|
}
|
|
945
831
|
const stepIndex = this.resolveStepIndex(record, e.testStepId);
|
|
946
832
|
const content = e.contentEncoding === "BASE64" ? e.body : Buffer.from(e.body, "utf8").toString("base64");
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
833
|
+
this.resolveAttachment(record, { name: e.fileName || "attachment", mimeType: e.mediaType, content, stepIndex });
|
|
834
|
+
}
|
|
835
|
+
/** Resolves one pending attachment, routing a video-like one through the
|
|
836
|
+
* write-to-`outputDir` flow (tracked in `record.pendingVideoWrites` so
|
|
837
|
+
* `finish()` can wait for it) and everything else through the synchronous
|
|
838
|
+
* inline path — shared by the real `World.attach()` handler above and both
|
|
839
|
+
* `qualflare.attachment()`/`attachmentFromFile()` runtime-message cases
|
|
840
|
+
* below. */
|
|
841
|
+
resolveAttachment(record, pending) {
|
|
842
|
+
if (isVideoLike(pending.mimeType, pending.path)) {
|
|
843
|
+
const write = resolveVideoAttachment(pending, this.config).then((resolved2) => {
|
|
844
|
+
if (resolved2) {
|
|
845
|
+
record.attachments.push(resolved2);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
record.pendingVideoWrites.push(write);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const resolved = resolvePendingAttachment(pending, this.config, this.attachmentBudget);
|
|
952
852
|
if (resolved) {
|
|
953
853
|
record.attachments.push(resolved);
|
|
954
854
|
}
|
|
@@ -1005,26 +905,12 @@ var AttemptTracker = class {
|
|
|
1005
905
|
}
|
|
1006
906
|
case "attachment": {
|
|
1007
907
|
const stepIndex = testStepId !== void 0 ? record.stepIndexByTestStepId.get(testStepId) : void 0;
|
|
1008
|
-
|
|
1009
|
-
{ name: message.name, mimeType: message.mimeType, content: message.contentBase64, stepIndex },
|
|
1010
|
-
this.config,
|
|
1011
|
-
this.attachmentBudget
|
|
1012
|
-
);
|
|
1013
|
-
if (resolved) {
|
|
1014
|
-
record.attachments.push(resolved);
|
|
1015
|
-
}
|
|
908
|
+
this.resolveAttachment(record, { name: message.name, mimeType: message.mimeType, content: message.contentBase64, stepIndex });
|
|
1016
909
|
return;
|
|
1017
910
|
}
|
|
1018
911
|
case "attachment_from_file": {
|
|
1019
912
|
const stepIndex = testStepId !== void 0 ? record.stepIndexByTestStepId.get(testStepId) : void 0;
|
|
1020
|
-
|
|
1021
|
-
{ name: message.name, mimeType: message.mimeType, path: message.path, stepIndex },
|
|
1022
|
-
this.config,
|
|
1023
|
-
this.attachmentBudget
|
|
1024
|
-
);
|
|
1025
|
-
if (resolved) {
|
|
1026
|
-
record.attachments.push(resolved);
|
|
1027
|
-
}
|
|
913
|
+
this.resolveAttachment(record, { name: message.name, mimeType: message.mimeType, path: message.path, stepIndex });
|
|
1028
914
|
return;
|
|
1029
915
|
}
|
|
1030
916
|
case "step_start": {
|
|
@@ -1049,8 +935,21 @@ var AttemptTracker = class {
|
|
|
1049
935
|
}
|
|
1050
936
|
/** Returns the collapsed result once all attempts of this logical
|
|
1051
937
|
* scenario have arrived, or `undefined` if more attempts are coming
|
|
1052
|
-
* (`willBeRetried === true`).
|
|
1053
|
-
|
|
938
|
+
* (`willBeRetried === true`).
|
|
939
|
+
*
|
|
940
|
+
* Async because it must first await every attempt's own
|
|
941
|
+
* `pendingVideoWrites` (any video attached anywhere across every retry
|
|
942
|
+
* of this scenario). This is load-bearing, not just tidiness:
|
|
943
|
+
* `collapseAttempts`/`buildCase` read `attachments` by REFERENCE, not by
|
|
944
|
+
* copy, and `buildCase` runs synchronously right after this resolves — a
|
|
945
|
+
* scenario whose ONLY attachment is a still-uploading video would have an
|
|
946
|
+
* EMPTY `attachments` array at that instant, and `buildCase` captures
|
|
947
|
+
* `attachments.length > 0 ? attachments : undefined` as a plain
|
|
948
|
+
* `undefined` VALUE right then, permanently — a later push onto the
|
|
949
|
+
* (still-live) array reference would no longer be visible through
|
|
950
|
+
* `undefined`. Awaiting here first guarantees `attachments` is complete
|
|
951
|
+
* before `buildCase` ever reads it. */
|
|
952
|
+
async finish(e) {
|
|
1054
953
|
const record = this.byTestCaseStartedId.get(e.testCaseStartedId);
|
|
1055
954
|
this.byTestCaseStartedId.delete(e.testCaseStartedId);
|
|
1056
955
|
if (!record) {
|
|
@@ -1062,6 +961,10 @@ var AttemptTracker = class {
|
|
|
1062
961
|
const testCaseId = record.testCase.id;
|
|
1063
962
|
const attempts = this.byTestCaseId.get(testCaseId) ?? [record];
|
|
1064
963
|
this.byTestCaseId.delete(testCaseId);
|
|
964
|
+
const pendingWrites = attempts.flatMap((a) => a.pendingVideoWrites);
|
|
965
|
+
if (pendingWrites.length > 0) {
|
|
966
|
+
await Promise.all(pendingWrites);
|
|
967
|
+
}
|
|
1065
968
|
const snapshots = attempts.map((a) => {
|
|
1066
969
|
const worst = a.stepResults.length > 0 ? (0, import_messages2.getWorstTestStepResult)(a.stepResults) : void 0;
|
|
1067
970
|
const duration = a.stepResults.reduce((sum, r) => sum + messageDurationToNs(r.duration), 0);
|
|
@@ -1096,6 +999,11 @@ function timestampMs(ts) {
|
|
|
1096
999
|
|
|
1097
1000
|
// src/formatter/collect-builder.ts
|
|
1098
1001
|
var os = __toESM(require("os"), 1);
|
|
1002
|
+
|
|
1003
|
+
// src/config/version.ts
|
|
1004
|
+
var PACKAGE_VERSION = "0.3.0";
|
|
1005
|
+
|
|
1006
|
+
// src/formatter/collect-builder.ts
|
|
1099
1007
|
function resolveOs(config) {
|
|
1100
1008
|
if (config.os) {
|
|
1101
1009
|
return config.os;
|
|
@@ -1116,7 +1024,8 @@ function buildCollectPayload(suites, config) {
|
|
|
1116
1024
|
metadata: {
|
|
1117
1025
|
version: PACKAGE_VERSION,
|
|
1118
1026
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1119
|
-
cliName: "qualflare-cucumberjs"
|
|
1027
|
+
cliName: "qualflare-cucumberjs",
|
|
1028
|
+
runId: config.runId
|
|
1120
1029
|
},
|
|
1121
1030
|
properties: config.properties,
|
|
1122
1031
|
suites,
|
|
@@ -1255,7 +1164,7 @@ var RunHookTracker = class {
|
|
|
1255
1164
|
}
|
|
1256
1165
|
return {
|
|
1257
1166
|
name: "(global hooks)",
|
|
1258
|
-
category: "
|
|
1167
|
+
category: "cucumber",
|
|
1259
1168
|
duration: this.failed.reduce((sum, c) => sum + c.duration, 0),
|
|
1260
1169
|
cases: this.failed
|
|
1261
1170
|
};
|
|
@@ -1263,7 +1172,7 @@ var RunHookTracker = class {
|
|
|
1263
1172
|
};
|
|
1264
1173
|
|
|
1265
1174
|
// src/formatter/suite-builder.ts
|
|
1266
|
-
var
|
|
1175
|
+
var path3 = __toESM(require("path"), 1);
|
|
1267
1176
|
function groupIntoSuites(cases, cwd, extraSuite) {
|
|
1268
1177
|
const byUri = /* @__PURE__ */ new Map();
|
|
1269
1178
|
for (const { uri, case: kase } of cases) {
|
|
@@ -1283,7 +1192,7 @@ function groupIntoSuites(cases, cwd, extraSuite) {
|
|
|
1283
1192
|
}
|
|
1284
1193
|
suites.push({
|
|
1285
1194
|
name: relativizeUri(uri, cwd),
|
|
1286
|
-
category: "
|
|
1195
|
+
category: "cucumber",
|
|
1287
1196
|
duration: kases.reduce((sum, c) => sum + c.duration, 0),
|
|
1288
1197
|
cases: kases.slice(0, MAX_CASES_PER_SUITE)
|
|
1289
1198
|
});
|
|
@@ -1303,10 +1212,10 @@ function relativizeUri(uri, cwd) {
|
|
|
1303
1212
|
if (normalized.startsWith("file://")) {
|
|
1304
1213
|
normalized = new URL(normalized).pathname;
|
|
1305
1214
|
}
|
|
1306
|
-
if (
|
|
1307
|
-
normalized =
|
|
1215
|
+
if (path3.isAbsolute(normalized)) {
|
|
1216
|
+
normalized = path3.relative(cwd, normalized);
|
|
1308
1217
|
}
|
|
1309
|
-
return normalized.split(
|
|
1218
|
+
return normalized.split(path3.sep).join("/");
|
|
1310
1219
|
}
|
|
1311
1220
|
|
|
1312
1221
|
// src/formatter/formatter.ts
|
|
@@ -1320,12 +1229,25 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1320
1229
|
attemptTracker;
|
|
1321
1230
|
runHookTracker = new RunHookTracker();
|
|
1322
1231
|
finishedCases = [];
|
|
1232
|
+
/** One promise per `testCaseFinished` envelope, resolving once that
|
|
1233
|
+
* scenario's `AttemptTracker.finish()` (which itself awaits any pending
|
|
1234
|
+
* video uploads — see its doc comment) has settled and, if it produced a
|
|
1235
|
+
* result, been pushed into `finishedCases`. `finished()` awaits all of
|
|
1236
|
+
* these before building/uploading the Collect payload, so a scenario
|
|
1237
|
+
* whose only attachment is a still-uploading video is never silently
|
|
1238
|
+
* dropped from the report. */
|
|
1239
|
+
pendingCaseBuilds = [];
|
|
1323
1240
|
constructor(options) {
|
|
1324
1241
|
super(options);
|
|
1325
1242
|
this.config = resolveConfig(options.parsedArgvOptions);
|
|
1326
1243
|
this.hookIndex = buildHookIndex(options.supportCodeLibrary);
|
|
1327
1244
|
this.attachmentBudget = new AttachmentBudget(this.config.maxTotalAttachmentBytes);
|
|
1328
|
-
this.attemptTracker = new AttemptTracker(
|
|
1245
|
+
this.attemptTracker = new AttemptTracker(
|
|
1246
|
+
this.hookIndex,
|
|
1247
|
+
this.gherkin,
|
|
1248
|
+
this.config,
|
|
1249
|
+
this.attachmentBudget
|
|
1250
|
+
);
|
|
1329
1251
|
if (!this.config.enabled) {
|
|
1330
1252
|
return;
|
|
1331
1253
|
}
|
|
@@ -1376,15 +1298,20 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1376
1298
|
return;
|
|
1377
1299
|
}
|
|
1378
1300
|
if (envelope.testCaseFinished) {
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1301
|
+
const pending = this.attemptTracker.finish(envelope.testCaseFinished).then((finished) => {
|
|
1302
|
+
if (!finished) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1381
1305
|
const pickle = this.pickleIndex.get(finished.pickleId);
|
|
1382
1306
|
if (pickle) {
|
|
1383
1307
|
this.finishedCases.push(buildCase(finished.uri, pickle, finished.collapsed, this.gherkin));
|
|
1384
1308
|
} else {
|
|
1385
1309
|
logger.warn(`could not resolve pickle "${finished.pickleId}" for a finished scenario \u2014 it will not be uploaded.`);
|
|
1386
1310
|
}
|
|
1387
|
-
}
|
|
1311
|
+
}).catch((err) => {
|
|
1312
|
+
logger.error("failed to process a cucumber-js event:", err);
|
|
1313
|
+
});
|
|
1314
|
+
this.pendingCaseBuilds.push(pending);
|
|
1388
1315
|
return;
|
|
1389
1316
|
}
|
|
1390
1317
|
if (envelope.testRunHookStarted) {
|
|
@@ -1401,41 +1328,41 @@ var QualflareCucumberFormatter = class extends import_cucumber.Formatter {
|
|
|
1401
1328
|
}
|
|
1402
1329
|
async finished() {
|
|
1403
1330
|
try {
|
|
1331
|
+
await Promise.all(this.pendingCaseBuilds);
|
|
1404
1332
|
if (this.config.enabled) {
|
|
1405
|
-
|
|
1333
|
+
this.writeResults();
|
|
1406
1334
|
}
|
|
1407
1335
|
} finally {
|
|
1408
1336
|
await super.finished();
|
|
1409
1337
|
}
|
|
1410
1338
|
}
|
|
1411
|
-
|
|
1339
|
+
/** Writes this process's Collect payload into `outputDir` under a unique
|
|
1340
|
+
* filename. Never uploads: `qualflare-cli collect <outputDir>` does that,
|
|
1341
|
+
* merging every file it finds there into one Launch. Multiple shards can
|
|
1342
|
+
* therefore share one directory safely — the UUID filename is what keeps
|
|
1343
|
+
* them from overwriting each other. */
|
|
1344
|
+
writeResults() {
|
|
1412
1345
|
const suites = groupIntoSuites(this.finishedCases, this.cwd, this.runHookTracker.buildSuite());
|
|
1413
1346
|
if (suites.length === 0) {
|
|
1414
1347
|
if (this.config.debug) {
|
|
1415
|
-
logger.debug("no scenarios reported \u2014 skipping
|
|
1348
|
+
logger.debug("no scenarios reported \u2014 skipping file write.");
|
|
1416
1349
|
}
|
|
1417
1350
|
return;
|
|
1418
1351
|
}
|
|
1419
1352
|
const payload = buildCollectPayload(suites, this.config);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
userAgent: `qualflare-cucumberjs/${PACKAGE_VERSION}`,
|
|
1426
|
-
debug: this.config.debug
|
|
1427
|
-
});
|
|
1428
|
-
try {
|
|
1429
|
-
const result = await client.send(payload);
|
|
1430
|
-
if (this.config.debug) {
|
|
1431
|
-
logger.debug(`uploaded launch #${result.seq}.`);
|
|
1432
|
-
}
|
|
1433
|
-
} catch (err) {
|
|
1434
|
-
if (this.config.failOnUploadError) {
|
|
1435
|
-
throw err;
|
|
1353
|
+
if (this.config.shardIndex !== void 0) {
|
|
1354
|
+
for (const suite of payload.suites) {
|
|
1355
|
+
for (const c of suite.cases) {
|
|
1356
|
+
c.shardIndex = this.config.shardIndex;
|
|
1357
|
+
}
|
|
1436
1358
|
}
|
|
1437
|
-
logger.error("failed to upload results to Qualflare:", err);
|
|
1438
1359
|
}
|
|
1360
|
+
fs3.mkdirSync(this.config.outputDir, { recursive: true });
|
|
1361
|
+
const outputPath = path4.join(this.config.outputDir, `${(0, import_node_crypto3.randomUUID)()}.json`);
|
|
1362
|
+
fs3.writeFileSync(outputPath, JSON.stringify(payload));
|
|
1363
|
+
logger.info(
|
|
1364
|
+
`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${this.config.outputDir}\` to upload it.`
|
|
1365
|
+
);
|
|
1439
1366
|
}
|
|
1440
1367
|
};
|
|
1441
1368
|
//# sourceMappingURL=index.cjs.map
|