@qualflare/playwright 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 +9 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/reporter/index.cjs +114 -6
- package/dist/reporter/index.cjs.map +1 -1
- package/dist/reporter/index.d.cts +2 -2
- package/dist/reporter/index.d.ts +2 -2
- package/dist/reporter/index.js +114 -6
- package/dist/reporter/index.js.map +1 -1
- package/dist/{resolve-config-CSjXFl7v.d.cts → resolve-config-N04M1Avn.d.cts} +67 -0
- package/dist/{resolve-config-CSjXFl7v.d.ts → resolve-config-N04M1Avn.d.ts} +67 -0
- package/package.json +1 -1
package/dist/reporter/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// src/reporter/reporter.ts
|
|
2
|
-
import { randomUUID as
|
|
2
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3
3
|
import * as fs3 from "fs";
|
|
4
4
|
import * as path3 from "path";
|
|
5
5
|
|
|
6
|
+
// src/config/resolve-config.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
|
|
6
9
|
// src/shared/constants.ts
|
|
7
10
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
8
11
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
@@ -12,6 +15,12 @@ var MAX_LABELS_PER_CASE = 100;
|
|
|
12
15
|
var MAX_LINKS_PER_CASE = 20;
|
|
13
16
|
var MAX_TAGS_PER_CASE = 64;
|
|
14
17
|
var MAX_TAG_LENGTH = 255;
|
|
18
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
19
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
20
|
+
var MAX_ATTEMPT_TRACE_RUNES = 32768;
|
|
21
|
+
var MAX_ATTEMPT_SNIPPET_RUNES = 4096;
|
|
22
|
+
var MAX_ATTEMPT_OUTPUT_RUNES = 16384;
|
|
23
|
+
var MAX_ATTEMPT_OUTPUT_LINES = 200;
|
|
15
24
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
16
25
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
17
26
|
|
|
@@ -32,6 +41,7 @@ var PROVIDERS = [
|
|
|
32
41
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
33
42
|
providerName: "GitHub Actions",
|
|
34
43
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
44
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
35
45
|
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,
|
|
36
46
|
prNumber: (env) => {
|
|
37
47
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -42,6 +52,7 @@ var PROVIDERS = [
|
|
|
42
52
|
detect: (env) => env.GITLAB_CI === "true",
|
|
43
53
|
providerName: "GitLab CI",
|
|
44
54
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
55
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
45
56
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
46
57
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
47
58
|
},
|
|
@@ -49,6 +60,7 @@ var PROVIDERS = [
|
|
|
49
60
|
detect: (env) => env.CIRCLECI === "true",
|
|
50
61
|
providerName: "CircleCI",
|
|
51
62
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
63
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
52
64
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
53
65
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
54
66
|
},
|
|
@@ -56,6 +68,7 @@ var PROVIDERS = [
|
|
|
56
68
|
detect: (env) => env.BUILDKITE === "true",
|
|
57
69
|
providerName: "Buildkite",
|
|
58
70
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
71
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
59
72
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
60
73
|
prNumber: (env) => {
|
|
61
74
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -71,6 +84,7 @@ var PROVIDERS = [
|
|
|
71
84
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
72
85
|
providerName: "Jenkins",
|
|
73
86
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
87
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
74
88
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
75
89
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
76
90
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -80,6 +94,7 @@ var PROVIDERS = [
|
|
|
80
94
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
81
95
|
providerName: "Azure Pipelines",
|
|
82
96
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
97
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
83
98
|
runUrl: (env) => {
|
|
84
99
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
85
100
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -95,6 +110,7 @@ var PROVIDERS = [
|
|
|
95
110
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
96
111
|
providerName: "Bitbucket Pipelines",
|
|
97
112
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
113
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
98
114
|
runUrl: (env) => {
|
|
99
115
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
100
116
|
if (!origin) {
|
|
@@ -116,6 +132,8 @@ function detectCi(env = process.env) {
|
|
|
116
132
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
117
133
|
const prNumber = provider.prNumber?.(env);
|
|
118
134
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
135
|
+
const runId = provider.runId?.(env);
|
|
136
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
119
137
|
return result;
|
|
120
138
|
}
|
|
121
139
|
if (ciInfo.name) {
|
|
@@ -205,6 +223,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
205
223
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
206
224
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
207
225
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
226
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? randomUUID();
|
|
208
227
|
return {
|
|
209
228
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
210
229
|
// fields — an explicit `''` option must not silently win over the
|
|
@@ -224,6 +243,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
224
243
|
ciBuildNumber,
|
|
225
244
|
ciRunUrl,
|
|
226
245
|
ciPrNumber,
|
|
246
|
+
runId,
|
|
227
247
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
228
248
|
includeApiSteps: options.includeApiSteps ?? envBool("QUALFLARE_INCLUDE_API_STEPS") ?? false,
|
|
229
249
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
@@ -259,7 +279,7 @@ import * as fs2 from "fs";
|
|
|
259
279
|
// src/reporter/video-writer.ts
|
|
260
280
|
import * as fs from "fs";
|
|
261
281
|
import * as path from "path";
|
|
262
|
-
import { randomUUID } from "crypto";
|
|
282
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
263
283
|
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
264
284
|
".mp4": "video/mp4",
|
|
265
285
|
".webm": "video/webm",
|
|
@@ -285,7 +305,7 @@ function copyVideoAttachment(filePath, outputDir, maxVideoBytes) {
|
|
|
285
305
|
);
|
|
286
306
|
return void 0;
|
|
287
307
|
}
|
|
288
|
-
const localVideoPath = `${
|
|
308
|
+
const localVideoPath = `${randomUUID2()}${ext}`;
|
|
289
309
|
try {
|
|
290
310
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
291
311
|
fs.copyFileSync(filePath, path.join(outputDir, localVideoPath));
|
|
@@ -431,6 +451,34 @@ function msToNs(ms) {
|
|
|
431
451
|
return Math.round(ms * NS_PER_MS);
|
|
432
452
|
}
|
|
433
453
|
|
|
454
|
+
// src/shared/text.ts
|
|
455
|
+
function truncateRunes(value, maxRunes) {
|
|
456
|
+
if (value.length <= maxRunes) {
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
const runes = Array.from(value);
|
|
460
|
+
if (runes.length <= maxRunes) {
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
return runes.slice(0, maxRunes).join("");
|
|
464
|
+
}
|
|
465
|
+
function clampOutputLines(lines, maxLines, maxRunes) {
|
|
466
|
+
const out = [];
|
|
467
|
+
let budget = maxRunes;
|
|
468
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
469
|
+
const cost = Array.from(line).length + 1;
|
|
470
|
+
if (cost > budget) {
|
|
471
|
+
if (budget > 1) {
|
|
472
|
+
out.push(truncateRunes(line, budget - 1));
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
out.push(line);
|
|
477
|
+
budget -= cost;
|
|
478
|
+
}
|
|
479
|
+
return out.length > 0 ? out : void 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
434
482
|
// src/reporter/step-mapper.ts
|
|
435
483
|
var CATEGORY_TEST_STEP = "test.step";
|
|
436
484
|
var CATEGORY_EXPECT = "expect";
|
|
@@ -539,6 +587,63 @@ function formatError(result) {
|
|
|
539
587
|
}
|
|
540
588
|
return parts.join("\n");
|
|
541
589
|
}
|
|
590
|
+
function outputLines(chunks) {
|
|
591
|
+
if (!chunks || chunks.length === 0) {
|
|
592
|
+
return void 0;
|
|
593
|
+
}
|
|
594
|
+
const text = chunks.map((c) => typeof c === "string" ? c : c.toString("utf8")).join("");
|
|
595
|
+
const lines = stripAnsi(text).split("\n");
|
|
596
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
597
|
+
lines.pop();
|
|
598
|
+
}
|
|
599
|
+
return clampOutputLines(lines, MAX_ATTEMPT_OUTPUT_LINES, MAX_ATTEMPT_OUTPUT_RUNES);
|
|
600
|
+
}
|
|
601
|
+
function buildAttempts(results) {
|
|
602
|
+
if (results.length < 2) {
|
|
603
|
+
return void 0;
|
|
604
|
+
}
|
|
605
|
+
let kept = results;
|
|
606
|
+
if (results.length > MAX_ATTEMPTS_PER_CASE) {
|
|
607
|
+
kept = [...results.slice(0, MAX_ATTEMPTS_PER_CASE - 1), results[results.length - 1]];
|
|
608
|
+
}
|
|
609
|
+
return kept.map((r, i) => {
|
|
610
|
+
const err = r.error;
|
|
611
|
+
const attempt = {
|
|
612
|
+
// 1-based and contiguous. Deliberately the index rather than
|
|
613
|
+
// `r.retry`: results are already ordered by attempt, and a filtered-out
|
|
614
|
+
// never-ran result would leave a hole in the retry numbering that the
|
|
615
|
+
// server reads as a truncated history.
|
|
616
|
+
attempt: i + 1,
|
|
617
|
+
status: mapStatus(r.status),
|
|
618
|
+
duration: msToNs(r.duration),
|
|
619
|
+
startedAt: r.startTime.toISOString()
|
|
620
|
+
};
|
|
621
|
+
if (err) {
|
|
622
|
+
const message = err.message ?? err.value;
|
|
623
|
+
if (message) {
|
|
624
|
+
attempt.message = truncateRunes(stripAnsi(message), MAX_ATTEMPT_MESSAGE_RUNES);
|
|
625
|
+
}
|
|
626
|
+
if (err.stack) {
|
|
627
|
+
attempt.trace = truncateRunes(stripAnsi(err.stack), MAX_ATTEMPT_TRACE_RUNES);
|
|
628
|
+
}
|
|
629
|
+
if (err.snippet) {
|
|
630
|
+
attempt.snippet = truncateRunes(stripAnsi(err.snippet), MAX_ATTEMPT_SNIPPET_RUNES);
|
|
631
|
+
}
|
|
632
|
+
if (typeof err.location?.line === "number") {
|
|
633
|
+
attempt.line = err.location.line;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const stdout = outputLines(r.stdout);
|
|
637
|
+
if (stdout) {
|
|
638
|
+
attempt.stdout = stdout;
|
|
639
|
+
}
|
|
640
|
+
const stderr = outputLines(r.stderr);
|
|
641
|
+
if (stderr) {
|
|
642
|
+
attempt.stderr = stderr;
|
|
643
|
+
}
|
|
644
|
+
return attempt;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
542
647
|
function replayMetadata(result, config, budget) {
|
|
543
648
|
const meta = {
|
|
544
649
|
labels: [],
|
|
@@ -661,6 +766,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
661
766
|
const nativeTags = test.tags ?? [];
|
|
662
767
|
const tags = capTags([...nativeTags, ...meta.tags]);
|
|
663
768
|
const error = formatError(final);
|
|
769
|
+
const attempts = buildAttempts(results);
|
|
664
770
|
return {
|
|
665
771
|
id: test.id,
|
|
666
772
|
name: test.title,
|
|
@@ -669,6 +775,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
669
775
|
duration: msToNs(final.duration),
|
|
670
776
|
retryCount: results.length - 1,
|
|
671
777
|
isFlaky: outcome === "flaky",
|
|
778
|
+
...attempts ? { attempts } : {},
|
|
672
779
|
...error ? { error } : {},
|
|
673
780
|
...meta.priority ? { priority: meta.priority } : {},
|
|
674
781
|
...meta.description ? { description: meta.description } : {},
|
|
@@ -686,7 +793,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
686
793
|
import * as os from "os";
|
|
687
794
|
|
|
688
795
|
// src/config/version.ts
|
|
689
|
-
var PACKAGE_VERSION = "0.
|
|
796
|
+
var PACKAGE_VERSION = "0.3.0";
|
|
690
797
|
|
|
691
798
|
// src/reporter/collect-builder.ts
|
|
692
799
|
function resolveOs(config) {
|
|
@@ -715,7 +822,8 @@ function buildCollectPayload(suites, config, browsers = []) {
|
|
|
715
822
|
metadata: {
|
|
716
823
|
version: PACKAGE_VERSION,
|
|
717
824
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
718
|
-
cliName: "qualflare-playwright"
|
|
825
|
+
cliName: "qualflare-playwright",
|
|
826
|
+
runId: config.runId
|
|
719
827
|
},
|
|
720
828
|
properties: config.properties,
|
|
721
829
|
suites,
|
|
@@ -865,7 +973,7 @@ var QualflareReporter = class {
|
|
|
865
973
|
}
|
|
866
974
|
const outputDir = this.resolveOutputDir(config.outputDir);
|
|
867
975
|
fs3.mkdirSync(outputDir, { recursive: true });
|
|
868
|
-
const outputPath = path3.join(outputDir, `${
|
|
976
|
+
const outputPath = path3.join(outputDir, `${randomUUID3()}.json`);
|
|
869
977
|
fs3.writeFileSync(outputPath, JSON.stringify(collect));
|
|
870
978
|
logger.info(`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${outputDir}\` to upload it.`);
|
|
871
979
|
}
|