@reportforge/playwright-pdf 0.28.0 → 0.30.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 +15 -6
- package/dist/cli/index.js +20 -2
- package/dist/index.d.mts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +276 -76
- package/dist/index.mjs +282 -82
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +2 -0
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/browser-matrix.hbs +34 -0
- package/dist/templates/partials/charts.hbs +9 -1
- package/dist/templates/partials/defect-log.hbs +2 -2
- package/dist/templates/partials/run-diff.hbs +2 -1
- package/dist/templates/styles/base.css +55 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1221,13 +1221,13 @@ var require_ast = __commonJS({
|
|
|
1221
1221
|
helperExpression: function helperExpression(node) {
|
|
1222
1222
|
return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
|
|
1223
1223
|
},
|
|
1224
|
-
scopedId: function scopedId(
|
|
1225
|
-
return /^\.|this\b/.test(
|
|
1224
|
+
scopedId: function scopedId(path16) {
|
|
1225
|
+
return /^\.|this\b/.test(path16.original);
|
|
1226
1226
|
},
|
|
1227
1227
|
// an ID is simple if it only has one part, and that part is not
|
|
1228
1228
|
// `..` or `this`.
|
|
1229
|
-
simpleId: function simpleId(
|
|
1230
|
-
return
|
|
1229
|
+
simpleId: function simpleId(path16) {
|
|
1230
|
+
return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.depth;
|
|
1231
1231
|
}
|
|
1232
1232
|
}
|
|
1233
1233
|
};
|
|
@@ -2301,12 +2301,12 @@ var require_helpers2 = __commonJS({
|
|
|
2301
2301
|
loc
|
|
2302
2302
|
};
|
|
2303
2303
|
}
|
|
2304
|
-
function prepareMustache(
|
|
2304
|
+
function prepareMustache(path16, params, hash, open, strip, locInfo) {
|
|
2305
2305
|
var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
|
|
2306
2306
|
var decorator = /\*/.test(open);
|
|
2307
2307
|
return {
|
|
2308
2308
|
type: decorator ? "Decorator" : "MustacheStatement",
|
|
2309
|
-
path:
|
|
2309
|
+
path: path16,
|
|
2310
2310
|
params,
|
|
2311
2311
|
hash,
|
|
2312
2312
|
escaped,
|
|
@@ -2626,9 +2626,9 @@ var require_compiler = __commonJS({
|
|
|
2626
2626
|
},
|
|
2627
2627
|
DecoratorBlock: function DecoratorBlock(decorator) {
|
|
2628
2628
|
var program = decorator.program && this.compileProgram(decorator.program);
|
|
2629
|
-
var params = this.setupFullMustacheParams(decorator, program, void 0),
|
|
2629
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
|
|
2630
2630
|
this.useDecorators = true;
|
|
2631
|
-
this.opcode("registerDecorator", params.length,
|
|
2631
|
+
this.opcode("registerDecorator", params.length, path16.original);
|
|
2632
2632
|
},
|
|
2633
2633
|
PartialStatement: function PartialStatement(partial) {
|
|
2634
2634
|
this.usePartial = true;
|
|
@@ -2692,46 +2692,46 @@ var require_compiler = __commonJS({
|
|
|
2692
2692
|
}
|
|
2693
2693
|
},
|
|
2694
2694
|
ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
|
|
2695
|
-
var
|
|
2696
|
-
this.opcode("getContext",
|
|
2695
|
+
var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
|
|
2696
|
+
this.opcode("getContext", path16.depth);
|
|
2697
2697
|
this.opcode("pushProgram", program);
|
|
2698
2698
|
this.opcode("pushProgram", inverse);
|
|
2699
|
-
|
|
2700
|
-
this.accept(
|
|
2699
|
+
path16.strict = true;
|
|
2700
|
+
this.accept(path16);
|
|
2701
2701
|
this.opcode("invokeAmbiguous", name, isBlock);
|
|
2702
2702
|
},
|
|
2703
2703
|
simpleSexpr: function simpleSexpr(sexpr) {
|
|
2704
|
-
var
|
|
2705
|
-
|
|
2706
|
-
this.accept(
|
|
2704
|
+
var path16 = sexpr.path;
|
|
2705
|
+
path16.strict = true;
|
|
2706
|
+
this.accept(path16);
|
|
2707
2707
|
this.opcode("resolvePossibleLambda");
|
|
2708
2708
|
},
|
|
2709
2709
|
helperSexpr: function helperSexpr(sexpr, program, inverse) {
|
|
2710
|
-
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
|
2710
|
+
var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.parts[0];
|
|
2711
2711
|
if (this.options.knownHelpers[name]) {
|
|
2712
2712
|
this.opcode("invokeKnownHelper", params.length, name);
|
|
2713
2713
|
} else if (this.options.knownHelpersOnly) {
|
|
2714
2714
|
throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
|
2715
2715
|
} else {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
this.accept(
|
|
2719
|
-
this.opcode("invokeHelper", params.length,
|
|
2716
|
+
path16.strict = true;
|
|
2717
|
+
path16.falsy = true;
|
|
2718
|
+
this.accept(path16);
|
|
2719
|
+
this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
|
|
2720
2720
|
}
|
|
2721
2721
|
},
|
|
2722
|
-
PathExpression: function PathExpression(
|
|
2723
|
-
this.addDepth(
|
|
2724
|
-
this.opcode("getContext",
|
|
2725
|
-
var name =
|
|
2722
|
+
PathExpression: function PathExpression(path16) {
|
|
2723
|
+
this.addDepth(path16.depth);
|
|
2724
|
+
this.opcode("getContext", path16.depth);
|
|
2725
|
+
var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
|
|
2726
2726
|
if (blockParamId) {
|
|
2727
|
-
this.opcode("lookupBlockParam", blockParamId,
|
|
2727
|
+
this.opcode("lookupBlockParam", blockParamId, path16.parts);
|
|
2728
2728
|
} else if (!name) {
|
|
2729
2729
|
this.opcode("pushContext");
|
|
2730
|
-
} else if (
|
|
2730
|
+
} else if (path16.data) {
|
|
2731
2731
|
this.options.data = true;
|
|
2732
|
-
this.opcode("lookupData",
|
|
2732
|
+
this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
|
|
2733
2733
|
} else {
|
|
2734
|
-
this.opcode("lookupOnContext",
|
|
2734
|
+
this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.strict, scoped);
|
|
2735
2735
|
}
|
|
2736
2736
|
},
|
|
2737
2737
|
StringLiteral: function StringLiteral(string) {
|
|
@@ -3087,16 +3087,16 @@ var require_util = __commonJS({
|
|
|
3087
3087
|
}
|
|
3088
3088
|
exports2.urlGenerate = urlGenerate;
|
|
3089
3089
|
function normalize(aPath) {
|
|
3090
|
-
var
|
|
3090
|
+
var path16 = aPath;
|
|
3091
3091
|
var url = urlParse(aPath);
|
|
3092
3092
|
if (url) {
|
|
3093
3093
|
if (!url.path) {
|
|
3094
3094
|
return aPath;
|
|
3095
3095
|
}
|
|
3096
|
-
|
|
3096
|
+
path16 = url.path;
|
|
3097
3097
|
}
|
|
3098
|
-
var isAbsolute = exports2.isAbsolute(
|
|
3099
|
-
var parts =
|
|
3098
|
+
var isAbsolute = exports2.isAbsolute(path16);
|
|
3099
|
+
var parts = path16.split(/\/+/);
|
|
3100
3100
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
3101
3101
|
part = parts[i];
|
|
3102
3102
|
if (part === ".") {
|
|
@@ -3113,15 +3113,15 @@ var require_util = __commonJS({
|
|
|
3113
3113
|
}
|
|
3114
3114
|
}
|
|
3115
3115
|
}
|
|
3116
|
-
|
|
3117
|
-
if (
|
|
3118
|
-
|
|
3116
|
+
path16 = parts.join("/");
|
|
3117
|
+
if (path16 === "") {
|
|
3118
|
+
path16 = isAbsolute ? "/" : ".";
|
|
3119
3119
|
}
|
|
3120
3120
|
if (url) {
|
|
3121
|
-
url.path =
|
|
3121
|
+
url.path = path16;
|
|
3122
3122
|
return urlGenerate(url);
|
|
3123
3123
|
}
|
|
3124
|
-
return
|
|
3124
|
+
return path16;
|
|
3125
3125
|
}
|
|
3126
3126
|
exports2.normalize = normalize;
|
|
3127
3127
|
function join(aRoot, aPath) {
|
|
@@ -5924,8 +5924,8 @@ var require_printer = __commonJS({
|
|
|
5924
5924
|
return this.accept(sexpr.path) + " " + params + hash;
|
|
5925
5925
|
};
|
|
5926
5926
|
PrintVisitor.prototype.PathExpression = function(id) {
|
|
5927
|
-
var
|
|
5928
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
5927
|
+
var path16 = id.parts.join("/");
|
|
5928
|
+
return (id.data ? "@" : "") + "PATH:" + path16;
|
|
5929
5929
|
};
|
|
5930
5930
|
PrintVisitor.prototype.StringLiteral = function(string) {
|
|
5931
5931
|
return '"' + string.value + '"';
|
|
@@ -6045,7 +6045,7 @@ var require_win32 = __commonJS({
|
|
|
6045
6045
|
"node_modules/chrome-finder/lib/win32.js"(exports2, module2) {
|
|
6046
6046
|
"use strict";
|
|
6047
6047
|
init_cjs_shims();
|
|
6048
|
-
var
|
|
6048
|
+
var path16 = require("path");
|
|
6049
6049
|
var { canAccess } = require_util2();
|
|
6050
6050
|
function win32() {
|
|
6051
6051
|
const installations = [];
|
|
@@ -6059,7 +6059,7 @@ var require_win32 = __commonJS({
|
|
|
6059
6059
|
const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
|
|
6060
6060
|
prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
|
|
6061
6061
|
if (prefix) {
|
|
6062
|
-
const chromePath =
|
|
6062
|
+
const chromePath = path16.join(prefix, suffix);
|
|
6063
6063
|
if (canAccess(chromePath)) {
|
|
6064
6064
|
installations.push(chromePath);
|
|
6065
6065
|
}
|
|
@@ -6077,7 +6077,7 @@ var require_darwin = __commonJS({
|
|
|
6077
6077
|
"use strict";
|
|
6078
6078
|
init_cjs_shims();
|
|
6079
6079
|
var { execSync: execSync3 } = require("child_process");
|
|
6080
|
-
var
|
|
6080
|
+
var path16 = require("path");
|
|
6081
6081
|
var { canAccess, newLineRegex, sort } = require_util2();
|
|
6082
6082
|
function darwin() {
|
|
6083
6083
|
const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
|
|
@@ -6087,7 +6087,7 @@ var require_darwin = __commonJS({
|
|
|
6087
6087
|
${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
|
|
6088
6088
|
`).toString().split(newLineRegex).forEach((inst) => {
|
|
6089
6089
|
suffixes.forEach((suffix) => {
|
|
6090
|
-
const execPath =
|
|
6090
|
+
const execPath = path16.join(inst.trim(), suffix);
|
|
6091
6091
|
if (canAccess(execPath)) {
|
|
6092
6092
|
installations.push(execPath);
|
|
6093
6093
|
}
|
|
@@ -6116,7 +6116,7 @@ var require_linux = __commonJS({
|
|
|
6116
6116
|
"use strict";
|
|
6117
6117
|
init_cjs_shims();
|
|
6118
6118
|
var { execSync: execSync3, execFileSync } = require("child_process");
|
|
6119
|
-
var
|
|
6119
|
+
var path16 = require("path");
|
|
6120
6120
|
var fs16 = require("fs");
|
|
6121
6121
|
var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
|
|
6122
6122
|
function findChromeExecutablesForLinuxDesktop(folder) {
|
|
@@ -6134,7 +6134,7 @@ var require_linux = __commonJS({
|
|
|
6134
6134
|
function linux() {
|
|
6135
6135
|
let installations = [];
|
|
6136
6136
|
const desktopInstallationFolders = [
|
|
6137
|
-
|
|
6137
|
+
path16.join(require("os").homedir(), ".local/share/applications/"),
|
|
6138
6138
|
"/usr/share/applications/"
|
|
6139
6139
|
];
|
|
6140
6140
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -6749,8 +6749,8 @@ function getErrorMap() {
|
|
|
6749
6749
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
6750
6750
|
init_cjs_shims();
|
|
6751
6751
|
var makeIssue = (params) => {
|
|
6752
|
-
const { data, path:
|
|
6753
|
-
const fullPath = [...
|
|
6752
|
+
const { data, path: path16, errorMaps, issueData } = params;
|
|
6753
|
+
const fullPath = [...path16, ...issueData.path || []];
|
|
6754
6754
|
const fullIssue = {
|
|
6755
6755
|
...issueData,
|
|
6756
6756
|
path: fullPath
|
|
@@ -6870,11 +6870,11 @@ var errorUtil;
|
|
|
6870
6870
|
|
|
6871
6871
|
// node_modules/zod/v3/types.js
|
|
6872
6872
|
var ParseInputLazyPath = class {
|
|
6873
|
-
constructor(parent, value,
|
|
6873
|
+
constructor(parent, value, path16, key) {
|
|
6874
6874
|
this._cachedPath = [];
|
|
6875
6875
|
this.parent = parent;
|
|
6876
6876
|
this.data = value;
|
|
6877
|
-
this._path =
|
|
6877
|
+
this._path = path16;
|
|
6878
6878
|
this._key = key;
|
|
6879
6879
|
}
|
|
6880
6880
|
get path() {
|
|
@@ -10330,6 +10330,7 @@ var DEFAULT_OPTIONS = {
|
|
|
10330
10330
|
historySize: 10,
|
|
10331
10331
|
showTrend: true,
|
|
10332
10332
|
flakinessTopN: 5,
|
|
10333
|
+
quarantineThreshold: 40,
|
|
10333
10334
|
slowTestThreshold: 10,
|
|
10334
10335
|
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
10335
10336
|
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
@@ -10355,6 +10356,7 @@ var SECTION_KEYS = [
|
|
|
10355
10356
|
"defectLog",
|
|
10356
10357
|
"briefBand",
|
|
10357
10358
|
"runDiff",
|
|
10359
|
+
"browserMatrix",
|
|
10358
10360
|
// display modifiers
|
|
10359
10361
|
"passRate",
|
|
10360
10362
|
"fullEnvironment",
|
|
@@ -10379,6 +10381,7 @@ var SECTION_DEFAULTS = {
|
|
|
10379
10381
|
defectLog: false,
|
|
10380
10382
|
briefBand: false,
|
|
10381
10383
|
runDiff: true,
|
|
10384
|
+
browserMatrix: false,
|
|
10382
10385
|
passRate: true,
|
|
10383
10386
|
fullEnvironment: false,
|
|
10384
10387
|
retries: true,
|
|
@@ -10401,6 +10404,7 @@ var SECTION_DEFAULTS = {
|
|
|
10401
10404
|
defectLog: true,
|
|
10402
10405
|
briefBand: false,
|
|
10403
10406
|
runDiff: true,
|
|
10407
|
+
browserMatrix: true,
|
|
10404
10408
|
passRate: true,
|
|
10405
10409
|
fullEnvironment: true,
|
|
10406
10410
|
retries: true,
|
|
@@ -10428,6 +10432,7 @@ var SECTION_DEFAULTS = {
|
|
|
10428
10432
|
defectLog: false,
|
|
10429
10433
|
briefBand: true,
|
|
10430
10434
|
runDiff: true,
|
|
10435
|
+
browserMatrix: false,
|
|
10431
10436
|
passRate: true,
|
|
10432
10437
|
fullEnvironment: false,
|
|
10433
10438
|
retries: false,
|
|
@@ -10580,6 +10585,10 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10580
10585
|
// local history file is wiped every run. Aggregate numbers only.
|
|
10581
10586
|
remoteHistory: external_exports.boolean().default(false),
|
|
10582
10587
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10588
|
+
quarantineThreshold: external_exports.number().int().min(0, "quarantineThreshold must be between 0 and 100").max(100, "quarantineThreshold must be between 0 and 100").optional().default(DEFAULT_OPTIONS.quarantineThreshold),
|
|
10589
|
+
evidenceUrlTemplate: external_exports.string().refine((s) => s.includes("{path}"), {
|
|
10590
|
+
message: "evidenceUrlTemplate must contain the {path} token"
|
|
10591
|
+
}).optional(),
|
|
10583
10592
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10584
10593
|
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10585
10594
|
try {
|
|
@@ -11296,7 +11305,7 @@ var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
|
|
|
11296
11305
|
var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
|
|
11297
11306
|
function flatten(steps) {
|
|
11298
11307
|
const out = [];
|
|
11299
|
-
const
|
|
11308
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
11300
11309
|
for (const s of nodes ?? []) {
|
|
11301
11310
|
const index = out.length;
|
|
11302
11311
|
out.push({
|
|
@@ -11306,10 +11315,10 @@ function flatten(steps) {
|
|
|
11306
11315
|
hasError: s.error != null,
|
|
11307
11316
|
ancestors
|
|
11308
11317
|
});
|
|
11309
|
-
if (s.steps?.length)
|
|
11318
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
11310
11319
|
}
|
|
11311
11320
|
};
|
|
11312
|
-
|
|
11321
|
+
walk2(steps ?? [], 0, []);
|
|
11313
11322
|
return out;
|
|
11314
11323
|
}
|
|
11315
11324
|
function materialize(n, isFailing, depth) {
|
|
@@ -11917,7 +11926,7 @@ var ShardMerger = class {
|
|
|
11917
11926
|
merge(filePaths) {
|
|
11918
11927
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11919
11928
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11920
|
-
const reports = this.loadReports(resolved);
|
|
11929
|
+
const reports = this.loadReports(resolved).map((r) => this.normalizeReport(r));
|
|
11921
11930
|
const projects = this.mergeProjects(reports);
|
|
11922
11931
|
const stats = this.mergeStats(reports, projects);
|
|
11923
11932
|
const failures = sortBySeverity(this.extractFailures(reports));
|
|
@@ -11955,6 +11964,49 @@ var ShardMerger = class {
|
|
|
11955
11964
|
return report;
|
|
11956
11965
|
});
|
|
11957
11966
|
}
|
|
11967
|
+
/**
|
|
11968
|
+
* Two top-level shapes reach the merger. Our language adapters (Python,
|
|
11969
|
+
* .NET) and the engine fixtures emit PROJECT wrapper suites (no `file`,
|
|
11970
|
+
* children are the file suites). Playwright's own --reporter=json emits
|
|
11971
|
+
* FILE suites at top level and hangs the project on each
|
|
11972
|
+
* spec.tests[].projectName instead. Everything downstream (mergeProjects,
|
|
11973
|
+
* extractFailures) assumes the wrapper shape, so real Playwright reports
|
|
11974
|
+
* are pivoted into it here: one wrapper per project, file trees filtered
|
|
11975
|
+
* to that project's tests. An unnamed project ('') renders as 'default',
|
|
11976
|
+
* matching the live reporter path.
|
|
11977
|
+
*/
|
|
11978
|
+
normalizeReport(report) {
|
|
11979
|
+
if (report.suites.every((s) => s.file === void 0)) return report;
|
|
11980
|
+
const wrappers = this.collectProjectNames(report).map((name) => ({
|
|
11981
|
+
title: name === "" ? "default" : name,
|
|
11982
|
+
specs: [],
|
|
11983
|
+
suites: report.suites.map((fileSuite) => this.filterSuiteForProject(fileSuite, name)).filter((s) => s !== null)
|
|
11984
|
+
})).filter((w) => w.suites.length > 0);
|
|
11985
|
+
return { ...report, suites: wrappers };
|
|
11986
|
+
}
|
|
11987
|
+
/** Config order first, then any project discovered only on the tests. */
|
|
11988
|
+
collectProjectNames(report) {
|
|
11989
|
+
const names = [];
|
|
11990
|
+
const push = (n) => {
|
|
11991
|
+
if (!names.includes(n)) names.push(n);
|
|
11992
|
+
};
|
|
11993
|
+
for (const p of report.config?.projects ?? []) push(p.name ?? "");
|
|
11994
|
+
const walk2 = (suite) => {
|
|
11995
|
+
for (const spec of suite.specs ?? []) for (const t of spec.tests ?? []) push(t.projectName ?? "");
|
|
11996
|
+
for (const child of suite.suites ?? []) walk2(child);
|
|
11997
|
+
};
|
|
11998
|
+
for (const s of report.suites) walk2(s);
|
|
11999
|
+
return names.length > 0 ? names : [""];
|
|
12000
|
+
}
|
|
12001
|
+
filterSuiteForProject(suite, project) {
|
|
12002
|
+
const specs = (suite.specs ?? []).map((spec) => ({
|
|
12003
|
+
...spec,
|
|
12004
|
+
tests: (spec.tests ?? []).filter((t) => (t.projectName ?? "") === project)
|
|
12005
|
+
})).filter((spec) => spec.tests.length > 0);
|
|
12006
|
+
const children = (suite.suites ?? []).map((child) => this.filterSuiteForProject(child, project)).filter((c) => c !== null);
|
|
12007
|
+
if (specs.length === 0 && children.length === 0) return null;
|
|
12008
|
+
return { ...suite, specs, suites: children };
|
|
12009
|
+
}
|
|
11958
12010
|
mergeProjects(reports) {
|
|
11959
12011
|
const projectMap = /* @__PURE__ */ new Map();
|
|
11960
12012
|
for (const report of reports) {
|
|
@@ -13735,6 +13787,118 @@ function sourceTally(classified) {
|
|
|
13735
13787
|
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
13736
13788
|
}
|
|
13737
13789
|
|
|
13790
|
+
// src/collector/BrowserMatrix.ts
|
|
13791
|
+
init_cjs_shims();
|
|
13792
|
+
var DEFAULT_ROW_CAP = 40;
|
|
13793
|
+
function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
13794
|
+
if (projects.length < 2) return void 0;
|
|
13795
|
+
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13796
|
+
const byTest = /* @__PURE__ */ new Map();
|
|
13797
|
+
projects.forEach((project, column) => {
|
|
13798
|
+
walk(project.suites, [], (path16, test) => {
|
|
13799
|
+
const key = [...path16, test.title].join(" > ");
|
|
13800
|
+
let statuses = byTest.get(key);
|
|
13801
|
+
if (!statuses) {
|
|
13802
|
+
statuses = projectNames.map(() => null);
|
|
13803
|
+
byTest.set(key, statuses);
|
|
13804
|
+
}
|
|
13805
|
+
statuses[column] = test.status;
|
|
13806
|
+
});
|
|
13807
|
+
});
|
|
13808
|
+
const divergent = [];
|
|
13809
|
+
let identicalCount = 0;
|
|
13810
|
+
const contributing = /* @__PURE__ */ new Set();
|
|
13811
|
+
for (const [title, statuses] of byTest) {
|
|
13812
|
+
const present = statuses.filter((s) => s !== null);
|
|
13813
|
+
if (present.length < 2) continue;
|
|
13814
|
+
statuses.forEach((s, i) => {
|
|
13815
|
+
if (s !== null) contributing.add(i);
|
|
13816
|
+
});
|
|
13817
|
+
if (present.every((s) => s === present[0])) {
|
|
13818
|
+
identicalCount++;
|
|
13819
|
+
} else {
|
|
13820
|
+
divergent.push({ title, statuses });
|
|
13821
|
+
}
|
|
13822
|
+
}
|
|
13823
|
+
if (divergent.length === 0 && identicalCount === 0) return void 0;
|
|
13824
|
+
if (contributing.size < projectNames.length) {
|
|
13825
|
+
const keep = [...contributing].sort((a, b) => a - b);
|
|
13826
|
+
projectNames = keep.map((i) => projectNames[i]);
|
|
13827
|
+
for (const row of divergent) row.statuses = keep.map((i) => row.statuses[i]);
|
|
13828
|
+
}
|
|
13829
|
+
divergent.sort((a, b) => severityRank(b) - severityRank(a) || a.title.localeCompare(b.title));
|
|
13830
|
+
return {
|
|
13831
|
+
projects: projectNames,
|
|
13832
|
+
rows: divergent.slice(0, cap),
|
|
13833
|
+
divergentCount: divergent.length,
|
|
13834
|
+
identicalCount,
|
|
13835
|
+
truncated: divergent.length > cap,
|
|
13836
|
+
hasFailures: divergent.some((r) => severityRank(r) > 0)
|
|
13837
|
+
};
|
|
13838
|
+
}
|
|
13839
|
+
function severityRank(row) {
|
|
13840
|
+
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13841
|
+
}
|
|
13842
|
+
function walk(suites, path16, visit) {
|
|
13843
|
+
for (const suite of suites) {
|
|
13844
|
+
const next = [...path16, suite.title];
|
|
13845
|
+
for (const test of suite.tests) visit(next, test);
|
|
13846
|
+
walk(suite.suites, next, visit);
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
13849
|
+
|
|
13850
|
+
// src/utils/evidence-links.ts
|
|
13851
|
+
init_cjs_shims();
|
|
13852
|
+
var import_node_path6 = __toESM(require("path"));
|
|
13853
|
+
var import_qrcode_generator = __toESM(require("qrcode-generator"));
|
|
13854
|
+
function applyEvidenceUrls(data, template, cwd) {
|
|
13855
|
+
let warned = false;
|
|
13856
|
+
const warnOnce = (err) => {
|
|
13857
|
+
if (warned) return;
|
|
13858
|
+
warned = true;
|
|
13859
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13860
|
+
logger.warn(`evidenceUrlTemplate: link generation failed for at least one artifact \u2014 ${message}`);
|
|
13861
|
+
};
|
|
13862
|
+
for (const failure of data.failures) {
|
|
13863
|
+
const evidence = failure.execution?.evidence;
|
|
13864
|
+
if (!evidence) continue;
|
|
13865
|
+
if (evidence.tracePath) {
|
|
13866
|
+
try {
|
|
13867
|
+
evidence.traceUrl = expandTemplate(template, relativizeArtifact(evidence.tracePath, cwd));
|
|
13868
|
+
evidence.traceQr = qrDataUri(evidence.traceUrl);
|
|
13869
|
+
} catch (err) {
|
|
13870
|
+
warnOnce(err);
|
|
13871
|
+
}
|
|
13872
|
+
}
|
|
13873
|
+
if (evidence.videoPath) {
|
|
13874
|
+
try {
|
|
13875
|
+
evidence.videoUrl = expandTemplate(template, relativizeArtifact(evidence.videoPath, cwd));
|
|
13876
|
+
evidence.videoQr = qrDataUri(evidence.videoUrl);
|
|
13877
|
+
} catch (err) {
|
|
13878
|
+
warnOnce(err);
|
|
13879
|
+
}
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
function relativizeArtifact(artifactPath, cwd) {
|
|
13884
|
+
if (!import_node_path6.default.isAbsolute(artifactPath)) return artifactPath;
|
|
13885
|
+
const rel = import_node_path6.default.relative(cwd, artifactPath);
|
|
13886
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path6.default.sep}`) || import_node_path6.default.isAbsolute(rel)) return artifactPath;
|
|
13887
|
+
return rel;
|
|
13888
|
+
}
|
|
13889
|
+
function expandTemplate(template, artifactPath) {
|
|
13890
|
+
const normalized = artifactPath.replace(/\\/g, "/");
|
|
13891
|
+
const encoded = normalized.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
13892
|
+
return template.split("{path}").join(encoded);
|
|
13893
|
+
}
|
|
13894
|
+
function qrDataUri(url) {
|
|
13895
|
+
const qr = (0, import_qrcode_generator.default)(0, "M");
|
|
13896
|
+
qr.addData(url);
|
|
13897
|
+
qr.make();
|
|
13898
|
+
const svg = qr.createSvgTag({ cellSize: 2, margin: 2, scalable: true });
|
|
13899
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
13900
|
+
}
|
|
13901
|
+
|
|
13738
13902
|
// src/redact/redact-report.ts
|
|
13739
13903
|
init_cjs_shims();
|
|
13740
13904
|
function redactReportData(data, r) {
|
|
@@ -13765,6 +13929,10 @@ function redactReportData(data, r) {
|
|
|
13765
13929
|
data.briefSentence = rd(data.briefSentence);
|
|
13766
13930
|
data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
|
|
13767
13931
|
data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
|
|
13932
|
+
if (data.browserMatrix) {
|
|
13933
|
+
data.browserMatrix.projects = data.browserMatrix.projects.map((p) => r.redactText(p));
|
|
13934
|
+
data.browserMatrix.rows.forEach((row) => row.title = r.redactText(row.title));
|
|
13935
|
+
}
|
|
13768
13936
|
data.environment.buildUrl = r.redactText(data.environment.buildUrl);
|
|
13769
13937
|
}
|
|
13770
13938
|
function redactStep(s, r) {
|
|
@@ -13896,7 +14064,7 @@ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses
|
|
|
13896
14064
|
return { fixed, noLongerRun };
|
|
13897
14065
|
}
|
|
13898
14066
|
function computeRunDiff(args) {
|
|
13899
|
-
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
14067
|
+
const { currentFailing, currentFlaky = [], currentStatuses, priorEntries, branch } = args;
|
|
13900
14068
|
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
13901
14069
|
if (!baseline) return null;
|
|
13902
14070
|
const baselineFailed = /* @__PURE__ */ new Map();
|
|
@@ -13905,7 +14073,15 @@ function computeRunDiff(args) {
|
|
|
13905
14073
|
}
|
|
13906
14074
|
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
13907
14075
|
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
13908
|
-
|
|
14076
|
+
const baselineFlakyIds = new Set((baseline.flakyTests ?? []).map((t) => t.id));
|
|
14077
|
+
const newlyFlaky = [];
|
|
14078
|
+
const seenFlaky = /* @__PURE__ */ new Set();
|
|
14079
|
+
for (const item of currentFlaky) {
|
|
14080
|
+
if (seenFlaky.has(item.id) || baselineFlakyIds.has(item.id)) continue;
|
|
14081
|
+
seenFlaky.add(item.id);
|
|
14082
|
+
newlyFlaky.push(item);
|
|
14083
|
+
}
|
|
14084
|
+
for (const bucket of [newFailures, stillFailing, fixed, noLongerRun, newlyFlaky]) {
|
|
13909
14085
|
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
13910
14086
|
}
|
|
13911
14087
|
return {
|
|
@@ -13913,6 +14089,7 @@ function computeRunDiff(args) {
|
|
|
13913
14089
|
stillFailing,
|
|
13914
14090
|
fixed,
|
|
13915
14091
|
noLongerRun,
|
|
14092
|
+
newlyFlaky,
|
|
13916
14093
|
baselineRunId: baseline.runId,
|
|
13917
14094
|
baselineTimestamp: baseline.timestamp,
|
|
13918
14095
|
baselineTruncated: baseline.failedTestsTruncated === true
|
|
@@ -13921,10 +14098,11 @@ function computeRunDiff(args) {
|
|
|
13921
14098
|
|
|
13922
14099
|
// src/history/FlakinessAnalyser.ts
|
|
13923
14100
|
init_cjs_shims();
|
|
13924
|
-
|
|
13925
|
-
|
|
14101
|
+
var QUARANTINE_MIN_RUNS = 3;
|
|
14102
|
+
function computeTopN(entries, topN, quarantineThreshold = 0) {
|
|
14103
|
+
if (topN === 0) return { rows: [], quarantineTotal: 0 };
|
|
13926
14104
|
const qualifying = entries.filter((e) => Array.isArray(e.flakyTests));
|
|
13927
|
-
if (qualifying.length === 0) return [];
|
|
14105
|
+
if (qualifying.length === 0) return { rows: [], quarantineTotal: 0 };
|
|
13928
14106
|
const map = /* @__PURE__ */ new Map();
|
|
13929
14107
|
qualifying.forEach((entry, idx) => {
|
|
13930
14108
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13942,17 +14120,26 @@ function computeTopN(entries, topN) {
|
|
|
13942
14120
|
const originalIdx = qualifying.length - 1 - reversedIdx;
|
|
13943
14121
|
return hits.has(originalIdx);
|
|
13944
14122
|
});
|
|
14123
|
+
let streak = 0;
|
|
14124
|
+
while (streak < totalRuns && hits.has(streak)) streak++;
|
|
14125
|
+
const flakeRate = Math.round(flakeCount / totalRuns * 100);
|
|
13945
14126
|
return {
|
|
13946
14127
|
title,
|
|
13947
|
-
flakeRate
|
|
14128
|
+
flakeRate,
|
|
13948
14129
|
flakeCount,
|
|
13949
14130
|
totalRuns,
|
|
13950
|
-
sparkline
|
|
14131
|
+
sparkline,
|
|
14132
|
+
streak,
|
|
14133
|
+
quarantine: quarantineThreshold > 0 && totalRuns >= QUARANTINE_MIN_RUNS && flakeRate >= quarantineThreshold
|
|
13951
14134
|
};
|
|
13952
14135
|
});
|
|
13953
|
-
|
|
13954
|
-
|
|
13955
|
-
|
|
14136
|
+
const quarantineTotal = rows.filter((r) => r.quarantine).length;
|
|
14137
|
+
return {
|
|
14138
|
+
rows: rows.sort(
|
|
14139
|
+
(a, b) => b.flakeRate - a.flakeRate || b.flakeCount - a.flakeCount || a.title.localeCompare(b.title)
|
|
14140
|
+
).slice(0, topN),
|
|
14141
|
+
quarantineTotal
|
|
14142
|
+
};
|
|
13956
14143
|
}
|
|
13957
14144
|
|
|
13958
14145
|
// src/notify/index.ts
|
|
@@ -14395,6 +14582,9 @@ async function runReportPipeline(hooks) {
|
|
|
14395
14582
|
}
|
|
14396
14583
|
}
|
|
14397
14584
|
const reportData = buildReportData(collected, licenseInfo, hooks);
|
|
14585
|
+
if (options.evidenceUrlTemplate) {
|
|
14586
|
+
applyEvidenceUrls(reportData, options.evidenceUrlTemplate, hooks.cwd);
|
|
14587
|
+
}
|
|
14398
14588
|
runFailureAnalysis(reportData, options, hooks.cwd);
|
|
14399
14589
|
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14400
14590
|
if (hooks.redactor) {
|
|
@@ -14443,7 +14633,7 @@ async function appendTrend(collected, licenseInfo, hooks) {
|
|
|
14443
14633
|
branch: collected.environment.branch
|
|
14444
14634
|
};
|
|
14445
14635
|
const localEntries = hm.append(entry, options.historySize);
|
|
14446
|
-
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
14636
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch, flakyTests);
|
|
14447
14637
|
let trendEntries = localEntries;
|
|
14448
14638
|
if (options.remoteHistory && licenseInfo.jwt) {
|
|
14449
14639
|
const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -14476,9 +14666,17 @@ function populateHistoryCharts(trendEntries, localEntries, collected, options) {
|
|
|
14476
14666
|
};
|
|
14477
14667
|
}
|
|
14478
14668
|
if (options.flakinessTopN > 0) {
|
|
14479
|
-
const
|
|
14480
|
-
|
|
14481
|
-
|
|
14669
|
+
const { rows, quarantineTotal } = computeTopN(
|
|
14670
|
+
localEntries,
|
|
14671
|
+
options.flakinessTopN,
|
|
14672
|
+
options.quarantineThreshold
|
|
14673
|
+
);
|
|
14674
|
+
if (rows.length > 0) {
|
|
14675
|
+
collected.charts.flakinessTable = rows;
|
|
14676
|
+
if (quarantineTotal > 0) {
|
|
14677
|
+
collected.charts.flakinessQuarantineTotal = quarantineTotal;
|
|
14678
|
+
collected.charts.flakinessQuarantineThreshold = options.quarantineThreshold;
|
|
14679
|
+
}
|
|
14482
14680
|
}
|
|
14483
14681
|
}
|
|
14484
14682
|
}
|
|
@@ -14505,7 +14703,8 @@ function buildReportData(collected, licenseInfo, hooks) {
|
|
|
14505
14703
|
failures,
|
|
14506
14704
|
environment,
|
|
14507
14705
|
charts,
|
|
14508
|
-
runDiff
|
|
14706
|
+
runDiff,
|
|
14707
|
+
browserMatrix: buildBrowserMatrix(projects)
|
|
14509
14708
|
};
|
|
14510
14709
|
}
|
|
14511
14710
|
function resolveProjectNameFromPkg(cwd) {
|
|
@@ -14614,9 +14813,10 @@ function buildFailedTestsFields(projects, redactor) {
|
|
|
14614
14813
|
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
14615
14814
|
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
14616
14815
|
}
|
|
14617
|
-
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
14816
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch, currentFlaky) {
|
|
14618
14817
|
return computeRunDiff({
|
|
14619
14818
|
currentFailing,
|
|
14819
|
+
currentFlaky,
|
|
14620
14820
|
currentStatuses: collectCurrentStatuses(projects),
|
|
14621
14821
|
priorEntries,
|
|
14622
14822
|
branch
|
|
@@ -14980,7 +15180,7 @@ var MAX_ERR_SNIPPET = 4096;
|
|
|
14980
15180
|
var isHook = (c) => c === "hook" || c === "fixture";
|
|
14981
15181
|
function flatten2(steps) {
|
|
14982
15182
|
const out = [];
|
|
14983
|
-
const
|
|
15183
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
14984
15184
|
for (const s of nodes ?? []) {
|
|
14985
15185
|
const index = out.length;
|
|
14986
15186
|
out.push({
|
|
@@ -14990,10 +15190,10 @@ function flatten2(steps) {
|
|
|
14990
15190
|
hasError: s.error != null,
|
|
14991
15191
|
ancestors
|
|
14992
15192
|
});
|
|
14993
|
-
if (s.steps?.length)
|
|
15193
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
14994
15194
|
}
|
|
14995
15195
|
};
|
|
14996
|
-
|
|
15196
|
+
walk2(steps ?? [], 0, []);
|
|
14997
15197
|
return out;
|
|
14998
15198
|
}
|
|
14999
15199
|
var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
|
|
@@ -15136,9 +15336,9 @@ var MAX_FILE_CHARS = 300;
|
|
|
15136
15336
|
var MAX_PROJECT_CHARS = 100;
|
|
15137
15337
|
var MAX_SUITE_SEGMENTS = 10;
|
|
15138
15338
|
function suiteIdentity(test) {
|
|
15139
|
-
const
|
|
15140
|
-
if (!Array.isArray(
|
|
15141
|
-
const [, project, file, ...rest] =
|
|
15339
|
+
const path16 = typeof test.titlePath === "function" ? test.titlePath() : [];
|
|
15340
|
+
if (!Array.isArray(path16) || path16.length < 4) return {};
|
|
15341
|
+
const [, project, file, ...rest] = path16;
|
|
15142
15342
|
const describes = rest.slice(0, -1);
|
|
15143
15343
|
const out = {};
|
|
15144
15344
|
if (typeof file === "string" && file) {
|
|
@@ -15257,7 +15457,7 @@ var PdfReporter = class {
|
|
|
15257
15457
|
if (this.options.redact.enabled) {
|
|
15258
15458
|
this.redactor = new Redactor(this.options.redact);
|
|
15259
15459
|
}
|
|
15260
|
-
this.version = true ? "0.
|
|
15460
|
+
this.version = true ? "0.30.0" : "0.x";
|
|
15261
15461
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15262
15462
|
this.licenseClient = new LicenseClient({
|
|
15263
15463
|
licenseKey: this.options.licenseKey,
|