@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.
@@ -1,8 +1,11 @@
1
1
  // src/reporter/reporter.ts
2
- import { randomUUID as randomUUID2 } from "crypto";
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 = `${randomUUID()}${ext}`;
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.1.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, `${randomUUID2()}.json`);
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
  }