@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.mjs
CHANGED
|
@@ -1229,13 +1229,13 @@ var require_ast = __commonJS({
|
|
|
1229
1229
|
helperExpression: function helperExpression(node) {
|
|
1230
1230
|
return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
|
|
1231
1231
|
},
|
|
1232
|
-
scopedId: function scopedId(
|
|
1233
|
-
return /^\.|this\b/.test(
|
|
1232
|
+
scopedId: function scopedId(path17) {
|
|
1233
|
+
return /^\.|this\b/.test(path17.original);
|
|
1234
1234
|
},
|
|
1235
1235
|
// an ID is simple if it only has one part, and that part is not
|
|
1236
1236
|
// `..` or `this`.
|
|
1237
|
-
simpleId: function simpleId(
|
|
1238
|
-
return
|
|
1237
|
+
simpleId: function simpleId(path17) {
|
|
1238
|
+
return path17.parts.length === 1 && !AST.helpers.scopedId(path17) && !path17.depth;
|
|
1239
1239
|
}
|
|
1240
1240
|
}
|
|
1241
1241
|
};
|
|
@@ -2309,12 +2309,12 @@ var require_helpers2 = __commonJS({
|
|
|
2309
2309
|
loc
|
|
2310
2310
|
};
|
|
2311
2311
|
}
|
|
2312
|
-
function prepareMustache(
|
|
2312
|
+
function prepareMustache(path17, params, hash, open, strip, locInfo) {
|
|
2313
2313
|
var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
|
|
2314
2314
|
var decorator = /\*/.test(open);
|
|
2315
2315
|
return {
|
|
2316
2316
|
type: decorator ? "Decorator" : "MustacheStatement",
|
|
2317
|
-
path:
|
|
2317
|
+
path: path17,
|
|
2318
2318
|
params,
|
|
2319
2319
|
hash,
|
|
2320
2320
|
escaped,
|
|
@@ -2634,9 +2634,9 @@ var require_compiler = __commonJS({
|
|
|
2634
2634
|
},
|
|
2635
2635
|
DecoratorBlock: function DecoratorBlock(decorator) {
|
|
2636
2636
|
var program = decorator.program && this.compileProgram(decorator.program);
|
|
2637
|
-
var params = this.setupFullMustacheParams(decorator, program, void 0),
|
|
2637
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path17 = decorator.path;
|
|
2638
2638
|
this.useDecorators = true;
|
|
2639
|
-
this.opcode("registerDecorator", params.length,
|
|
2639
|
+
this.opcode("registerDecorator", params.length, path17.original);
|
|
2640
2640
|
},
|
|
2641
2641
|
PartialStatement: function PartialStatement(partial) {
|
|
2642
2642
|
this.usePartial = true;
|
|
@@ -2700,46 +2700,46 @@ var require_compiler = __commonJS({
|
|
|
2700
2700
|
}
|
|
2701
2701
|
},
|
|
2702
2702
|
ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
|
|
2703
|
-
var
|
|
2704
|
-
this.opcode("getContext",
|
|
2703
|
+
var path17 = sexpr.path, name = path17.parts[0], isBlock = program != null || inverse != null;
|
|
2704
|
+
this.opcode("getContext", path17.depth);
|
|
2705
2705
|
this.opcode("pushProgram", program);
|
|
2706
2706
|
this.opcode("pushProgram", inverse);
|
|
2707
|
-
|
|
2708
|
-
this.accept(
|
|
2707
|
+
path17.strict = true;
|
|
2708
|
+
this.accept(path17);
|
|
2709
2709
|
this.opcode("invokeAmbiguous", name, isBlock);
|
|
2710
2710
|
},
|
|
2711
2711
|
simpleSexpr: function simpleSexpr(sexpr) {
|
|
2712
|
-
var
|
|
2713
|
-
|
|
2714
|
-
this.accept(
|
|
2712
|
+
var path17 = sexpr.path;
|
|
2713
|
+
path17.strict = true;
|
|
2714
|
+
this.accept(path17);
|
|
2715
2715
|
this.opcode("resolvePossibleLambda");
|
|
2716
2716
|
},
|
|
2717
2717
|
helperSexpr: function helperSexpr(sexpr, program, inverse) {
|
|
2718
|
-
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
|
2718
|
+
var params = this.setupFullMustacheParams(sexpr, program, inverse), path17 = sexpr.path, name = path17.parts[0];
|
|
2719
2719
|
if (this.options.knownHelpers[name]) {
|
|
2720
2720
|
this.opcode("invokeKnownHelper", params.length, name);
|
|
2721
2721
|
} else if (this.options.knownHelpersOnly) {
|
|
2722
2722
|
throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
|
2723
2723
|
} else {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
this.accept(
|
|
2727
|
-
this.opcode("invokeHelper", params.length,
|
|
2724
|
+
path17.strict = true;
|
|
2725
|
+
path17.falsy = true;
|
|
2726
|
+
this.accept(path17);
|
|
2727
|
+
this.opcode("invokeHelper", params.length, path17.original, _ast2["default"].helpers.simpleId(path17));
|
|
2728
2728
|
}
|
|
2729
2729
|
},
|
|
2730
|
-
PathExpression: function PathExpression(
|
|
2731
|
-
this.addDepth(
|
|
2732
|
-
this.opcode("getContext",
|
|
2733
|
-
var name =
|
|
2730
|
+
PathExpression: function PathExpression(path17) {
|
|
2731
|
+
this.addDepth(path17.depth);
|
|
2732
|
+
this.opcode("getContext", path17.depth);
|
|
2733
|
+
var name = path17.parts[0], scoped = _ast2["default"].helpers.scopedId(path17), blockParamId = !path17.depth && !scoped && this.blockParamIndex(name);
|
|
2734
2734
|
if (blockParamId) {
|
|
2735
|
-
this.opcode("lookupBlockParam", blockParamId,
|
|
2735
|
+
this.opcode("lookupBlockParam", blockParamId, path17.parts);
|
|
2736
2736
|
} else if (!name) {
|
|
2737
2737
|
this.opcode("pushContext");
|
|
2738
|
-
} else if (
|
|
2738
|
+
} else if (path17.data) {
|
|
2739
2739
|
this.options.data = true;
|
|
2740
|
-
this.opcode("lookupData",
|
|
2740
|
+
this.opcode("lookupData", path17.depth, path17.parts, path17.strict);
|
|
2741
2741
|
} else {
|
|
2742
|
-
this.opcode("lookupOnContext",
|
|
2742
|
+
this.opcode("lookupOnContext", path17.parts, path17.falsy, path17.strict, scoped);
|
|
2743
2743
|
}
|
|
2744
2744
|
},
|
|
2745
2745
|
StringLiteral: function StringLiteral(string) {
|
|
@@ -3095,16 +3095,16 @@ var require_util = __commonJS({
|
|
|
3095
3095
|
}
|
|
3096
3096
|
exports.urlGenerate = urlGenerate;
|
|
3097
3097
|
function normalize(aPath) {
|
|
3098
|
-
var
|
|
3098
|
+
var path17 = aPath;
|
|
3099
3099
|
var url = urlParse(aPath);
|
|
3100
3100
|
if (url) {
|
|
3101
3101
|
if (!url.path) {
|
|
3102
3102
|
return aPath;
|
|
3103
3103
|
}
|
|
3104
|
-
|
|
3104
|
+
path17 = url.path;
|
|
3105
3105
|
}
|
|
3106
|
-
var isAbsolute = exports.isAbsolute(
|
|
3107
|
-
var parts =
|
|
3106
|
+
var isAbsolute = exports.isAbsolute(path17);
|
|
3107
|
+
var parts = path17.split(/\/+/);
|
|
3108
3108
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
3109
3109
|
part = parts[i];
|
|
3110
3110
|
if (part === ".") {
|
|
@@ -3121,15 +3121,15 @@ var require_util = __commonJS({
|
|
|
3121
3121
|
}
|
|
3122
3122
|
}
|
|
3123
3123
|
}
|
|
3124
|
-
|
|
3125
|
-
if (
|
|
3126
|
-
|
|
3124
|
+
path17 = parts.join("/");
|
|
3125
|
+
if (path17 === "") {
|
|
3126
|
+
path17 = isAbsolute ? "/" : ".";
|
|
3127
3127
|
}
|
|
3128
3128
|
if (url) {
|
|
3129
|
-
url.path =
|
|
3129
|
+
url.path = path17;
|
|
3130
3130
|
return urlGenerate(url);
|
|
3131
3131
|
}
|
|
3132
|
-
return
|
|
3132
|
+
return path17;
|
|
3133
3133
|
}
|
|
3134
3134
|
exports.normalize = normalize;
|
|
3135
3135
|
function join(aRoot, aPath) {
|
|
@@ -5932,8 +5932,8 @@ var require_printer = __commonJS({
|
|
|
5932
5932
|
return this.accept(sexpr.path) + " " + params + hash;
|
|
5933
5933
|
};
|
|
5934
5934
|
PrintVisitor.prototype.PathExpression = function(id) {
|
|
5935
|
-
var
|
|
5936
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
5935
|
+
var path17 = id.parts.join("/");
|
|
5936
|
+
return (id.data ? "@" : "") + "PATH:" + path17;
|
|
5937
5937
|
};
|
|
5938
5938
|
PrintVisitor.prototype.StringLiteral = function(string) {
|
|
5939
5939
|
return '"' + string.value + '"';
|
|
@@ -6053,7 +6053,7 @@ var require_win32 = __commonJS({
|
|
|
6053
6053
|
"node_modules/chrome-finder/lib/win32.js"(exports, module) {
|
|
6054
6054
|
"use strict";
|
|
6055
6055
|
init_esm_shims();
|
|
6056
|
-
var
|
|
6056
|
+
var path17 = __require("path");
|
|
6057
6057
|
var { canAccess } = require_util2();
|
|
6058
6058
|
function win32() {
|
|
6059
6059
|
const installations = [];
|
|
@@ -6067,7 +6067,7 @@ var require_win32 = __commonJS({
|
|
|
6067
6067
|
const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
|
|
6068
6068
|
prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
|
|
6069
6069
|
if (prefix) {
|
|
6070
|
-
const chromePath =
|
|
6070
|
+
const chromePath = path17.join(prefix, suffix);
|
|
6071
6071
|
if (canAccess(chromePath)) {
|
|
6072
6072
|
installations.push(chromePath);
|
|
6073
6073
|
}
|
|
@@ -6085,7 +6085,7 @@ var require_darwin = __commonJS({
|
|
|
6085
6085
|
"use strict";
|
|
6086
6086
|
init_esm_shims();
|
|
6087
6087
|
var { execSync: execSync3 } = __require("child_process");
|
|
6088
|
-
var
|
|
6088
|
+
var path17 = __require("path");
|
|
6089
6089
|
var { canAccess, newLineRegex, sort } = require_util2();
|
|
6090
6090
|
function darwin() {
|
|
6091
6091
|
const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
|
|
@@ -6095,7 +6095,7 @@ var require_darwin = __commonJS({
|
|
|
6095
6095
|
${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
|
|
6096
6096
|
`).toString().split(newLineRegex).forEach((inst) => {
|
|
6097
6097
|
suffixes.forEach((suffix) => {
|
|
6098
|
-
const execPath =
|
|
6098
|
+
const execPath = path17.join(inst.trim(), suffix);
|
|
6099
6099
|
if (canAccess(execPath)) {
|
|
6100
6100
|
installations.push(execPath);
|
|
6101
6101
|
}
|
|
@@ -6124,7 +6124,7 @@ var require_linux = __commonJS({
|
|
|
6124
6124
|
"use strict";
|
|
6125
6125
|
init_esm_shims();
|
|
6126
6126
|
var { execSync: execSync3, execFileSync } = __require("child_process");
|
|
6127
|
-
var
|
|
6127
|
+
var path17 = __require("path");
|
|
6128
6128
|
var fs16 = __require("fs");
|
|
6129
6129
|
var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
|
|
6130
6130
|
function findChromeExecutablesForLinuxDesktop(folder) {
|
|
@@ -6142,7 +6142,7 @@ var require_linux = __commonJS({
|
|
|
6142
6142
|
function linux() {
|
|
6143
6143
|
let installations = [];
|
|
6144
6144
|
const desktopInstallationFolders = [
|
|
6145
|
-
|
|
6145
|
+
path17.join(__require("os").homedir(), ".local/share/applications/"),
|
|
6146
6146
|
"/usr/share/applications/"
|
|
6147
6147
|
];
|
|
6148
6148
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -6750,8 +6750,8 @@ function getErrorMap() {
|
|
|
6750
6750
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
6751
6751
|
init_esm_shims();
|
|
6752
6752
|
var makeIssue = (params) => {
|
|
6753
|
-
const { data, path:
|
|
6754
|
-
const fullPath = [...
|
|
6753
|
+
const { data, path: path17, errorMaps, issueData } = params;
|
|
6754
|
+
const fullPath = [...path17, ...issueData.path || []];
|
|
6755
6755
|
const fullIssue = {
|
|
6756
6756
|
...issueData,
|
|
6757
6757
|
path: fullPath
|
|
@@ -6871,11 +6871,11 @@ var errorUtil;
|
|
|
6871
6871
|
|
|
6872
6872
|
// node_modules/zod/v3/types.js
|
|
6873
6873
|
var ParseInputLazyPath = class {
|
|
6874
|
-
constructor(parent, value,
|
|
6874
|
+
constructor(parent, value, path17, key) {
|
|
6875
6875
|
this._cachedPath = [];
|
|
6876
6876
|
this.parent = parent;
|
|
6877
6877
|
this.data = value;
|
|
6878
|
-
this._path =
|
|
6878
|
+
this._path = path17;
|
|
6879
6879
|
this._key = key;
|
|
6880
6880
|
}
|
|
6881
6881
|
get path() {
|
|
@@ -10331,6 +10331,7 @@ var DEFAULT_OPTIONS = {
|
|
|
10331
10331
|
historySize: 10,
|
|
10332
10332
|
showTrend: true,
|
|
10333
10333
|
flakinessTopN: 5,
|
|
10334
|
+
quarantineThreshold: 40,
|
|
10334
10335
|
slowTestThreshold: 10,
|
|
10335
10336
|
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
10336
10337
|
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
@@ -10356,6 +10357,7 @@ var SECTION_KEYS = [
|
|
|
10356
10357
|
"defectLog",
|
|
10357
10358
|
"briefBand",
|
|
10358
10359
|
"runDiff",
|
|
10360
|
+
"browserMatrix",
|
|
10359
10361
|
// display modifiers
|
|
10360
10362
|
"passRate",
|
|
10361
10363
|
"fullEnvironment",
|
|
@@ -10380,6 +10382,7 @@ var SECTION_DEFAULTS = {
|
|
|
10380
10382
|
defectLog: false,
|
|
10381
10383
|
briefBand: false,
|
|
10382
10384
|
runDiff: true,
|
|
10385
|
+
browserMatrix: false,
|
|
10383
10386
|
passRate: true,
|
|
10384
10387
|
fullEnvironment: false,
|
|
10385
10388
|
retries: true,
|
|
@@ -10402,6 +10405,7 @@ var SECTION_DEFAULTS = {
|
|
|
10402
10405
|
defectLog: true,
|
|
10403
10406
|
briefBand: false,
|
|
10404
10407
|
runDiff: true,
|
|
10408
|
+
browserMatrix: true,
|
|
10405
10409
|
passRate: true,
|
|
10406
10410
|
fullEnvironment: true,
|
|
10407
10411
|
retries: true,
|
|
@@ -10429,6 +10433,7 @@ var SECTION_DEFAULTS = {
|
|
|
10429
10433
|
defectLog: false,
|
|
10430
10434
|
briefBand: true,
|
|
10431
10435
|
runDiff: true,
|
|
10436
|
+
browserMatrix: false,
|
|
10432
10437
|
passRate: true,
|
|
10433
10438
|
fullEnvironment: false,
|
|
10434
10439
|
retries: false,
|
|
@@ -10581,6 +10586,10 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10581
10586
|
// local history file is wiped every run. Aggregate numbers only.
|
|
10582
10587
|
remoteHistory: external_exports.boolean().default(false),
|
|
10583
10588
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10589
|
+
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),
|
|
10590
|
+
evidenceUrlTemplate: external_exports.string().refine((s) => s.includes("{path}"), {
|
|
10591
|
+
message: "evidenceUrlTemplate must contain the {path} token"
|
|
10592
|
+
}).optional(),
|
|
10584
10593
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10585
10594
|
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10586
10595
|
try {
|
|
@@ -11297,7 +11306,7 @@ var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
|
|
|
11297
11306
|
var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
|
|
11298
11307
|
function flatten(steps) {
|
|
11299
11308
|
const out = [];
|
|
11300
|
-
const
|
|
11309
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
11301
11310
|
for (const s of nodes ?? []) {
|
|
11302
11311
|
const index = out.length;
|
|
11303
11312
|
out.push({
|
|
@@ -11307,10 +11316,10 @@ function flatten(steps) {
|
|
|
11307
11316
|
hasError: s.error != null,
|
|
11308
11317
|
ancestors
|
|
11309
11318
|
});
|
|
11310
|
-
if (s.steps?.length)
|
|
11319
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
11311
11320
|
}
|
|
11312
11321
|
};
|
|
11313
|
-
|
|
11322
|
+
walk2(steps ?? [], 0, []);
|
|
11314
11323
|
return out;
|
|
11315
11324
|
}
|
|
11316
11325
|
function materialize(n, isFailing, depth) {
|
|
@@ -11918,7 +11927,7 @@ var ShardMerger = class {
|
|
|
11918
11927
|
merge(filePaths) {
|
|
11919
11928
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11920
11929
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11921
|
-
const reports = this.loadReports(resolved);
|
|
11930
|
+
const reports = this.loadReports(resolved).map((r) => this.normalizeReport(r));
|
|
11922
11931
|
const projects = this.mergeProjects(reports);
|
|
11923
11932
|
const stats = this.mergeStats(reports, projects);
|
|
11924
11933
|
const failures = sortBySeverity(this.extractFailures(reports));
|
|
@@ -11956,6 +11965,49 @@ var ShardMerger = class {
|
|
|
11956
11965
|
return report;
|
|
11957
11966
|
});
|
|
11958
11967
|
}
|
|
11968
|
+
/**
|
|
11969
|
+
* Two top-level shapes reach the merger. Our language adapters (Python,
|
|
11970
|
+
* .NET) and the engine fixtures emit PROJECT wrapper suites (no `file`,
|
|
11971
|
+
* children are the file suites). Playwright's own --reporter=json emits
|
|
11972
|
+
* FILE suites at top level and hangs the project on each
|
|
11973
|
+
* spec.tests[].projectName instead. Everything downstream (mergeProjects,
|
|
11974
|
+
* extractFailures) assumes the wrapper shape, so real Playwright reports
|
|
11975
|
+
* are pivoted into it here: one wrapper per project, file trees filtered
|
|
11976
|
+
* to that project's tests. An unnamed project ('') renders as 'default',
|
|
11977
|
+
* matching the live reporter path.
|
|
11978
|
+
*/
|
|
11979
|
+
normalizeReport(report) {
|
|
11980
|
+
if (report.suites.every((s) => s.file === void 0)) return report;
|
|
11981
|
+
const wrappers = this.collectProjectNames(report).map((name) => ({
|
|
11982
|
+
title: name === "" ? "default" : name,
|
|
11983
|
+
specs: [],
|
|
11984
|
+
suites: report.suites.map((fileSuite) => this.filterSuiteForProject(fileSuite, name)).filter((s) => s !== null)
|
|
11985
|
+
})).filter((w) => w.suites.length > 0);
|
|
11986
|
+
return { ...report, suites: wrappers };
|
|
11987
|
+
}
|
|
11988
|
+
/** Config order first, then any project discovered only on the tests. */
|
|
11989
|
+
collectProjectNames(report) {
|
|
11990
|
+
const names = [];
|
|
11991
|
+
const push = (n) => {
|
|
11992
|
+
if (!names.includes(n)) names.push(n);
|
|
11993
|
+
};
|
|
11994
|
+
for (const p of report.config?.projects ?? []) push(p.name ?? "");
|
|
11995
|
+
const walk2 = (suite) => {
|
|
11996
|
+
for (const spec of suite.specs ?? []) for (const t of spec.tests ?? []) push(t.projectName ?? "");
|
|
11997
|
+
for (const child of suite.suites ?? []) walk2(child);
|
|
11998
|
+
};
|
|
11999
|
+
for (const s of report.suites) walk2(s);
|
|
12000
|
+
return names.length > 0 ? names : [""];
|
|
12001
|
+
}
|
|
12002
|
+
filterSuiteForProject(suite, project) {
|
|
12003
|
+
const specs = (suite.specs ?? []).map((spec) => ({
|
|
12004
|
+
...spec,
|
|
12005
|
+
tests: (spec.tests ?? []).filter((t) => (t.projectName ?? "") === project)
|
|
12006
|
+
})).filter((spec) => spec.tests.length > 0);
|
|
12007
|
+
const children = (suite.suites ?? []).map((child) => this.filterSuiteForProject(child, project)).filter((c) => c !== null);
|
|
12008
|
+
if (specs.length === 0 && children.length === 0) return null;
|
|
12009
|
+
return { ...suite, specs, suites: children };
|
|
12010
|
+
}
|
|
11959
12011
|
mergeProjects(reports) {
|
|
11960
12012
|
const projectMap = /* @__PURE__ */ new Map();
|
|
11961
12013
|
for (const report of reports) {
|
|
@@ -13327,7 +13379,7 @@ var Redactor = class {
|
|
|
13327
13379
|
|
|
13328
13380
|
// src/pipeline/orchestrator.ts
|
|
13329
13381
|
init_esm_shims();
|
|
13330
|
-
import
|
|
13382
|
+
import path16 from "path";
|
|
13331
13383
|
|
|
13332
13384
|
// src/license/update-notice.ts
|
|
13333
13385
|
init_esm_shims();
|
|
@@ -13736,6 +13788,118 @@ function sourceTally(classified) {
|
|
|
13736
13788
|
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
13737
13789
|
}
|
|
13738
13790
|
|
|
13791
|
+
// src/collector/BrowserMatrix.ts
|
|
13792
|
+
init_esm_shims();
|
|
13793
|
+
var DEFAULT_ROW_CAP = 40;
|
|
13794
|
+
function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
13795
|
+
if (projects.length < 2) return void 0;
|
|
13796
|
+
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13797
|
+
const byTest = /* @__PURE__ */ new Map();
|
|
13798
|
+
projects.forEach((project, column) => {
|
|
13799
|
+
walk(project.suites, [], (path17, test) => {
|
|
13800
|
+
const key = [...path17, test.title].join(" > ");
|
|
13801
|
+
let statuses = byTest.get(key);
|
|
13802
|
+
if (!statuses) {
|
|
13803
|
+
statuses = projectNames.map(() => null);
|
|
13804
|
+
byTest.set(key, statuses);
|
|
13805
|
+
}
|
|
13806
|
+
statuses[column] = test.status;
|
|
13807
|
+
});
|
|
13808
|
+
});
|
|
13809
|
+
const divergent = [];
|
|
13810
|
+
let identicalCount = 0;
|
|
13811
|
+
const contributing = /* @__PURE__ */ new Set();
|
|
13812
|
+
for (const [title, statuses] of byTest) {
|
|
13813
|
+
const present = statuses.filter((s) => s !== null);
|
|
13814
|
+
if (present.length < 2) continue;
|
|
13815
|
+
statuses.forEach((s, i) => {
|
|
13816
|
+
if (s !== null) contributing.add(i);
|
|
13817
|
+
});
|
|
13818
|
+
if (present.every((s) => s === present[0])) {
|
|
13819
|
+
identicalCount++;
|
|
13820
|
+
} else {
|
|
13821
|
+
divergent.push({ title, statuses });
|
|
13822
|
+
}
|
|
13823
|
+
}
|
|
13824
|
+
if (divergent.length === 0 && identicalCount === 0) return void 0;
|
|
13825
|
+
if (contributing.size < projectNames.length) {
|
|
13826
|
+
const keep = [...contributing].sort((a, b) => a - b);
|
|
13827
|
+
projectNames = keep.map((i) => projectNames[i]);
|
|
13828
|
+
for (const row of divergent) row.statuses = keep.map((i) => row.statuses[i]);
|
|
13829
|
+
}
|
|
13830
|
+
divergent.sort((a, b) => severityRank(b) - severityRank(a) || a.title.localeCompare(b.title));
|
|
13831
|
+
return {
|
|
13832
|
+
projects: projectNames,
|
|
13833
|
+
rows: divergent.slice(0, cap),
|
|
13834
|
+
divergentCount: divergent.length,
|
|
13835
|
+
identicalCount,
|
|
13836
|
+
truncated: divergent.length > cap,
|
|
13837
|
+
hasFailures: divergent.some((r) => severityRank(r) > 0)
|
|
13838
|
+
};
|
|
13839
|
+
}
|
|
13840
|
+
function severityRank(row) {
|
|
13841
|
+
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13842
|
+
}
|
|
13843
|
+
function walk(suites, path17, visit) {
|
|
13844
|
+
for (const suite of suites) {
|
|
13845
|
+
const next = [...path17, suite.title];
|
|
13846
|
+
for (const test of suite.tests) visit(next, test);
|
|
13847
|
+
walk(suite.suites, next, visit);
|
|
13848
|
+
}
|
|
13849
|
+
}
|
|
13850
|
+
|
|
13851
|
+
// src/utils/evidence-links.ts
|
|
13852
|
+
init_esm_shims();
|
|
13853
|
+
import path14 from "path";
|
|
13854
|
+
import qrcodeFactory from "qrcode-generator";
|
|
13855
|
+
function applyEvidenceUrls(data, template, cwd) {
|
|
13856
|
+
let warned = false;
|
|
13857
|
+
const warnOnce = (err) => {
|
|
13858
|
+
if (warned) return;
|
|
13859
|
+
warned = true;
|
|
13860
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13861
|
+
logger.warn(`evidenceUrlTemplate: link generation failed for at least one artifact \u2014 ${message}`);
|
|
13862
|
+
};
|
|
13863
|
+
for (const failure of data.failures) {
|
|
13864
|
+
const evidence = failure.execution?.evidence;
|
|
13865
|
+
if (!evidence) continue;
|
|
13866
|
+
if (evidence.tracePath) {
|
|
13867
|
+
try {
|
|
13868
|
+
evidence.traceUrl = expandTemplate(template, relativizeArtifact(evidence.tracePath, cwd));
|
|
13869
|
+
evidence.traceQr = qrDataUri(evidence.traceUrl);
|
|
13870
|
+
} catch (err) {
|
|
13871
|
+
warnOnce(err);
|
|
13872
|
+
}
|
|
13873
|
+
}
|
|
13874
|
+
if (evidence.videoPath) {
|
|
13875
|
+
try {
|
|
13876
|
+
evidence.videoUrl = expandTemplate(template, relativizeArtifact(evidence.videoPath, cwd));
|
|
13877
|
+
evidence.videoQr = qrDataUri(evidence.videoUrl);
|
|
13878
|
+
} catch (err) {
|
|
13879
|
+
warnOnce(err);
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
}
|
|
13884
|
+
function relativizeArtifact(artifactPath, cwd) {
|
|
13885
|
+
if (!path14.isAbsolute(artifactPath)) return artifactPath;
|
|
13886
|
+
const rel = path14.relative(cwd, artifactPath);
|
|
13887
|
+
if (rel === ".." || rel.startsWith(`..${path14.sep}`) || path14.isAbsolute(rel)) return artifactPath;
|
|
13888
|
+
return rel;
|
|
13889
|
+
}
|
|
13890
|
+
function expandTemplate(template, artifactPath) {
|
|
13891
|
+
const normalized = artifactPath.replace(/\\/g, "/");
|
|
13892
|
+
const encoded = normalized.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
13893
|
+
return template.split("{path}").join(encoded);
|
|
13894
|
+
}
|
|
13895
|
+
function qrDataUri(url) {
|
|
13896
|
+
const qr = qrcodeFactory(0, "M");
|
|
13897
|
+
qr.addData(url);
|
|
13898
|
+
qr.make();
|
|
13899
|
+
const svg = qr.createSvgTag({ cellSize: 2, margin: 2, scalable: true });
|
|
13900
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
13901
|
+
}
|
|
13902
|
+
|
|
13739
13903
|
// src/redact/redact-report.ts
|
|
13740
13904
|
init_esm_shims();
|
|
13741
13905
|
function redactReportData(data, r) {
|
|
@@ -13766,6 +13930,10 @@ function redactReportData(data, r) {
|
|
|
13766
13930
|
data.briefSentence = rd(data.briefSentence);
|
|
13767
13931
|
data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
|
|
13768
13932
|
data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
|
|
13933
|
+
if (data.browserMatrix) {
|
|
13934
|
+
data.browserMatrix.projects = data.browserMatrix.projects.map((p) => r.redactText(p));
|
|
13935
|
+
data.browserMatrix.rows.forEach((row) => row.title = r.redactText(row.title));
|
|
13936
|
+
}
|
|
13769
13937
|
data.environment.buildUrl = r.redactText(data.environment.buildUrl);
|
|
13770
13938
|
}
|
|
13771
13939
|
function redactStep(s, r) {
|
|
@@ -13788,7 +13956,7 @@ function redactSuite(s, r) {
|
|
|
13788
13956
|
// src/history/HistoryManager.ts
|
|
13789
13957
|
init_esm_shims();
|
|
13790
13958
|
import fs15 from "fs";
|
|
13791
|
-
import
|
|
13959
|
+
import path15 from "path";
|
|
13792
13960
|
var HistoryManager = class {
|
|
13793
13961
|
constructor(filePath) {
|
|
13794
13962
|
this.filePath = filePath;
|
|
@@ -13809,7 +13977,7 @@ var HistoryManager = class {
|
|
|
13809
13977
|
}
|
|
13810
13978
|
append(entry, maxSize) {
|
|
13811
13979
|
try {
|
|
13812
|
-
fs15.mkdirSync(
|
|
13980
|
+
fs15.mkdirSync(path15.dirname(this.filePath), { recursive: true });
|
|
13813
13981
|
const next = {
|
|
13814
13982
|
version: 1,
|
|
13815
13983
|
entries: [entry, ...this.read()].slice(0, maxSize)
|
|
@@ -13897,7 +14065,7 @@ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses
|
|
|
13897
14065
|
return { fixed, noLongerRun };
|
|
13898
14066
|
}
|
|
13899
14067
|
function computeRunDiff(args) {
|
|
13900
|
-
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
14068
|
+
const { currentFailing, currentFlaky = [], currentStatuses, priorEntries, branch } = args;
|
|
13901
14069
|
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
13902
14070
|
if (!baseline) return null;
|
|
13903
14071
|
const baselineFailed = /* @__PURE__ */ new Map();
|
|
@@ -13906,7 +14074,15 @@ function computeRunDiff(args) {
|
|
|
13906
14074
|
}
|
|
13907
14075
|
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
13908
14076
|
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
13909
|
-
|
|
14077
|
+
const baselineFlakyIds = new Set((baseline.flakyTests ?? []).map((t) => t.id));
|
|
14078
|
+
const newlyFlaky = [];
|
|
14079
|
+
const seenFlaky = /* @__PURE__ */ new Set();
|
|
14080
|
+
for (const item of currentFlaky) {
|
|
14081
|
+
if (seenFlaky.has(item.id) || baselineFlakyIds.has(item.id)) continue;
|
|
14082
|
+
seenFlaky.add(item.id);
|
|
14083
|
+
newlyFlaky.push(item);
|
|
14084
|
+
}
|
|
14085
|
+
for (const bucket of [newFailures, stillFailing, fixed, noLongerRun, newlyFlaky]) {
|
|
13910
14086
|
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
13911
14087
|
}
|
|
13912
14088
|
return {
|
|
@@ -13914,6 +14090,7 @@ function computeRunDiff(args) {
|
|
|
13914
14090
|
stillFailing,
|
|
13915
14091
|
fixed,
|
|
13916
14092
|
noLongerRun,
|
|
14093
|
+
newlyFlaky,
|
|
13917
14094
|
baselineRunId: baseline.runId,
|
|
13918
14095
|
baselineTimestamp: baseline.timestamp,
|
|
13919
14096
|
baselineTruncated: baseline.failedTestsTruncated === true
|
|
@@ -13922,10 +14099,11 @@ function computeRunDiff(args) {
|
|
|
13922
14099
|
|
|
13923
14100
|
// src/history/FlakinessAnalyser.ts
|
|
13924
14101
|
init_esm_shims();
|
|
13925
|
-
|
|
13926
|
-
|
|
14102
|
+
var QUARANTINE_MIN_RUNS = 3;
|
|
14103
|
+
function computeTopN(entries, topN, quarantineThreshold = 0) {
|
|
14104
|
+
if (topN === 0) return { rows: [], quarantineTotal: 0 };
|
|
13927
14105
|
const qualifying = entries.filter((e) => Array.isArray(e.flakyTests));
|
|
13928
|
-
if (qualifying.length === 0) return [];
|
|
14106
|
+
if (qualifying.length === 0) return { rows: [], quarantineTotal: 0 };
|
|
13929
14107
|
const map = /* @__PURE__ */ new Map();
|
|
13930
14108
|
qualifying.forEach((entry, idx) => {
|
|
13931
14109
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13943,17 +14121,26 @@ function computeTopN(entries, topN) {
|
|
|
13943
14121
|
const originalIdx = qualifying.length - 1 - reversedIdx;
|
|
13944
14122
|
return hits.has(originalIdx);
|
|
13945
14123
|
});
|
|
14124
|
+
let streak = 0;
|
|
14125
|
+
while (streak < totalRuns && hits.has(streak)) streak++;
|
|
14126
|
+
const flakeRate = Math.round(flakeCount / totalRuns * 100);
|
|
13946
14127
|
return {
|
|
13947
14128
|
title,
|
|
13948
|
-
flakeRate
|
|
14129
|
+
flakeRate,
|
|
13949
14130
|
flakeCount,
|
|
13950
14131
|
totalRuns,
|
|
13951
|
-
sparkline
|
|
14132
|
+
sparkline,
|
|
14133
|
+
streak,
|
|
14134
|
+
quarantine: quarantineThreshold > 0 && totalRuns >= QUARANTINE_MIN_RUNS && flakeRate >= quarantineThreshold
|
|
13952
14135
|
};
|
|
13953
14136
|
});
|
|
13954
|
-
|
|
13955
|
-
|
|
13956
|
-
|
|
14137
|
+
const quarantineTotal = rows.filter((r) => r.quarantine).length;
|
|
14138
|
+
return {
|
|
14139
|
+
rows: rows.sort(
|
|
14140
|
+
(a, b) => b.flakeRate - a.flakeRate || b.flakeCount - a.flakeCount || a.title.localeCompare(b.title)
|
|
14141
|
+
).slice(0, topN),
|
|
14142
|
+
quarantineTotal
|
|
14143
|
+
};
|
|
13957
14144
|
}
|
|
13958
14145
|
|
|
13959
14146
|
// src/notify/index.ts
|
|
@@ -14396,6 +14583,9 @@ async function runReportPipeline(hooks) {
|
|
|
14396
14583
|
}
|
|
14397
14584
|
}
|
|
14398
14585
|
const reportData = buildReportData(collected, licenseInfo, hooks);
|
|
14586
|
+
if (options.evidenceUrlTemplate) {
|
|
14587
|
+
applyEvidenceUrls(reportData, options.evidenceUrlTemplate, hooks.cwd);
|
|
14588
|
+
}
|
|
14399
14589
|
runFailureAnalysis(reportData, options, hooks.cwd);
|
|
14400
14590
|
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14401
14591
|
if (hooks.redactor) {
|
|
@@ -14444,7 +14634,7 @@ async function appendTrend(collected, licenseInfo, hooks) {
|
|
|
14444
14634
|
branch: collected.environment.branch
|
|
14445
14635
|
};
|
|
14446
14636
|
const localEntries = hm.append(entry, options.historySize);
|
|
14447
|
-
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
14637
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch, flakyTests);
|
|
14448
14638
|
let trendEntries = localEntries;
|
|
14449
14639
|
if (options.remoteHistory && licenseInfo.jwt) {
|
|
14450
14640
|
const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -14477,9 +14667,17 @@ function populateHistoryCharts(trendEntries, localEntries, collected, options) {
|
|
|
14477
14667
|
};
|
|
14478
14668
|
}
|
|
14479
14669
|
if (options.flakinessTopN > 0) {
|
|
14480
|
-
const
|
|
14481
|
-
|
|
14482
|
-
|
|
14670
|
+
const { rows, quarantineTotal } = computeTopN(
|
|
14671
|
+
localEntries,
|
|
14672
|
+
options.flakinessTopN,
|
|
14673
|
+
options.quarantineThreshold
|
|
14674
|
+
);
|
|
14675
|
+
if (rows.length > 0) {
|
|
14676
|
+
collected.charts.flakinessTable = rows;
|
|
14677
|
+
if (quarantineTotal > 0) {
|
|
14678
|
+
collected.charts.flakinessQuarantineTotal = quarantineTotal;
|
|
14679
|
+
collected.charts.flakinessQuarantineThreshold = options.quarantineThreshold;
|
|
14680
|
+
}
|
|
14483
14681
|
}
|
|
14484
14682
|
}
|
|
14485
14683
|
}
|
|
@@ -14506,12 +14704,13 @@ function buildReportData(collected, licenseInfo, hooks) {
|
|
|
14506
14704
|
failures,
|
|
14507
14705
|
environment,
|
|
14508
14706
|
charts,
|
|
14509
|
-
runDiff
|
|
14707
|
+
runDiff,
|
|
14708
|
+
browserMatrix: buildBrowserMatrix(projects)
|
|
14510
14709
|
};
|
|
14511
14710
|
}
|
|
14512
14711
|
function resolveProjectNameFromPkg(cwd) {
|
|
14513
14712
|
try {
|
|
14514
|
-
const pkgPath =
|
|
14713
|
+
const pkgPath = path16.resolve(cwd, "package.json");
|
|
14515
14714
|
const pkg = __require(pkgPath);
|
|
14516
14715
|
return pkg.name ?? "Playwright Tests";
|
|
14517
14716
|
} catch {
|
|
@@ -14560,9 +14759,9 @@ function deriveVerdict(stats) {
|
|
|
14560
14759
|
}
|
|
14561
14760
|
function resolveHistoryPath(options, cwd) {
|
|
14562
14761
|
if (options.historyFile) {
|
|
14563
|
-
return
|
|
14762
|
+
return path16.isAbsolute(options.historyFile) ? options.historyFile : path16.resolve(cwd, options.historyFile);
|
|
14564
14763
|
}
|
|
14565
|
-
return
|
|
14764
|
+
return path16.join(resolveProjectDir(options, cwd), "history.json");
|
|
14566
14765
|
}
|
|
14567
14766
|
function flattenSuiteNodes(suites) {
|
|
14568
14767
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|
|
@@ -14615,9 +14814,10 @@ function buildFailedTestsFields(projects, redactor) {
|
|
|
14615
14814
|
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
14616
14815
|
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
14617
14816
|
}
|
|
14618
|
-
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
14817
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch, currentFlaky) {
|
|
14619
14818
|
return computeRunDiff({
|
|
14620
14819
|
currentFailing,
|
|
14820
|
+
currentFlaky,
|
|
14621
14821
|
currentStatuses: collectCurrentStatuses(projects),
|
|
14622
14822
|
priorEntries,
|
|
14623
14823
|
branch
|
|
@@ -14981,7 +15181,7 @@ var MAX_ERR_SNIPPET = 4096;
|
|
|
14981
15181
|
var isHook = (c) => c === "hook" || c === "fixture";
|
|
14982
15182
|
function flatten2(steps) {
|
|
14983
15183
|
const out = [];
|
|
14984
|
-
const
|
|
15184
|
+
const walk2 = (nodes, depth, ancestors) => {
|
|
14985
15185
|
for (const s of nodes ?? []) {
|
|
14986
15186
|
const index = out.length;
|
|
14987
15187
|
out.push({
|
|
@@ -14991,10 +15191,10 @@ function flatten2(steps) {
|
|
|
14991
15191
|
hasError: s.error != null,
|
|
14992
15192
|
ancestors
|
|
14993
15193
|
});
|
|
14994
|
-
if (s.steps?.length)
|
|
15194
|
+
if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
|
|
14995
15195
|
}
|
|
14996
15196
|
};
|
|
14997
|
-
|
|
15197
|
+
walk2(steps ?? [], 0, []);
|
|
14998
15198
|
return out;
|
|
14999
15199
|
}
|
|
15000
15200
|
var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
|
|
@@ -15137,9 +15337,9 @@ var MAX_FILE_CHARS = 300;
|
|
|
15137
15337
|
var MAX_PROJECT_CHARS = 100;
|
|
15138
15338
|
var MAX_SUITE_SEGMENTS = 10;
|
|
15139
15339
|
function suiteIdentity(test) {
|
|
15140
|
-
const
|
|
15141
|
-
if (!Array.isArray(
|
|
15142
|
-
const [, project, file, ...rest] =
|
|
15340
|
+
const path17 = typeof test.titlePath === "function" ? test.titlePath() : [];
|
|
15341
|
+
if (!Array.isArray(path17) || path17.length < 4) return {};
|
|
15342
|
+
const [, project, file, ...rest] = path17;
|
|
15143
15343
|
const describes = rest.slice(0, -1);
|
|
15144
15344
|
const out = {};
|
|
15145
15345
|
if (typeof file === "string" && file) {
|
|
@@ -15258,7 +15458,7 @@ var PdfReporter = class {
|
|
|
15258
15458
|
if (this.options.redact.enabled) {
|
|
15259
15459
|
this.redactor = new Redactor(this.options.redact);
|
|
15260
15460
|
}
|
|
15261
|
-
this.version = true ? "0.
|
|
15461
|
+
this.version = true ? "0.30.0" : "0.x";
|
|
15262
15462
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15263
15463
|
this.licenseClient = new LicenseClient({
|
|
15264
15464
|
licenseKey: this.options.licenseKey,
|