@reportforge/playwright-pdf 0.29.0 → 0.31.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 +11 -5
- package/dist/cli/index.js +16 -2
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +160 -72
- package/dist/index.mjs +166 -78
- 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 +31 -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.
|
|
@@ -10585,6 +10586,10 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10585
10586
|
// local history file is wiped every run. Aggregate numbers only.
|
|
10586
10587
|
remoteHistory: external_exports.boolean().default(false),
|
|
10587
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(),
|
|
10588
10593
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10589
10594
|
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10590
10595
|
try {
|
|
@@ -13374,7 +13379,7 @@ var Redactor = class {
|
|
|
13374
13379
|
|
|
13375
13380
|
// src/pipeline/orchestrator.ts
|
|
13376
13381
|
init_esm_shims();
|
|
13377
|
-
import
|
|
13382
|
+
import path16 from "path";
|
|
13378
13383
|
|
|
13379
13384
|
// src/license/update-notice.ts
|
|
13380
13385
|
init_esm_shims();
|
|
@@ -13791,8 +13796,8 @@ function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
|
13791
13796
|
let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
|
|
13792
13797
|
const byTest = /* @__PURE__ */ new Map();
|
|
13793
13798
|
projects.forEach((project, column) => {
|
|
13794
|
-
walk(project.suites, [], (
|
|
13795
|
-
const key = [...
|
|
13799
|
+
walk(project.suites, [], (path17, test) => {
|
|
13800
|
+
const key = [...path17, test.title].join(" > ");
|
|
13796
13801
|
let statuses = byTest.get(key);
|
|
13797
13802
|
if (!statuses) {
|
|
13798
13803
|
statuses = projectNames.map(() => null);
|
|
@@ -13835,14 +13840,66 @@ function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
|
|
|
13835
13840
|
function severityRank(row) {
|
|
13836
13841
|
return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
|
|
13837
13842
|
}
|
|
13838
|
-
function walk(suites,
|
|
13843
|
+
function walk(suites, path17, visit) {
|
|
13839
13844
|
for (const suite of suites) {
|
|
13840
|
-
const next = [...
|
|
13845
|
+
const next = [...path17, suite.title];
|
|
13841
13846
|
for (const test of suite.tests) visit(next, test);
|
|
13842
13847
|
walk(suite.suites, next, visit);
|
|
13843
13848
|
}
|
|
13844
13849
|
}
|
|
13845
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
|
+
|
|
13846
13903
|
// src/redact/redact-report.ts
|
|
13847
13904
|
init_esm_shims();
|
|
13848
13905
|
function redactReportData(data, r) {
|
|
@@ -13899,7 +13956,7 @@ function redactSuite(s, r) {
|
|
|
13899
13956
|
// src/history/HistoryManager.ts
|
|
13900
13957
|
init_esm_shims();
|
|
13901
13958
|
import fs15 from "fs";
|
|
13902
|
-
import
|
|
13959
|
+
import path15 from "path";
|
|
13903
13960
|
var HistoryManager = class {
|
|
13904
13961
|
constructor(filePath) {
|
|
13905
13962
|
this.filePath = filePath;
|
|
@@ -13920,7 +13977,7 @@ var HistoryManager = class {
|
|
|
13920
13977
|
}
|
|
13921
13978
|
append(entry, maxSize) {
|
|
13922
13979
|
try {
|
|
13923
|
-
fs15.mkdirSync(
|
|
13980
|
+
fs15.mkdirSync(path15.dirname(this.filePath), { recursive: true });
|
|
13924
13981
|
const next = {
|
|
13925
13982
|
version: 1,
|
|
13926
13983
|
entries: [entry, ...this.read()].slice(0, maxSize)
|
|
@@ -14008,7 +14065,7 @@ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses
|
|
|
14008
14065
|
return { fixed, noLongerRun };
|
|
14009
14066
|
}
|
|
14010
14067
|
function computeRunDiff(args) {
|
|
14011
|
-
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
14068
|
+
const { currentFailing, currentFlaky = [], currentStatuses, priorEntries, branch } = args;
|
|
14012
14069
|
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
14013
14070
|
if (!baseline) return null;
|
|
14014
14071
|
const baselineFailed = /* @__PURE__ */ new Map();
|
|
@@ -14017,7 +14074,15 @@ function computeRunDiff(args) {
|
|
|
14017
14074
|
}
|
|
14018
14075
|
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
14019
14076
|
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
14020
|
-
|
|
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]) {
|
|
14021
14086
|
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
14022
14087
|
}
|
|
14023
14088
|
return {
|
|
@@ -14025,6 +14090,7 @@ function computeRunDiff(args) {
|
|
|
14025
14090
|
stillFailing,
|
|
14026
14091
|
fixed,
|
|
14027
14092
|
noLongerRun,
|
|
14093
|
+
newlyFlaky,
|
|
14028
14094
|
baselineRunId: baseline.runId,
|
|
14029
14095
|
baselineTimestamp: baseline.timestamp,
|
|
14030
14096
|
baselineTruncated: baseline.failedTestsTruncated === true
|
|
@@ -14033,10 +14099,11 @@ function computeRunDiff(args) {
|
|
|
14033
14099
|
|
|
14034
14100
|
// src/history/FlakinessAnalyser.ts
|
|
14035
14101
|
init_esm_shims();
|
|
14036
|
-
|
|
14037
|
-
|
|
14102
|
+
var QUARANTINE_MIN_RUNS = 3;
|
|
14103
|
+
function computeTopN(entries, topN, quarantineThreshold = 0) {
|
|
14104
|
+
if (topN === 0) return { rows: [], quarantineTotal: 0 };
|
|
14038
14105
|
const qualifying = entries.filter((e) => Array.isArray(e.flakyTests));
|
|
14039
|
-
if (qualifying.length === 0) return [];
|
|
14106
|
+
if (qualifying.length === 0) return { rows: [], quarantineTotal: 0 };
|
|
14040
14107
|
const map = /* @__PURE__ */ new Map();
|
|
14041
14108
|
qualifying.forEach((entry, idx) => {
|
|
14042
14109
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14054,17 +14121,26 @@ function computeTopN(entries, topN) {
|
|
|
14054
14121
|
const originalIdx = qualifying.length - 1 - reversedIdx;
|
|
14055
14122
|
return hits.has(originalIdx);
|
|
14056
14123
|
});
|
|
14124
|
+
let streak = 0;
|
|
14125
|
+
while (streak < totalRuns && hits.has(streak)) streak++;
|
|
14126
|
+
const flakeRate = Math.round(flakeCount / totalRuns * 100);
|
|
14057
14127
|
return {
|
|
14058
14128
|
title,
|
|
14059
|
-
flakeRate
|
|
14129
|
+
flakeRate,
|
|
14060
14130
|
flakeCount,
|
|
14061
14131
|
totalRuns,
|
|
14062
|
-
sparkline
|
|
14132
|
+
sparkline,
|
|
14133
|
+
streak,
|
|
14134
|
+
quarantine: quarantineThreshold > 0 && totalRuns >= QUARANTINE_MIN_RUNS && flakeRate >= quarantineThreshold
|
|
14063
14135
|
};
|
|
14064
14136
|
});
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
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
|
+
};
|
|
14068
14144
|
}
|
|
14069
14145
|
|
|
14070
14146
|
// src/notify/index.ts
|
|
@@ -14507,6 +14583,9 @@ async function runReportPipeline(hooks) {
|
|
|
14507
14583
|
}
|
|
14508
14584
|
}
|
|
14509
14585
|
const reportData = buildReportData(collected, licenseInfo, hooks);
|
|
14586
|
+
if (options.evidenceUrlTemplate) {
|
|
14587
|
+
applyEvidenceUrls(reportData, options.evidenceUrlTemplate, hooks.cwd);
|
|
14588
|
+
}
|
|
14510
14589
|
runFailureAnalysis(reportData, options, hooks.cwd);
|
|
14511
14590
|
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14512
14591
|
if (hooks.redactor) {
|
|
@@ -14555,7 +14634,7 @@ async function appendTrend(collected, licenseInfo, hooks) {
|
|
|
14555
14634
|
branch: collected.environment.branch
|
|
14556
14635
|
};
|
|
14557
14636
|
const localEntries = hm.append(entry, options.historySize);
|
|
14558
|
-
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);
|
|
14559
14638
|
let trendEntries = localEntries;
|
|
14560
14639
|
if (options.remoteHistory && licenseInfo.jwt) {
|
|
14561
14640
|
const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -14588,9 +14667,17 @@ function populateHistoryCharts(trendEntries, localEntries, collected, options) {
|
|
|
14588
14667
|
};
|
|
14589
14668
|
}
|
|
14590
14669
|
if (options.flakinessTopN > 0) {
|
|
14591
|
-
const
|
|
14592
|
-
|
|
14593
|
-
|
|
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
|
+
}
|
|
14594
14681
|
}
|
|
14595
14682
|
}
|
|
14596
14683
|
}
|
|
@@ -14623,7 +14710,7 @@ function buildReportData(collected, licenseInfo, hooks) {
|
|
|
14623
14710
|
}
|
|
14624
14711
|
function resolveProjectNameFromPkg(cwd) {
|
|
14625
14712
|
try {
|
|
14626
|
-
const pkgPath =
|
|
14713
|
+
const pkgPath = path16.resolve(cwd, "package.json");
|
|
14627
14714
|
const pkg = __require(pkgPath);
|
|
14628
14715
|
return pkg.name ?? "Playwright Tests";
|
|
14629
14716
|
} catch {
|
|
@@ -14672,9 +14759,9 @@ function deriveVerdict(stats) {
|
|
|
14672
14759
|
}
|
|
14673
14760
|
function resolveHistoryPath(options, cwd) {
|
|
14674
14761
|
if (options.historyFile) {
|
|
14675
|
-
return
|
|
14762
|
+
return path16.isAbsolute(options.historyFile) ? options.historyFile : path16.resolve(cwd, options.historyFile);
|
|
14676
14763
|
}
|
|
14677
|
-
return
|
|
14764
|
+
return path16.join(resolveProjectDir(options, cwd), "history.json");
|
|
14678
14765
|
}
|
|
14679
14766
|
function flattenSuiteNodes(suites) {
|
|
14680
14767
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|
|
@@ -14727,9 +14814,10 @@ function buildFailedTestsFields(projects, redactor) {
|
|
|
14727
14814
|
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
14728
14815
|
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
14729
14816
|
}
|
|
14730
|
-
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
14817
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch, currentFlaky) {
|
|
14731
14818
|
return computeRunDiff({
|
|
14732
14819
|
currentFailing,
|
|
14820
|
+
currentFlaky,
|
|
14733
14821
|
currentStatuses: collectCurrentStatuses(projects),
|
|
14734
14822
|
priorEntries,
|
|
14735
14823
|
branch
|
|
@@ -15249,9 +15337,9 @@ var MAX_FILE_CHARS = 300;
|
|
|
15249
15337
|
var MAX_PROJECT_CHARS = 100;
|
|
15250
15338
|
var MAX_SUITE_SEGMENTS = 10;
|
|
15251
15339
|
function suiteIdentity(test) {
|
|
15252
|
-
const
|
|
15253
|
-
if (!Array.isArray(
|
|
15254
|
-
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;
|
|
15255
15343
|
const describes = rest.slice(0, -1);
|
|
15256
15344
|
const out = {};
|
|
15257
15345
|
if (typeof file === "string" && file) {
|
|
@@ -15370,7 +15458,7 @@ var PdfReporter = class {
|
|
|
15370
15458
|
if (this.options.redact.enabled) {
|
|
15371
15459
|
this.redactor = new Redactor(this.options.redact);
|
|
15372
15460
|
}
|
|
15373
|
-
this.version = true ? "0.
|
|
15461
|
+
this.version = true ? "0.31.0" : "0.x";
|
|
15374
15462
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
15375
15463
|
this.licenseClient = new LicenseClient({
|
|
15376
15464
|
licenseKey: this.options.licenseKey,
|
|
@@ -13,21 +13,29 @@
|
|
|
13
13
|
<div class="flakiness-card__header">
|
|
14
14
|
<span class="flakiness-card__title">Top flaky tests — last {{charts.flakinessTable.[0].totalRuns}} runs</span>
|
|
15
15
|
</div>
|
|
16
|
+
{{#if charts.flakinessQuarantineTotal}}
|
|
17
|
+
<div class="flakiness-quarantine-callout">
|
|
18
|
+
<strong>{{charts.flakinessQuarantineTotal}} {{pluralise charts.flakinessQuarantineTotal "test exceeds" "tests exceed"}} the {{#if charts.flakinessQuarantineThreshold}}{{charts.flakinessQuarantineThreshold}}% {{/if}}quarantine threshold.</strong>
|
|
19
|
+
Quarantine or stabilize {{pluralise charts.flakinessQuarantineTotal "it before it masks" "them before they mask"}} real regressions.
|
|
20
|
+
</div>
|
|
21
|
+
{{/if}}
|
|
16
22
|
<table class="flakiness-table">
|
|
17
23
|
<thead>
|
|
18
24
|
<tr>
|
|
19
25
|
<th class="flakiness-table__test">Test</th>
|
|
20
26
|
<th class="flakiness-table__rate">Flake rate</th>
|
|
21
27
|
<th class="flakiness-table__count">Runs flaky</th>
|
|
28
|
+
<th class="flakiness-table__streak">Streak</th>
|
|
22
29
|
<th class="flakiness-table__spark">History</th>
|
|
23
30
|
</tr>
|
|
24
31
|
</thead>
|
|
25
32
|
<tbody>
|
|
26
33
|
{{#each charts.flakinessTable}}
|
|
27
34
|
<tr>
|
|
28
|
-
<td class="flakiness-table__test" title="{{this.title}}">{{this.title}}</td>
|
|
35
|
+
<td class="flakiness-table__test" title="{{this.title}}"><span class="flakiness-table__test-inner"><span class="flakiness-table__test-title">{{this.title}}</span>{{#if this.quarantine}}<span class="quarantine-chip">QUARANTINE</span>{{/if}}</span></td>
|
|
29
36
|
<td class="flakiness-table__rate">{{this.flakeRate}}%</td>
|
|
30
37
|
<td class="flakiness-table__count">{{this.flakeCount}} / {{this.totalRuns}}</td>
|
|
38
|
+
<td class="flakiness-table__streak">{{#if this.streak}}{{this.streak}}×{{else}}–{{/if}}</td>
|
|
31
39
|
<td class="flakiness-table__spark">
|
|
32
40
|
<span class="flaky-spark">
|
|
33
41
|
{{#each this.sparkline}}
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
{{#if execution.evidence}}
|
|
66
66
|
<div class="evidence-links">
|
|
67
|
-
{{#if execution.evidence.tracePath}}<span class="evidence-link"><span class="evidence-link__tag">Trace</span
|
|
68
|
-
{{#if execution.evidence.videoPath}}<span class="evidence-link"><span class="evidence-link__tag">Video</span
|
|
67
|
+
{{#if execution.evidence.tracePath}}<span class="evidence-link"><span class="evidence-link__tag">Trace</span>{{#if execution.evidence.traceUrl}}<a class="evidence-link__path evidence-link__path--url" href="{{execution.evidence.traceUrl}}">{{execution.evidence.traceUrl}}</a>{{#if execution.evidence.traceQr}}<img class="evidence-link__qr" src="{{execution.evidence.traceQr}}" alt="Trace QR" />{{/if}}{{else}}<span class="evidence-link__path">{{execution.evidence.tracePath}}</span>{{/if}}</span>{{/if}}
|
|
68
|
+
{{#if execution.evidence.videoPath}}<span class="evidence-link"><span class="evidence-link__tag">Video</span>{{#if execution.evidence.videoUrl}}<a class="evidence-link__path evidence-link__path--url" href="{{execution.evidence.videoUrl}}">{{execution.evidence.videoUrl}}</a>{{#if execution.evidence.videoQr}}<img class="evidence-link__qr" src="{{execution.evidence.videoQr}}" alt="Video QR" />{{/if}}{{else}}<span class="evidence-link__path">{{execution.evidence.videoPath}}</span>{{/if}}</span>{{/if}}
|
|
69
69
|
</div>
|
|
70
70
|
{{/if}}
|
|
71
71
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<h2>Since Last Run</h2>
|
|
4
4
|
<div class="run-diff__head">
|
|
5
5
|
<div class="run-diff__strip">
|
|
6
|
-
<span class="run-diff__count run-diff__count--new"><strong>{{runDiff.newFailures.length}}</strong> {{pluralise runDiff.newFailures.length "new failure" "new failures"}}</span> · <span class="run-diff__count run-diff__count--fixed"><strong>{{runDiff.fixed.length}}</strong> fixed</span> · <span class="run-diff__count run-diff__count--still"><strong>{{runDiff.stillFailing.length}}</strong> still failing</span>{{#if runDiff.noLongerRun.length}} · <span class="run-diff__count run-diff__count--neutral"><strong>{{runDiff.noLongerRun.length}}</strong> no longer run</span>{{/if}}
|
|
6
|
+
<span class="run-diff__count run-diff__count--new"><strong>{{runDiff.newFailures.length}}</strong> {{pluralise runDiff.newFailures.length "new failure" "new failures"}}</span> · <span class="run-diff__count run-diff__count--fixed"><strong>{{runDiff.fixed.length}}</strong> fixed</span> · <span class="run-diff__count run-diff__count--still"><strong>{{runDiff.stillFailing.length}}</strong> still failing</span>{{#if runDiff.newlyFlaky.length}} · <span class="run-diff__count run-diff__count--flaky"><strong>{{runDiff.newlyFlaky.length}}</strong> newly flaky</span>{{/if}}{{#if runDiff.noLongerRun.length}} · <span class="run-diff__count run-diff__count--neutral"><strong>{{runDiff.noLongerRun.length}}</strong> no longer run</span>{{/if}}
|
|
7
7
|
</div>
|
|
8
8
|
<div class="run-diff__baseline">vs. previous run on {{formatDate runDiff.baselineTimestamp}}</div>
|
|
9
9
|
{{#if runDiff.baselineTruncated}}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
{{#if (eq variant 'full')}}
|
|
15
15
|
{{> run-diff-list items=runDiff.newFailures label="New Failures" }}
|
|
16
|
+
{{> run-diff-list items=runDiff.newlyFlaky label="Newly Flaky" }}
|
|
16
17
|
{{> run-diff-list items=runDiff.fixed label="Fixed" }}
|
|
17
18
|
{{> run-diff-list items=runDiff.stillFailing label="Still Failing" }}
|
|
18
19
|
{{#if runDiff.noLongerRun.length}}
|
|
@@ -978,3 +978,34 @@ code {
|
|
|
978
978
|
.browser-matrix__note {
|
|
979
979
|
font-size: 9px; color: var(--text-3); margin-top: 6px;
|
|
980
980
|
}
|
|
981
|
+
|
|
982
|
+
/* ── Flaky intelligence (quarantine + streak) ───────────────────────────── */
|
|
983
|
+
/* The title cell ellipsizes; the chip must never be the part that clips, so
|
|
984
|
+
the cell content is a flex row: shrinking title span + fixed chip. */
|
|
985
|
+
.flakiness-table__test-inner { display: flex; align-items: center; gap: 5px; min-width: 0; }
|
|
986
|
+
.flakiness-table__test-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
987
|
+
.flakiness-quarantine-callout {
|
|
988
|
+
font-size: 9.5px; color: var(--text-2);
|
|
989
|
+
background: var(--flaky-dim);
|
|
990
|
+
border: 1px solid var(--flaky);
|
|
991
|
+
border-radius: 6px; padding: 7px 10px; margin-bottom: 10px;
|
|
992
|
+
}
|
|
993
|
+
.quarantine-chip {
|
|
994
|
+
flex: none;
|
|
995
|
+
font-family: 'Inter', system-ui, sans-serif; font-weight: 700;
|
|
996
|
+
font-size: 6.5px; letter-spacing: 0.7px;
|
|
997
|
+
color: var(--flaky); border: 1px solid var(--flaky);
|
|
998
|
+
border-radius: 3px; padding: 1px 4px;
|
|
999
|
+
}
|
|
1000
|
+
.flakiness-table__streak { text-align: center; width: 52px; }
|
|
1001
|
+
.run-diff__count--flaky strong { color: var(--flaky); }
|
|
1002
|
+
|
|
1003
|
+
/* ── Evidence deep links + QR (evidenceUrlTemplate) ─────────────────────── */
|
|
1004
|
+
.evidence-link__path--url {
|
|
1005
|
+
color: var(--accent); text-decoration: none;
|
|
1006
|
+
word-break: break-all;
|
|
1007
|
+
}
|
|
1008
|
+
.evidence-link__qr {
|
|
1009
|
+
width: 44px; height: 44px; margin-left: 8px; vertical-align: middle;
|
|
1010
|
+
border: 1px solid var(--border); border-radius: 3px; background: #fff;
|
|
1011
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"author": "ReportForge",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"chrome-finder": "^1.0.7",
|
|
92
92
|
"glob": "^13.0.6",
|
|
93
93
|
"handlebars": "^4.7.8",
|
|
94
|
+
"qrcode-generator": "^2.0.4",
|
|
94
95
|
"sharp": "^0.35.3",
|
|
95
96
|
"zod": "^3.23.8"
|
|
96
97
|
},
|