@qualflare/playwright 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.
@@ -15,7 +15,14 @@ var MAX_LABELS_PER_CASE = 100;
15
15
  var MAX_LINKS_PER_CASE = 20;
16
16
  var MAX_TAGS_PER_CASE = 64;
17
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;
18
24
  var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
25
+ var MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;
19
26
  var MAX_STEPS_PER_TEST_ATTEMPT = 300;
20
27
 
21
28
  // src/config/ci-detect.ts
@@ -243,6 +250,7 @@ function resolveConfig(options, deps = {}) {
243
250
  maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
244
251
  maxTotalAttachmentBytes: options.maxTotalAttachmentBytes ?? envInt("QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES") ?? 75e4,
245
252
  maxVideoBytes: options.maxVideoBytes ?? envInt("QUALFLARE_MAX_VIDEO_BYTES") ?? MAX_VIDEO_UPLOAD_BYTES,
253
+ maxTraceBytes: options.maxTraceBytes ?? envInt("QUALFLARE_MAX_TRACE_BYTES") ?? MAX_TRACE_UPLOAD_BYTES,
246
254
  debug: options.debug ?? envBool("QUALFLARE_DEBUG", "QF_DEBUG") ?? false,
247
255
  enabled,
248
256
  outputDir,
@@ -279,35 +287,51 @@ var VIDEO_MIME_TYPES_BY_EXTENSION = {
279
287
  ".webm": "video/webm",
280
288
  ".mov": "video/quicktime"
281
289
  };
282
- function copyVideoAttachment(filePath, outputDir, maxVideoBytes) {
283
- const ext = path.extname(filePath).toLowerCase();
284
- const mimeType = VIDEO_MIME_TYPES_BY_EXTENSION[ext];
285
- if (!mimeType) {
286
- logger.warn(`skipping video attachment "${filePath}": unsupported video format.`);
287
- return void 0;
288
- }
290
+ var TRACE_MIME_TYPE = "application/zip";
291
+ var TRACE_EXTENSION = ".zip";
292
+ function copyArtifact(filePath, outputDir, maxBytes, ext, capName, label) {
289
293
  let fileSize;
290
294
  try {
291
295
  fileSize = fs.statSync(filePath).size;
292
296
  } catch (err) {
293
- logger.warn(`skipping video attachment "${filePath}": could not stat file: ${err.message}`);
297
+ logger.warn(`skipping ${label} attachment "${filePath}": could not stat file: ${err.message}`);
294
298
  return void 0;
295
299
  }
296
- if (fileSize > maxVideoBytes) {
300
+ if (fileSize > maxBytes) {
297
301
  logger.warn(
298
- `skipping video attachment "${filePath}": ${fileSize} bytes exceeds the configured maxVideoBytes cap of ${maxVideoBytes} bytes.`
302
+ `skipping ${label} attachment "${filePath}": ${fileSize} bytes exceeds the configured ${capName} cap of ${maxBytes} bytes.`
299
303
  );
300
304
  return void 0;
301
305
  }
302
- const localVideoPath = `${randomUUID2()}${ext}`;
306
+ const localPath = `${randomUUID2()}${ext}`;
303
307
  try {
304
308
  fs.mkdirSync(outputDir, { recursive: true });
305
- fs.copyFileSync(filePath, path.join(outputDir, localVideoPath));
309
+ fs.copyFileSync(filePath, path.join(outputDir, localPath));
306
310
  } catch (err) {
307
- logger.warn(`skipping video attachment "${filePath}": could not copy file: ${err.message}`);
311
+ logger.warn(`skipping ${label} attachment "${filePath}": could not copy file: ${err.message}`);
312
+ return void 0;
313
+ }
314
+ return { localPath, fileSize };
315
+ }
316
+ function copyVideoAttachment(filePath, outputDir, maxVideoBytes) {
317
+ const ext = path.extname(filePath).toLowerCase();
318
+ const mimeType = VIDEO_MIME_TYPES_BY_EXTENSION[ext];
319
+ if (!mimeType) {
320
+ logger.warn(`skipping video attachment "${filePath}": unsupported video format.`);
321
+ return void 0;
322
+ }
323
+ const copied = copyArtifact(filePath, outputDir, maxVideoBytes, ext, "maxVideoBytes", "video");
324
+ if (!copied) {
308
325
  return void 0;
309
326
  }
310
- return { localVideoPath, fileSize, mimeType };
327
+ return { localVideoPath: copied.localPath, fileSize: copied.fileSize, mimeType };
328
+ }
329
+ function copyTraceAttachment(filePath, outputDir, maxTraceBytes) {
330
+ const copied = copyArtifact(filePath, outputDir, maxTraceBytes, TRACE_EXTENSION, "maxTraceBytes", "trace");
331
+ if (!copied) {
332
+ return void 0;
333
+ }
334
+ return { localTracePath: copied.localPath, fileSize: copied.fileSize, mimeType: TRACE_MIME_TYPE };
311
335
  }
312
336
 
313
337
  // src/reporter/attachment-reader.ts
@@ -337,6 +361,7 @@ var AttachmentBudget = class {
337
361
  };
338
362
  var NAME_VIDEO = "video";
339
363
  var NAME_TRACE = "trace";
364
+ var TRACE_CONTENT_TYPE = "application/zip";
340
365
  function isVideo(a) {
341
366
  return a.name === NAME_VIDEO || (a.contentType?.startsWith("video/") ?? false);
342
367
  }
@@ -357,7 +382,20 @@ function resolveAttachments(result, config, budget) {
357
382
  }
358
383
  break;
359
384
  }
360
- if (a.name === NAME_TRACE || a.contentType === "application/zip") {
385
+ if (a.name === NAME_TRACE || a.contentType === TRACE_CONTENT_TYPE) {
386
+ if (!a.path) {
387
+ logger.warn(`skipping in-memory trace attachment "${a.name}": only file-backed traces are supported.`);
388
+ continue;
389
+ }
390
+ const copied = copyTraceAttachment(a.path, config.outputDir, config.maxTraceBytes);
391
+ if (copied) {
392
+ out.push({
393
+ name: a.name,
394
+ mimeType: copied.mimeType,
395
+ localTracePath: copied.localTracePath,
396
+ fileSize: copied.fileSize
397
+ });
398
+ }
361
399
  continue;
362
400
  }
363
401
  if (isVideo(a)) {
@@ -445,6 +483,34 @@ function msToNs(ms) {
445
483
  return Math.round(ms * NS_PER_MS);
446
484
  }
447
485
 
486
+ // src/shared/text.ts
487
+ function truncateRunes(value, maxRunes) {
488
+ if (value.length <= maxRunes) {
489
+ return value;
490
+ }
491
+ const runes = Array.from(value);
492
+ if (runes.length <= maxRunes) {
493
+ return value;
494
+ }
495
+ return runes.slice(0, maxRunes).join("");
496
+ }
497
+ function clampOutputLines(lines, maxLines, maxRunes) {
498
+ const out = [];
499
+ let budget = maxRunes;
500
+ for (const line of lines.slice(0, maxLines)) {
501
+ const cost = Array.from(line).length + 1;
502
+ if (cost > budget) {
503
+ if (budget > 1) {
504
+ out.push(truncateRunes(line, budget - 1));
505
+ }
506
+ break;
507
+ }
508
+ out.push(line);
509
+ budget -= cost;
510
+ }
511
+ return out.length > 0 ? out : void 0;
512
+ }
513
+
448
514
  // src/reporter/step-mapper.ts
449
515
  var CATEGORY_TEST_STEP = "test.step";
450
516
  var CATEGORY_EXPECT = "expect";
@@ -553,6 +619,63 @@ function formatError(result) {
553
619
  }
554
620
  return parts.join("\n");
555
621
  }
622
+ function outputLines(chunks) {
623
+ if (!chunks || chunks.length === 0) {
624
+ return void 0;
625
+ }
626
+ const text = chunks.map((c) => typeof c === "string" ? c : c.toString("utf8")).join("");
627
+ const lines = stripAnsi(text).split("\n");
628
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
629
+ lines.pop();
630
+ }
631
+ return clampOutputLines(lines, MAX_ATTEMPT_OUTPUT_LINES, MAX_ATTEMPT_OUTPUT_RUNES);
632
+ }
633
+ function buildAttempts(results) {
634
+ if (results.length < 2) {
635
+ return void 0;
636
+ }
637
+ let kept = results;
638
+ if (results.length > MAX_ATTEMPTS_PER_CASE) {
639
+ kept = [...results.slice(0, MAX_ATTEMPTS_PER_CASE - 1), results[results.length - 1]];
640
+ }
641
+ return kept.map((r, i) => {
642
+ const err = r.error;
643
+ const attempt = {
644
+ // 1-based and contiguous. Deliberately the index rather than
645
+ // `r.retry`: results are already ordered by attempt, and a filtered-out
646
+ // never-ran result would leave a hole in the retry numbering that the
647
+ // server reads as a truncated history.
648
+ attempt: i + 1,
649
+ status: mapStatus(r.status),
650
+ duration: msToNs(r.duration),
651
+ startedAt: r.startTime.toISOString()
652
+ };
653
+ if (err) {
654
+ const message = err.message ?? err.value;
655
+ if (message) {
656
+ attempt.message = truncateRunes(stripAnsi(message), MAX_ATTEMPT_MESSAGE_RUNES);
657
+ }
658
+ if (err.stack) {
659
+ attempt.trace = truncateRunes(stripAnsi(err.stack), MAX_ATTEMPT_TRACE_RUNES);
660
+ }
661
+ if (err.snippet) {
662
+ attempt.snippet = truncateRunes(stripAnsi(err.snippet), MAX_ATTEMPT_SNIPPET_RUNES);
663
+ }
664
+ if (typeof err.location?.line === "number") {
665
+ attempt.line = err.location.line;
666
+ }
667
+ }
668
+ const stdout = outputLines(r.stdout);
669
+ if (stdout) {
670
+ attempt.stdout = stdout;
671
+ }
672
+ const stderr = outputLines(r.stderr);
673
+ if (stderr) {
674
+ attempt.stderr = stderr;
675
+ }
676
+ return attempt;
677
+ });
678
+ }
556
679
  function replayMetadata(result, config, budget) {
557
680
  const meta = {
558
681
  labels: [],
@@ -675,6 +798,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
675
798
  const nativeTags = test.tags ?? [];
676
799
  const tags = capTags([...nativeTags, ...meta.tags]);
677
800
  const error = formatError(final);
801
+ const attempts = buildAttempts(results);
678
802
  return {
679
803
  id: test.id,
680
804
  name: test.title,
@@ -683,6 +807,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
683
807
  duration: msToNs(final.duration),
684
808
  retryCount: results.length - 1,
685
809
  isFlaky: outcome === "flaky",
810
+ ...attempts ? { attempts } : {},
686
811
  ...error ? { error } : {},
687
812
  ...meta.priority ? { priority: meta.priority } : {},
688
813
  ...meta.description ? { description: meta.description } : {},
@@ -700,7 +825,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
700
825
  import * as os from "os";
701
826
 
702
827
  // src/config/version.ts
703
- var PACKAGE_VERSION = "0.2.0";
828
+ var PACKAGE_VERSION = "0.4.0";
704
829
 
705
830
  // src/reporter/collect-builder.ts
706
831
  function resolveOs(config) {
@@ -891,7 +1016,8 @@ var QualflareReporter = class {
891
1016
  return path3.isAbsolute(outputDir) ? outputDir : path3.resolve(this.options.configDir ?? this.rootDir, outputDir);
892
1017
  }
893
1018
  /** Drops everything an earlier, now-superseded attempt produced: deletes the
894
- * video copied into outputDir and refunds its bytes to the run budget. */
1019
+ * video or trace copied into outputDir and refunds its bytes to the run
1020
+ * budget. */
895
1021
  discardSupersededAttempt(testId, retry, outputDir) {
896
1022
  const previous = this.latestAttemptByTest.get(testId);
897
1023
  if (previous === void 0 || previous >= retry) {
@@ -899,9 +1025,10 @@ var QualflareReporter = class {
899
1025
  }
900
1026
  const key = `${testId}:${previous}`;
901
1027
  for (const attachment of this.attachmentsByResult.get(key) ?? []) {
902
- if (attachment.localVideoPath) {
1028
+ const orphan = attachment.localVideoPath ?? attachment.localTracePath;
1029
+ if (orphan) {
903
1030
  try {
904
- fs3.rmSync(path3.join(this.resolveOutputDir(outputDir), attachment.localVideoPath), { force: true });
1031
+ fs3.rmSync(path3.join(this.resolveOutputDir(outputDir), orphan), { force: true });
905
1032
  } catch {
906
1033
  }
907
1034
  }