@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.
- package/README.md +21 -6
- package/dist/index.cjs +1 -0
- 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 +1 -0
- package/dist/index.js.map +1 -1
- package/dist/reporter/index.cjs +146 -19
- package/dist/reporter/index.cjs.map +1 -1
- package/dist/reporter/index.d.cts +4 -3
- package/dist/reporter/index.d.ts +4 -3
- package/dist/reporter/index.js +146 -19
- package/dist/reporter/index.js.map +1 -1
- package/dist/{resolve-config-CQe-oDpg.d.cts → resolve-config-T2ChZzAm.d.cts} +59 -0
- package/dist/{resolve-config-CQe-oDpg.d.ts → resolve-config-T2ChZzAm.d.ts} +59 -0
- package/package.json +1 -1
package/dist/reporter/index.js
CHANGED
|
@@ -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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
297
|
+
logger.warn(`skipping ${label} attachment "${filePath}": could not stat file: ${err.message}`);
|
|
294
298
|
return void 0;
|
|
295
299
|
}
|
|
296
|
-
if (fileSize >
|
|
300
|
+
if (fileSize > maxBytes) {
|
|
297
301
|
logger.warn(
|
|
298
|
-
`skipping
|
|
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
|
|
306
|
+
const localPath = `${randomUUID2()}${ext}`;
|
|
303
307
|
try {
|
|
304
308
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
305
|
-
fs.copyFileSync(filePath, path.join(outputDir,
|
|
309
|
+
fs.copyFileSync(filePath, path.join(outputDir, localPath));
|
|
306
310
|
} catch (err) {
|
|
307
|
-
logger.warn(`skipping
|
|
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 ===
|
|
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.
|
|
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
|
|
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
|
-
|
|
1028
|
+
const orphan = attachment.localVideoPath ?? attachment.localTracePath;
|
|
1029
|
+
if (orphan) {
|
|
903
1030
|
try {
|
|
904
|
-
fs3.rmSync(path3.join(this.resolveOutputDir(outputDir),
|
|
1031
|
+
fs3.rmSync(path3.join(this.resolveOutputDir(outputDir), orphan), { force: true });
|
|
905
1032
|
} catch {
|
|
906
1033
|
}
|
|
907
1034
|
}
|