@reportforge/playwright-pdf 0.3.1 → 0.4.1
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/CHANGELOG.md +8 -0
- package/README.md +3 -0
- package/dist/cli/export-feedback.js +87 -0
- package/dist/demo/cli.js +413 -59
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +461 -68
- package/dist/index.mjs +538 -145
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +2 -0
- package/dist/templates/partials/analysis-oneliner.hbs +3 -0
- package/dist/templates/partials/failure-analysis.hbs +42 -0
- package/dist/templates/styles/base.css +3 -0
- package/dist/templates/styles/detailed.css +21 -0
- package/package.json +8 -3
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(path15) {
|
|
1233
|
+
return /^\.|this\b/.test(path15.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(path15) {
|
|
1238
|
+
return path15.parts.length === 1 && !AST.helpers.scopedId(path15) && !path15.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(path15, 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: path15,
|
|
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), path15 = decorator.path;
|
|
2638
2638
|
this.useDecorators = true;
|
|
2639
|
-
this.opcode("registerDecorator", params.length,
|
|
2639
|
+
this.opcode("registerDecorator", params.length, path15.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 path15 = sexpr.path, name = path15.parts[0], isBlock = program != null || inverse != null;
|
|
2704
|
+
this.opcode("getContext", path15.depth);
|
|
2705
2705
|
this.opcode("pushProgram", program);
|
|
2706
2706
|
this.opcode("pushProgram", inverse);
|
|
2707
|
-
|
|
2708
|
-
this.accept(
|
|
2707
|
+
path15.strict = true;
|
|
2708
|
+
this.accept(path15);
|
|
2709
2709
|
this.opcode("invokeAmbiguous", name, isBlock);
|
|
2710
2710
|
},
|
|
2711
2711
|
simpleSexpr: function simpleSexpr(sexpr) {
|
|
2712
|
-
var
|
|
2713
|
-
|
|
2714
|
-
this.accept(
|
|
2712
|
+
var path15 = sexpr.path;
|
|
2713
|
+
path15.strict = true;
|
|
2714
|
+
this.accept(path15);
|
|
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), path15 = sexpr.path, name = path15.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
|
+
path15.strict = true;
|
|
2725
|
+
path15.falsy = true;
|
|
2726
|
+
this.accept(path15);
|
|
2727
|
+
this.opcode("invokeHelper", params.length, path15.original, _ast2["default"].helpers.simpleId(path15));
|
|
2728
2728
|
}
|
|
2729
2729
|
},
|
|
2730
|
-
PathExpression: function PathExpression(
|
|
2731
|
-
this.addDepth(
|
|
2732
|
-
this.opcode("getContext",
|
|
2733
|
-
var name =
|
|
2730
|
+
PathExpression: function PathExpression(path15) {
|
|
2731
|
+
this.addDepth(path15.depth);
|
|
2732
|
+
this.opcode("getContext", path15.depth);
|
|
2733
|
+
var name = path15.parts[0], scoped = _ast2["default"].helpers.scopedId(path15), blockParamId = !path15.depth && !scoped && this.blockParamIndex(name);
|
|
2734
2734
|
if (blockParamId) {
|
|
2735
|
-
this.opcode("lookupBlockParam", blockParamId,
|
|
2735
|
+
this.opcode("lookupBlockParam", blockParamId, path15.parts);
|
|
2736
2736
|
} else if (!name) {
|
|
2737
2737
|
this.opcode("pushContext");
|
|
2738
|
-
} else if (
|
|
2738
|
+
} else if (path15.data) {
|
|
2739
2739
|
this.options.data = true;
|
|
2740
|
-
this.opcode("lookupData",
|
|
2740
|
+
this.opcode("lookupData", path15.depth, path15.parts, path15.strict);
|
|
2741
2741
|
} else {
|
|
2742
|
-
this.opcode("lookupOnContext",
|
|
2742
|
+
this.opcode("lookupOnContext", path15.parts, path15.falsy, path15.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 path15 = aPath;
|
|
3099
3099
|
var url = urlParse(aPath);
|
|
3100
3100
|
if (url) {
|
|
3101
3101
|
if (!url.path) {
|
|
3102
3102
|
return aPath;
|
|
3103
3103
|
}
|
|
3104
|
-
|
|
3104
|
+
path15 = url.path;
|
|
3105
3105
|
}
|
|
3106
|
-
var isAbsolute = exports.isAbsolute(
|
|
3107
|
-
var parts =
|
|
3106
|
+
var isAbsolute = exports.isAbsolute(path15);
|
|
3107
|
+
var parts = path15.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
|
+
path15 = parts.join("/");
|
|
3125
|
+
if (path15 === "") {
|
|
3126
|
+
path15 = isAbsolute ? "/" : ".";
|
|
3127
3127
|
}
|
|
3128
3128
|
if (url) {
|
|
3129
|
-
url.path =
|
|
3129
|
+
url.path = path15;
|
|
3130
3130
|
return urlGenerate(url);
|
|
3131
3131
|
}
|
|
3132
|
-
return
|
|
3132
|
+
return path15;
|
|
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 path15 = id.parts.join("/");
|
|
5936
|
+
return (id.data ? "@" : "") + "PATH:" + path15;
|
|
5937
5937
|
};
|
|
5938
5938
|
PrintVisitor.prototype.StringLiteral = function(string) {
|
|
5939
5939
|
return '"' + string.value + '"';
|
|
@@ -5974,8 +5974,8 @@ var require_lib = __commonJS({
|
|
|
5974
5974
|
handlebars.print = printer.print;
|
|
5975
5975
|
module.exports = handlebars;
|
|
5976
5976
|
function extension(module2, filename) {
|
|
5977
|
-
var
|
|
5978
|
-
var templateString =
|
|
5977
|
+
var fs14 = __require("fs");
|
|
5978
|
+
var templateString = fs14.readFileSync(filename, "utf8");
|
|
5979
5979
|
module2.exports = handlebars.compile(templateString);
|
|
5980
5980
|
}
|
|
5981
5981
|
if (typeof __require !== "undefined" && __require.extensions) {
|
|
@@ -6004,7 +6004,7 @@ var require_util2 = __commonJS({
|
|
|
6004
6004
|
"node_modules/chrome-finder/lib/util.js"(exports, module) {
|
|
6005
6005
|
"use strict";
|
|
6006
6006
|
init_esm_shims();
|
|
6007
|
-
var
|
|
6007
|
+
var fs14 = __require("fs");
|
|
6008
6008
|
var newLineRegex = /\r?\n/;
|
|
6009
6009
|
function sort(installations, priorities) {
|
|
6010
6010
|
const defaultPriority = 10;
|
|
@@ -6022,7 +6022,7 @@ var require_util2 = __commonJS({
|
|
|
6022
6022
|
return false;
|
|
6023
6023
|
}
|
|
6024
6024
|
try {
|
|
6025
|
-
|
|
6025
|
+
fs14.accessSync(file);
|
|
6026
6026
|
return true;
|
|
6027
6027
|
} catch (e) {
|
|
6028
6028
|
return false;
|
|
@@ -6033,7 +6033,7 @@ var require_util2 = __commonJS({
|
|
|
6033
6033
|
return false;
|
|
6034
6034
|
}
|
|
6035
6035
|
try {
|
|
6036
|
-
var stat =
|
|
6036
|
+
var stat = fs14.statSync(file);
|
|
6037
6037
|
return stat && typeof stat.isFile === "function" && stat.isFile();
|
|
6038
6038
|
} catch (e) {
|
|
6039
6039
|
return false;
|
|
@@ -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 path15 = __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 = path15.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 path15 = __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 = path15.join(inst.trim(), suffix);
|
|
6099
6099
|
if (canAccess(execPath)) {
|
|
6100
6100
|
installations.push(execPath);
|
|
6101
6101
|
}
|
|
@@ -6124,8 +6124,8 @@ var require_linux = __commonJS({
|
|
|
6124
6124
|
"use strict";
|
|
6125
6125
|
init_esm_shims();
|
|
6126
6126
|
var { execSync: execSync3, execFileSync: execFileSync2 } = __require("child_process");
|
|
6127
|
-
var
|
|
6128
|
-
var
|
|
6127
|
+
var path15 = __require("path");
|
|
6128
|
+
var fs14 = __require("fs");
|
|
6129
6129
|
var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
|
|
6130
6130
|
function findChromeExecutablesForLinuxDesktop(folder) {
|
|
6131
6131
|
const argumentsRegex = /(^[^ ]+).*/;
|
|
@@ -6142,7 +6142,7 @@ var require_linux = __commonJS({
|
|
|
6142
6142
|
function linux() {
|
|
6143
6143
|
let installations = [];
|
|
6144
6144
|
const desktopInstallationFolders = [
|
|
6145
|
-
|
|
6145
|
+
path15.join(__require("os").homedir(), ".local/share/applications/"),
|
|
6146
6146
|
"/usr/share/applications/"
|
|
6147
6147
|
];
|
|
6148
6148
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -6168,7 +6168,7 @@ var require_linux = __commonJS({
|
|
|
6168
6168
|
].map((possiblePath) => {
|
|
6169
6169
|
try {
|
|
6170
6170
|
const chromePathToTest = possiblePath + "/" + executable;
|
|
6171
|
-
if (
|
|
6171
|
+
if (fs14.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
|
|
6172
6172
|
installations.push(chromePathToTest);
|
|
6173
6173
|
return chromePathToTest;
|
|
6174
6174
|
}
|
|
@@ -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: path15, errorMaps, issueData } = params;
|
|
6754
|
+
const fullPath = [...path15, ...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, path15, key) {
|
|
6875
6875
|
this._cachedPath = [];
|
|
6876
6876
|
this.parent = parent;
|
|
6877
6877
|
this.data = value;
|
|
6878
|
-
this._path =
|
|
6878
|
+
this._path = path15;
|
|
6879
6879
|
this._key = key;
|
|
6880
6880
|
}
|
|
6881
6881
|
get path() {
|
|
@@ -10355,6 +10355,16 @@ var notifyConfigSchema = external_exports.object({
|
|
|
10355
10355
|
discord: channelConfigSchema.optional(),
|
|
10356
10356
|
email: emailChannelConfigSchema.optional()
|
|
10357
10357
|
}).optional();
|
|
10358
|
+
var failureAnalysisConfigSchema = external_exports.object({
|
|
10359
|
+
enabled: external_exports.boolean().default(true),
|
|
10360
|
+
maxClusters: external_exports.number().int().min(1).max(50).default(10),
|
|
10361
|
+
// Qualitative floor (not a numeric % — consistent with the match-strength design).
|
|
10362
|
+
minStrength: external_exports.enum(["weak", "moderate", "strong"]).default("weak"),
|
|
10363
|
+
maxFailuresToAnalyse: external_exports.number().int().min(1).default(500),
|
|
10364
|
+
// Consumed by the Plan 3 feedback collector; inert until then.
|
|
10365
|
+
collectUnclassified: external_exports.boolean().default(true),
|
|
10366
|
+
autoUpdateModel: external_exports.boolean().default(true)
|
|
10367
|
+
}).default({});
|
|
10358
10368
|
var ReporterOptionsSchema = external_exports.object({
|
|
10359
10369
|
outputFile: external_exports.string().optional().default(DEFAULT_OPTIONS.outputFile),
|
|
10360
10370
|
template: external_exports.union([
|
|
@@ -10384,6 +10394,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10384
10394
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10385
10395
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10386
10396
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10397
|
+
failureAnalysis: failureAnalysisConfigSchema,
|
|
10387
10398
|
templatePath: external_exports.union([
|
|
10388
10399
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
10389
10400
|
external_exports.array(
|
|
@@ -10623,6 +10634,17 @@ function clearCache() {
|
|
|
10623
10634
|
init_esm_shims();
|
|
10624
10635
|
var LICENSE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbvknstk0gxoJ7UwATdlxR8btxvO7u7RdpO1uV90PtuE=\n-----END PUBLIC KEY-----";
|
|
10625
10636
|
|
|
10637
|
+
// src/analysis/ModelStore.ts
|
|
10638
|
+
init_esm_shims();
|
|
10639
|
+
import fs3 from "fs";
|
|
10640
|
+
import os4 from "os";
|
|
10641
|
+
import path4 from "path";
|
|
10642
|
+
import { createPublicKey as createPublicKey2, verify as verify2 } from "crypto";
|
|
10643
|
+
|
|
10644
|
+
// src/analysis/model-bundle.ts
|
|
10645
|
+
init_esm_shims();
|
|
10646
|
+
var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
|
|
10647
|
+
|
|
10626
10648
|
// src/utils/logger.ts
|
|
10627
10649
|
init_esm_shims();
|
|
10628
10650
|
var PREFIX = "[reportforge]";
|
|
@@ -10644,6 +10666,83 @@ ${JSON.stringify(data, null, 2)}` : "";
|
|
|
10644
10666
|
}
|
|
10645
10667
|
};
|
|
10646
10668
|
|
|
10669
|
+
// src/analysis/ModelStore.ts
|
|
10670
|
+
var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
|
|
10671
|
+
var testDeps = null;
|
|
10672
|
+
function cacheFile() {
|
|
10673
|
+
return testDeps?.cacheFile ?? path4.join(os4.homedir(), ".reportforge", "model.json");
|
|
10674
|
+
}
|
|
10675
|
+
function publicKey() {
|
|
10676
|
+
const pem = testDeps?.publicKeyPem ?? LICENSE_PUBLIC_KEY_PEM;
|
|
10677
|
+
try {
|
|
10678
|
+
return pem && pem.includes("PUBLIC KEY") ? createPublicKey2(pem) : null;
|
|
10679
|
+
} catch {
|
|
10680
|
+
return null;
|
|
10681
|
+
}
|
|
10682
|
+
}
|
|
10683
|
+
function verifyModelSignature(modelJson, signatureB64url) {
|
|
10684
|
+
const key = publicKey();
|
|
10685
|
+
if (!key) return false;
|
|
10686
|
+
try {
|
|
10687
|
+
return verify2(null, Buffer.from(MODEL_SIG_DOMAIN + modelJson), key, Buffer.from(signatureB64url, "base64url"));
|
|
10688
|
+
} catch {
|
|
10689
|
+
return false;
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10692
|
+
function schemaValid(m) {
|
|
10693
|
+
const x = m;
|
|
10694
|
+
return !!x && Array.isArray(x.labels) && x.labels.length > 0 && typeof x.version === "number" && !!x.priors && !!x.totals && !!x.logLikelihoods && typeof x.vocabSize === "number" && typeof x.alpha === "number";
|
|
10695
|
+
}
|
|
10696
|
+
function readCache2() {
|
|
10697
|
+
try {
|
|
10698
|
+
const raw = fs3.readFileSync(cacheFile(), "utf8");
|
|
10699
|
+
const c = JSON.parse(raw);
|
|
10700
|
+
if (typeof c.version === "number" && typeof c.modelJson === "string" && typeof c.signature === "string") {
|
|
10701
|
+
return c;
|
|
10702
|
+
}
|
|
10703
|
+
} catch {
|
|
10704
|
+
}
|
|
10705
|
+
return null;
|
|
10706
|
+
}
|
|
10707
|
+
function loadModel(autoUpdate) {
|
|
10708
|
+
if (!autoUpdate) return MODEL;
|
|
10709
|
+
const c = readCache2();
|
|
10710
|
+
if (!c || c.version <= MODEL.version) return MODEL;
|
|
10711
|
+
if (!verifyModelSignature(c.modelJson, c.signature)) {
|
|
10712
|
+
logger.debug(`cached model v${c.version} signature invalid \u2014 using bundled (possible tampering or key mismatch)`);
|
|
10713
|
+
return MODEL;
|
|
10714
|
+
}
|
|
10715
|
+
try {
|
|
10716
|
+
const m = JSON.parse(c.modelJson);
|
|
10717
|
+
if (schemaValid(m)) return m;
|
|
10718
|
+
logger.debug(`cached model v${c.version} failed schema validation \u2014 using bundled`);
|
|
10719
|
+
} catch {
|
|
10720
|
+
logger.debug(`cached model v${c.version} JSON parse failed \u2014 using bundled`);
|
|
10721
|
+
}
|
|
10722
|
+
return MODEL;
|
|
10723
|
+
}
|
|
10724
|
+
function acceptDeliveredModel(d) {
|
|
10725
|
+
try {
|
|
10726
|
+
const current = readCache2()?.version ?? MODEL.version;
|
|
10727
|
+
if (d.version <= current) return;
|
|
10728
|
+
if (!verifyModelSignature(d.modelJson, d.signature)) {
|
|
10729
|
+
logger.debug(`delivered model v${d.version} signature invalid \u2014 rejected`);
|
|
10730
|
+
return;
|
|
10731
|
+
}
|
|
10732
|
+
const m = JSON.parse(d.modelJson);
|
|
10733
|
+
if (!schemaValid(m)) {
|
|
10734
|
+
logger.debug(`delivered model v${d.version} failed schema validation \u2014 rejected`);
|
|
10735
|
+
return;
|
|
10736
|
+
}
|
|
10737
|
+
const file = cacheFile();
|
|
10738
|
+
fs3.mkdirSync(path4.dirname(file), { recursive: true });
|
|
10739
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
10740
|
+
fs3.writeFileSync(tmp, JSON.stringify({ version: d.version, modelJson: d.modelJson, signature: d.signature }), { mode: 384 });
|
|
10741
|
+
fs3.renameSync(tmp, file);
|
|
10742
|
+
} catch {
|
|
10743
|
+
}
|
|
10744
|
+
}
|
|
10745
|
+
|
|
10647
10746
|
// src/license/LicenseClient.ts
|
|
10648
10747
|
var DEFAULT_SERVER_URL = "https://reportforge.org";
|
|
10649
10748
|
var REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -10657,6 +10756,8 @@ var LicenseClient = class {
|
|
|
10657
10756
|
this.serverUrl = normalizeUrl(
|
|
10658
10757
|
opts.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL
|
|
10659
10758
|
);
|
|
10759
|
+
this.autoUpdateModel = opts.autoUpdateModel ?? true;
|
|
10760
|
+
this.modelVersionForServer = this.autoUpdateModel ? opts.currentModelVersion ?? 0 : Number.MAX_SAFE_INTEGER;
|
|
10660
10761
|
}
|
|
10661
10762
|
async resolve() {
|
|
10662
10763
|
const key = this.rawKey || process.env.RF_LICENSE_KEY?.trim();
|
|
@@ -10737,6 +10838,9 @@ var LicenseClient = class {
|
|
|
10737
10838
|
machinesUsed: result.machinesUsed,
|
|
10738
10839
|
machineLimit: result.machineLimit
|
|
10739
10840
|
});
|
|
10841
|
+
if (this.autoUpdateModel && result.model) {
|
|
10842
|
+
acceptDeliveredModel(result.model);
|
|
10843
|
+
}
|
|
10740
10844
|
return {
|
|
10741
10845
|
plan: "subscription",
|
|
10742
10846
|
source: "jwt-online",
|
|
@@ -10773,13 +10877,15 @@ var LicenseClient = class {
|
|
|
10773
10877
|
return this.post("/api/license/activate", {
|
|
10774
10878
|
licenseKey,
|
|
10775
10879
|
fingerprint: this.fingerprint.hash,
|
|
10776
|
-
label: this.fingerprint.label
|
|
10880
|
+
label: this.fingerprint.label,
|
|
10881
|
+
modelVersion: this.modelVersionForServer
|
|
10777
10882
|
});
|
|
10778
10883
|
}
|
|
10779
10884
|
async refresh(jwt) {
|
|
10780
10885
|
return this.post("/api/license/refresh", {
|
|
10781
10886
|
jwt,
|
|
10782
|
-
label: this.fingerprint.label
|
|
10887
|
+
label: this.fingerprint.label,
|
|
10888
|
+
modelVersion: this.modelVersionForServer
|
|
10783
10889
|
});
|
|
10784
10890
|
}
|
|
10785
10891
|
async post(route, body) {
|
|
@@ -10837,9 +10943,292 @@ function normalizeUrl(url) {
|
|
|
10837
10943
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
10838
10944
|
}
|
|
10839
10945
|
|
|
10946
|
+
// src/analysis/index.ts
|
|
10947
|
+
init_esm_shims();
|
|
10948
|
+
|
|
10949
|
+
// src/analysis/FailureClusterer.ts
|
|
10950
|
+
init_esm_shims();
|
|
10951
|
+
|
|
10952
|
+
// src/analysis/NaiveBayesClassifier.ts
|
|
10953
|
+
init_esm_shims();
|
|
10954
|
+
|
|
10955
|
+
// src/analysis/tokenize.ts
|
|
10956
|
+
init_esm_shims();
|
|
10957
|
+
function tokenize(text) {
|
|
10958
|
+
return text.toLowerCase().replace(/https?:\/\/\S+/gi, " URL ").replace(/[^a-zA-Z0-9\s]/g, " ").replace(/\b\d{3,}[a-z]*\b/g, " NUM ").split(/\s+/).filter((t) => t.length > 1);
|
|
10959
|
+
}
|
|
10960
|
+
|
|
10961
|
+
// src/analysis/NaiveBayesClassifier.ts
|
|
10962
|
+
function classify(model, text) {
|
|
10963
|
+
const tokens = tokenize(text);
|
|
10964
|
+
const vocabTokens = tokens.filter(
|
|
10965
|
+
(tok) => model.labels.some((l) => model.logLikelihoods[l]?.[tok] !== void 0)
|
|
10966
|
+
);
|
|
10967
|
+
if (vocabTokens.length === 0) {
|
|
10968
|
+
return { label: "unknown", margin: 0 };
|
|
10969
|
+
}
|
|
10970
|
+
const scores = {};
|
|
10971
|
+
for (const label of model.labels) {
|
|
10972
|
+
let s = model.priors[label];
|
|
10973
|
+
const oov = Math.log(model.alpha / (model.totals[label] + model.alpha * model.vocabSize));
|
|
10974
|
+
const ll = model.logLikelihoods[label];
|
|
10975
|
+
for (const tok of vocabTokens) {
|
|
10976
|
+
s += ll[tok] ?? oov;
|
|
10977
|
+
}
|
|
10978
|
+
scores[label] = s;
|
|
10979
|
+
}
|
|
10980
|
+
const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
10981
|
+
const topLog = ranked[0][1];
|
|
10982
|
+
const secondLog = ranked[1]?.[1] ?? topLog;
|
|
10983
|
+
const margin = (topLog - secondLog) / vocabTokens.length;
|
|
10984
|
+
return { label: ranked[0][0], margin };
|
|
10985
|
+
}
|
|
10986
|
+
|
|
10987
|
+
// src/analysis/constants.ts
|
|
10988
|
+
init_esm_shims();
|
|
10989
|
+
var STRENGTH = {
|
|
10990
|
+
unknownMaxMargin: 0.05,
|
|
10991
|
+
collectMaxMargin: 0.1,
|
|
10992
|
+
harvestMinMargin: 0.25,
|
|
10993
|
+
moderateMinMargin: 0.1,
|
|
10994
|
+
strongMinMargin: 0.25
|
|
10995
|
+
};
|
|
10996
|
+
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
10997
|
+
var FLAKY_MARGIN = 999;
|
|
10998
|
+
|
|
10999
|
+
// src/analysis/FailureClusterer.ts
|
|
11000
|
+
var MAX_TESTS_PER_CLUSTER = 20;
|
|
11001
|
+
var MAX_MESSAGE_CHARS = 200;
|
|
11002
|
+
function strengthOf(margin) {
|
|
11003
|
+
if (margin >= STRENGTH.strongMinMargin) return "strong";
|
|
11004
|
+
if (margin >= STRENGTH.moderateMinMargin) return "moderate";
|
|
11005
|
+
return "weak";
|
|
11006
|
+
}
|
|
11007
|
+
function classifyInput(f) {
|
|
11008
|
+
const head = (f.error.stack ?? "").split("\n").slice(0, 3).join(" ");
|
|
11009
|
+
return `${f.error.message} ${head}`;
|
|
11010
|
+
}
|
|
11011
|
+
function topFrame(stack) {
|
|
11012
|
+
if (!stack) return void 0;
|
|
11013
|
+
for (const line of stack.split("\n")) {
|
|
11014
|
+
const t = line.trim();
|
|
11015
|
+
if (t.startsWith("at ") && !t.replace(/\\/g, "/").includes("node_modules/playwright")) return t;
|
|
11016
|
+
}
|
|
11017
|
+
return void 0;
|
|
11018
|
+
}
|
|
11019
|
+
function representativeMessage(message) {
|
|
11020
|
+
const firstLine = message.split("\n")[0];
|
|
11021
|
+
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
11022
|
+
}
|
|
11023
|
+
function classifyOne(f, model) {
|
|
11024
|
+
if (f.retryHistory.some((r) => r.status === "passed")) {
|
|
11025
|
+
return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
|
|
11026
|
+
}
|
|
11027
|
+
const { label, margin } = classify(model, classifyInput(f));
|
|
11028
|
+
return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
|
|
11029
|
+
}
|
|
11030
|
+
function classifyAll(failures, model) {
|
|
11031
|
+
return failures.map((f) => classifyOne(f, model));
|
|
11032
|
+
}
|
|
11033
|
+
function clusterClassified(classified) {
|
|
11034
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11035
|
+
for (const c of classified) {
|
|
11036
|
+
const key = `${c.category}::${c.frame ?? "__no-stack__"}`;
|
|
11037
|
+
const bucket = groups.get(key);
|
|
11038
|
+
if (bucket) bucket.push(c);
|
|
11039
|
+
else groups.set(key, [c]);
|
|
11040
|
+
}
|
|
11041
|
+
const clusters = [];
|
|
11042
|
+
for (const members of groups.values()) {
|
|
11043
|
+
const margin = Math.max(...members.map((m) => m.margin));
|
|
11044
|
+
clusters.push({
|
|
11045
|
+
category: members[0].category,
|
|
11046
|
+
strength: strengthOf(margin),
|
|
11047
|
+
margin,
|
|
11048
|
+
count: members.length,
|
|
11049
|
+
tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
|
|
11050
|
+
testsTotal: members.length,
|
|
11051
|
+
representativeMessage: representativeMessage(members[0].failure.error.message),
|
|
11052
|
+
stackFrame: members[0].frame
|
|
11053
|
+
});
|
|
11054
|
+
}
|
|
11055
|
+
return clusters;
|
|
11056
|
+
}
|
|
11057
|
+
|
|
11058
|
+
// src/analysis/SummaryWriter.ts
|
|
11059
|
+
init_esm_shims();
|
|
11060
|
+
var CATEGORY_LABEL = {
|
|
11061
|
+
assertion: "Assertion failure",
|
|
11062
|
+
timeout: "Timeout",
|
|
11063
|
+
"locator-not-found": "Element not found",
|
|
11064
|
+
network: "Network error",
|
|
11065
|
+
navigation: "Navigation error",
|
|
11066
|
+
"env-config": "Environment / config",
|
|
11067
|
+
"flaky-by-retry": "Flaky (passed on retry)",
|
|
11068
|
+
unknown: "Unclassified"
|
|
11069
|
+
};
|
|
11070
|
+
var SHORT_LABEL = {
|
|
11071
|
+
assertion: "assertion",
|
|
11072
|
+
timeout: "timeout",
|
|
11073
|
+
"locator-not-found": "element not found",
|
|
11074
|
+
network: "network",
|
|
11075
|
+
navigation: "navigation",
|
|
11076
|
+
"env-config": "env/config",
|
|
11077
|
+
"flaky-by-retry": "flaky",
|
|
11078
|
+
unknown: "unclassified"
|
|
11079
|
+
};
|
|
11080
|
+
function buildOneLiner(clusters) {
|
|
11081
|
+
if (clusters.length === 0) return "";
|
|
11082
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
11083
|
+
for (const c of clusters) {
|
|
11084
|
+
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
11085
|
+
}
|
|
11086
|
+
const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
|
|
11087
|
+
const tally = [...byCategory.entries()].sort((a, b) => b[1] - a[1]).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
|
|
11088
|
+
return [head, ...tally].join(" \xB7 ");
|
|
11089
|
+
}
|
|
11090
|
+
function dominantCategory(clusters) {
|
|
11091
|
+
return clusters[0]?.category ?? "unknown";
|
|
11092
|
+
}
|
|
11093
|
+
|
|
11094
|
+
// src/analysis/UnclassifiedCollector.ts
|
|
11095
|
+
init_esm_shims();
|
|
11096
|
+
import fs4 from "fs";
|
|
11097
|
+
import os5 from "os";
|
|
11098
|
+
import path5 from "path";
|
|
11099
|
+
|
|
11100
|
+
// src/analysis/feedback-csv.ts
|
|
11101
|
+
init_esm_shims();
|
|
11102
|
+
var FEEDBACK_HEADER = "category,margin,text";
|
|
11103
|
+
function parseFeedbackRows(content) {
|
|
11104
|
+
return content.trim().split("\n").slice(1).filter(Boolean).map((line) => {
|
|
11105
|
+
const parts = line.split(",");
|
|
11106
|
+
return { category: parts[0], margin: parts[1], text: parts.slice(2).join(","), raw: line };
|
|
11107
|
+
});
|
|
11108
|
+
}
|
|
11109
|
+
function feedbackIdentity(category, text) {
|
|
11110
|
+
return `${category} ${text}`;
|
|
11111
|
+
}
|
|
11112
|
+
|
|
11113
|
+
// src/analysis/UnclassifiedCollector.ts
|
|
11114
|
+
var MAX_ROWS = 500;
|
|
11115
|
+
var FILENAME = "unclassified.csv";
|
|
11116
|
+
var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
|
|
11117
|
+
var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
|
|
11118
|
+
var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
|
|
11119
|
+
var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
|
|
11120
|
+
var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
|
|
11121
|
+
var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
|
|
11122
|
+
function redactForStorage(raw) {
|
|
11123
|
+
const redacted = raw.replace(URI_RE, " URL ").replace(EMAIL_RE, " EMAIL ").replace(SECRET_RE, " SECRET ").replace(IP_RE, " IP ").replace(WIN_PATH_RE, " PATH ").replace(UNIX_PATH_RE, " PATH ");
|
|
11124
|
+
return tokenize(redacted).join(" ");
|
|
11125
|
+
}
|
|
11126
|
+
function shouldCollect(c) {
|
|
11127
|
+
return c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin;
|
|
11128
|
+
}
|
|
11129
|
+
function tildeify(p) {
|
|
11130
|
+
const home = os5.homedir();
|
|
11131
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
11132
|
+
}
|
|
11133
|
+
function collectUnclassified(classified, projectDir) {
|
|
11134
|
+
const rows = classified.filter(shouldCollect).map((c) => ({ text: redactForStorage(c.failure.error.message), category: c.category, margin: c.margin })).filter((r) => r.text.length > 0);
|
|
11135
|
+
if (rows.length === 0) return;
|
|
11136
|
+
const file = path5.join(projectDir, FILENAME);
|
|
11137
|
+
const firstTime = !fs4.existsSync(file);
|
|
11138
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
11139
|
+
if (!firstTime) {
|
|
11140
|
+
for (const r of parseFeedbackRows(fs4.readFileSync(file, "utf8"))) {
|
|
11141
|
+
byIdentity.set(feedbackIdentity(r.category, r.text), r.raw);
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
let added = false;
|
|
11145
|
+
for (const r of rows) {
|
|
11146
|
+
const id = feedbackIdentity(r.category, r.text);
|
|
11147
|
+
byIdentity.delete(id);
|
|
11148
|
+
byIdentity.set(id, `${r.category},${r.margin},${r.text}`);
|
|
11149
|
+
added = true;
|
|
11150
|
+
}
|
|
11151
|
+
if (!added) return;
|
|
11152
|
+
const all = [...byIdentity.values()].slice(-MAX_ROWS);
|
|
11153
|
+
fs4.mkdirSync(projectDir, { recursive: true });
|
|
11154
|
+
fs4.writeFileSync(file, `${FEEDBACK_HEADER}
|
|
11155
|
+
${all.join("\n")}
|
|
11156
|
+
`, "utf8");
|
|
11157
|
+
if (firstTime) {
|
|
11158
|
+
logger.info(
|
|
11159
|
+
`Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
|
|
11160
|
+
);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
|
|
11164
|
+
// src/utils/paths.ts
|
|
11165
|
+
init_esm_shims();
|
|
11166
|
+
import os6 from "os";
|
|
11167
|
+
import path6 from "path";
|
|
11168
|
+
import crypto from "crypto";
|
|
11169
|
+
function resolveProjectDir(options, cwd) {
|
|
11170
|
+
const projectKey = crypto.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
|
|
11171
|
+
return path6.join(os6.homedir(), ".reportforge", projectKey);
|
|
11172
|
+
}
|
|
11173
|
+
|
|
11174
|
+
// src/analysis/index.ts
|
|
11175
|
+
var DEFAULTS = { maxClusters: 10, minStrength: "weak", maxFailuresToAnalyse: 500 };
|
|
11176
|
+
var STRENGTH_RANK = Object.fromEntries(
|
|
11177
|
+
STRENGTH_LEVELS.map((level, i) => [level, i])
|
|
11178
|
+
);
|
|
11179
|
+
function analyseClassified(classified, totalFailures, opts = {}) {
|
|
11180
|
+
const maxClusters = opts.maxClusters ?? DEFAULTS.maxClusters;
|
|
11181
|
+
const minRank = STRENGTH_RANK[opts.minStrength ?? DEFAULTS.minStrength];
|
|
11182
|
+
const cap = opts.maxFailuresToAnalyse ?? DEFAULTS.maxFailuresToAnalyse;
|
|
11183
|
+
const clusters = clusterClassified(classified).filter((c) => STRENGTH_RANK[c.strength] >= minRank).sort((a, b) => b.count - a.count).slice(0, maxClusters);
|
|
11184
|
+
return {
|
|
11185
|
+
clusters,
|
|
11186
|
+
totalFailures,
|
|
11187
|
+
analysedCount: classified.length,
|
|
11188
|
+
truncated: totalFailures > cap,
|
|
11189
|
+
// dominantCategory is part of the AnalysisResult contract; reserved for
|
|
11190
|
+
// consumers (e.g. a future executive badge / notify summary). Not rendered
|
|
11191
|
+
// by the v1 partials.
|
|
11192
|
+
dominantCategory: dominantCategory(clusters),
|
|
11193
|
+
oneLiner: buildOneLiner(clusters)
|
|
11194
|
+
};
|
|
11195
|
+
}
|
|
11196
|
+
function templatesConsumeAnalysis(options) {
|
|
11197
|
+
if (options.templatePath && options.templatePath.length > 0) return true;
|
|
11198
|
+
const templates = Array.isArray(options.template) ? options.template : [options.template];
|
|
11199
|
+
return templates.some((t) => t === "detailed" || t === "executive");
|
|
11200
|
+
}
|
|
11201
|
+
function runFailureAnalysis(reportData, options, cwd) {
|
|
11202
|
+
const fa = options.failureAnalysis;
|
|
11203
|
+
if (reportData.stats.failed === 0 || !fa.enabled) return;
|
|
11204
|
+
const cap = fa.maxFailuresToAnalyse;
|
|
11205
|
+
const analysed = reportData.failures.slice(0, cap);
|
|
11206
|
+
const model = loadModel(fa.autoUpdateModel);
|
|
11207
|
+
const classified = classifyAll(analysed, model);
|
|
11208
|
+
if (templatesConsumeAnalysis(options)) {
|
|
11209
|
+
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
11210
|
+
}
|
|
11211
|
+
if (fa.collectUnclassified) {
|
|
11212
|
+
collectUnclassified(classified, resolveProjectDir(options, cwd));
|
|
11213
|
+
}
|
|
11214
|
+
}
|
|
11215
|
+
|
|
10840
11216
|
// src/collector/DataCollector.ts
|
|
10841
11217
|
init_esm_shims();
|
|
10842
11218
|
|
|
11219
|
+
// src/utils/strip-ansi.ts
|
|
11220
|
+
init_esm_shims();
|
|
11221
|
+
var ANSI_PATTERN = new RegExp(
|
|
11222
|
+
[
|
|
11223
|
+
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
11224
|
+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
|
|
11225
|
+
].join("|"),
|
|
11226
|
+
"g"
|
|
11227
|
+
);
|
|
11228
|
+
function stripAnsi(input) {
|
|
11229
|
+
return input.replace(ANSI_PATTERN, "");
|
|
11230
|
+
}
|
|
11231
|
+
|
|
10843
11232
|
// src/collector/SuiteWalker.ts
|
|
10844
11233
|
init_esm_shims();
|
|
10845
11234
|
|
|
@@ -11074,8 +11463,8 @@ var DataCollector = class {
|
|
|
11074
11463
|
testTitle: test.title,
|
|
11075
11464
|
suitePath,
|
|
11076
11465
|
error: {
|
|
11077
|
-
message: error.message ?? String(error),
|
|
11078
|
-
stack: error.stack
|
|
11466
|
+
message: stripAnsi(error.message ?? String(error)),
|
|
11467
|
+
stack: error.stack ? stripAnsi(error.stack) : void 0
|
|
11079
11468
|
},
|
|
11080
11469
|
screenshotPath: screenshotAttachment?.path,
|
|
11081
11470
|
screenshotBuffer: screenshotAttachment?.body,
|
|
@@ -11397,8 +11786,8 @@ var ShardMerger = class {
|
|
|
11397
11786
|
testTitle: spec.title,
|
|
11398
11787
|
suitePath,
|
|
11399
11788
|
error: {
|
|
11400
|
-
message: error?.message ?? "Test failed",
|
|
11401
|
-
stack: error?.stack
|
|
11789
|
+
message: stripAnsi(error?.message ?? "Test failed"),
|
|
11790
|
+
stack: error?.stack ? stripAnsi(error.stack) : void 0
|
|
11402
11791
|
},
|
|
11403
11792
|
screenshotPath: lastResult?.attachments.find(
|
|
11404
11793
|
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
@@ -11459,14 +11848,14 @@ var ShardMerger = class {
|
|
|
11459
11848
|
|
|
11460
11849
|
// src/pdf/PdfGenerator.ts
|
|
11461
11850
|
init_esm_shims();
|
|
11462
|
-
import
|
|
11463
|
-
import
|
|
11851
|
+
import fs12 from "fs";
|
|
11852
|
+
import path12 from "path";
|
|
11464
11853
|
|
|
11465
11854
|
// src/templates/engine.ts
|
|
11466
11855
|
init_esm_shims();
|
|
11467
11856
|
var import_handlebars = __toESM(require_lib());
|
|
11468
|
-
import
|
|
11469
|
-
import
|
|
11857
|
+
import fs5 from "fs";
|
|
11858
|
+
import path7 from "path";
|
|
11470
11859
|
|
|
11471
11860
|
// src/utils/duration.ts
|
|
11472
11861
|
init_esm_shims();
|
|
@@ -11486,9 +11875,9 @@ var TemplateEngine = class {
|
|
|
11486
11875
|
this.fontsBundleCss = "";
|
|
11487
11876
|
this.customTemplateCache = /* @__PURE__ */ new Map();
|
|
11488
11877
|
this.handlebars = import_handlebars.default.create();
|
|
11489
|
-
this.templatesDir =
|
|
11490
|
-
if (!
|
|
11491
|
-
this.templatesDir =
|
|
11878
|
+
this.templatesDir = path7.resolve(__dirname, "templates");
|
|
11879
|
+
if (!fs5.existsSync(this.templatesDir)) {
|
|
11880
|
+
this.templatesDir = path7.resolve(__dirname, "../templates");
|
|
11492
11881
|
}
|
|
11493
11882
|
this.registerHelpers();
|
|
11494
11883
|
this.registerPartials();
|
|
@@ -11505,21 +11894,21 @@ var TemplateEngine = class {
|
|
|
11505
11894
|
this.chartjsInline = inline;
|
|
11506
11895
|
}
|
|
11507
11896
|
render(data, template) {
|
|
11508
|
-
const layoutPath =
|
|
11509
|
-
if (!
|
|
11897
|
+
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
11898
|
+
if (!fs5.existsSync(layoutPath)) {
|
|
11510
11899
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
11511
11900
|
}
|
|
11512
|
-
const layoutSource =
|
|
11901
|
+
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
11513
11902
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
11514
11903
|
const ctx = this.buildContext(data, template);
|
|
11515
11904
|
return compiledLayout(ctx);
|
|
11516
11905
|
}
|
|
11517
11906
|
renderFromPath(data, filePath) {
|
|
11518
11907
|
if (!this.customTemplateCache.has(filePath)) {
|
|
11519
|
-
if (!
|
|
11908
|
+
if (!fs5.existsSync(filePath)) {
|
|
11520
11909
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
11521
11910
|
}
|
|
11522
|
-
const source =
|
|
11911
|
+
const source = fs5.readFileSync(filePath, "utf8");
|
|
11523
11912
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
11524
11913
|
}
|
|
11525
11914
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
@@ -11632,17 +12021,17 @@ var TemplateEngine = class {
|
|
|
11632
12021
|
return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
11633
12022
|
}
|
|
11634
12023
|
readStyle(filename) {
|
|
11635
|
-
const stylePath =
|
|
11636
|
-
if (!
|
|
11637
|
-
return
|
|
12024
|
+
const stylePath = path7.join(this.templatesDir, "styles", filename);
|
|
12025
|
+
if (!fs5.existsSync(stylePath)) return "";
|
|
12026
|
+
return fs5.readFileSync(stylePath, "utf8");
|
|
11638
12027
|
}
|
|
11639
12028
|
registerPartials() {
|
|
11640
|
-
const partialsDir =
|
|
11641
|
-
if (!
|
|
11642
|
-
for (const file of
|
|
12029
|
+
const partialsDir = path7.join(this.templatesDir, "partials");
|
|
12030
|
+
if (!fs5.existsSync(partialsDir)) return;
|
|
12031
|
+
for (const file of fs5.readdirSync(partialsDir)) {
|
|
11643
12032
|
if (!file.endsWith(".hbs")) continue;
|
|
11644
|
-
const name =
|
|
11645
|
-
const src =
|
|
12033
|
+
const name = path7.basename(file, ".hbs");
|
|
12034
|
+
const src = fs5.readFileSync(path7.join(partialsDir, file), "utf8");
|
|
11646
12035
|
this.handlebars.registerPartial(name, src);
|
|
11647
12036
|
}
|
|
11648
12037
|
}
|
|
@@ -11705,6 +12094,10 @@ var TemplateEngine = class {
|
|
|
11705
12094
|
);
|
|
11706
12095
|
this.handlebars.registerHelper("lt", (a, b) => a < b);
|
|
11707
12096
|
this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
|
|
12097
|
+
this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
|
|
12098
|
+
this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
|
|
12099
|
+
this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
|
|
12100
|
+
this.handlebars.registerHelper("subtract", (a, b) => (a ?? 0) - (b ?? 0));
|
|
11708
12101
|
this.handlebars.registerHelper("statusBadge", (status) => {
|
|
11709
12102
|
const map = {
|
|
11710
12103
|
passed: "badge--passed",
|
|
@@ -11726,7 +12119,7 @@ var TemplateEngine = class {
|
|
|
11726
12119
|
|
|
11727
12120
|
// src/screenshots/ScreenshotEmbedder.ts
|
|
11728
12121
|
init_esm_shims();
|
|
11729
|
-
import
|
|
12122
|
+
import fs6 from "fs";
|
|
11730
12123
|
import sharp from "sharp";
|
|
11731
12124
|
var ScreenshotEmbedder = class {
|
|
11732
12125
|
async embedAll(failures, opts) {
|
|
@@ -11735,7 +12128,7 @@ var ScreenshotEmbedder = class {
|
|
|
11735
12128
|
async embed(failure, opts) {
|
|
11736
12129
|
if (!failure.screenshotPath && !failure.screenshotBuffer) return;
|
|
11737
12130
|
try {
|
|
11738
|
-
const raw = failure.screenshotBuffer ?? await
|
|
12131
|
+
const raw = failure.screenshotBuffer ?? await fs6.promises.readFile(failure.screenshotPath);
|
|
11739
12132
|
const { buffer, mime } = opts.reencode ? await this.recompress(raw, opts) : { buffer: raw, mime: this.detectMimeType(raw) };
|
|
11740
12133
|
failure.screenshotBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
11741
12134
|
} catch (e) {
|
|
@@ -11747,7 +12140,7 @@ var ScreenshotEmbedder = class {
|
|
|
11747
12140
|
}
|
|
11748
12141
|
async embedLogoFile(logoPath) {
|
|
11749
12142
|
try {
|
|
11750
|
-
const buffer = await
|
|
12143
|
+
const buffer = await fs6.promises.readFile(logoPath);
|
|
11751
12144
|
const ext = logoPath.split(".").pop()?.toLowerCase();
|
|
11752
12145
|
let mime = "image/png";
|
|
11753
12146
|
if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
|
|
@@ -11792,7 +12185,7 @@ var ScreenshotEmbedder = class {
|
|
|
11792
12185
|
// src/pdf/BrowserManager.ts
|
|
11793
12186
|
init_esm_shims();
|
|
11794
12187
|
import { execSync as execSync2 } from "child_process";
|
|
11795
|
-
import
|
|
12188
|
+
import fs7 from "fs";
|
|
11796
12189
|
var BrowserManager = class {
|
|
11797
12190
|
async launch(executablePath) {
|
|
11798
12191
|
const puppeteer = __require("puppeteer-core");
|
|
@@ -11820,13 +12213,13 @@ var BrowserManager = class {
|
|
|
11820
12213
|
}
|
|
11821
12214
|
}
|
|
11822
12215
|
resolveChromePath(explicit, puppeteer) {
|
|
11823
|
-
if (explicit &&
|
|
12216
|
+
if (explicit && fs7.existsSync(explicit)) return explicit;
|
|
11824
12217
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
11825
|
-
if (envPath &&
|
|
12218
|
+
if (envPath && fs7.existsSync(envPath)) return envPath;
|
|
11826
12219
|
try {
|
|
11827
12220
|
const chromeFinder = require_lib2();
|
|
11828
12221
|
const found = chromeFinder();
|
|
11829
|
-
if (found &&
|
|
12222
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11830
12223
|
} catch {
|
|
11831
12224
|
}
|
|
11832
12225
|
const linuxPaths = [
|
|
@@ -11837,19 +12230,19 @@ var BrowserManager = class {
|
|
|
11837
12230
|
"/snap/bin/chromium"
|
|
11838
12231
|
];
|
|
11839
12232
|
for (const p of linuxPaths) {
|
|
11840
|
-
if (
|
|
12233
|
+
if (fs7.existsSync(p)) return p;
|
|
11841
12234
|
}
|
|
11842
12235
|
try {
|
|
11843
12236
|
const found = execSync2("which google-chrome chromium chromium-browser 2>/dev/null", {
|
|
11844
12237
|
stdio: "pipe"
|
|
11845
12238
|
}).toString().trim().split("\n")[0];
|
|
11846
|
-
if (found &&
|
|
12239
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11847
12240
|
} catch {
|
|
11848
12241
|
}
|
|
11849
12242
|
if (puppeteer.executablePath) {
|
|
11850
12243
|
try {
|
|
11851
12244
|
const bundled = puppeteer.executablePath();
|
|
11852
|
-
if (bundled &&
|
|
12245
|
+
if (bundled && fs7.existsSync(bundled)) return bundled;
|
|
11853
12246
|
} catch {
|
|
11854
12247
|
}
|
|
11855
12248
|
}
|
|
@@ -11862,21 +12255,21 @@ var BrowserManager = class {
|
|
|
11862
12255
|
|
|
11863
12256
|
// src/pdf/HtmlWriter.ts
|
|
11864
12257
|
init_esm_shims();
|
|
11865
|
-
import
|
|
11866
|
-
import
|
|
11867
|
-
import
|
|
11868
|
-
import
|
|
12258
|
+
import fs8 from "fs";
|
|
12259
|
+
import os7 from "os";
|
|
12260
|
+
import path8 from "path";
|
|
12261
|
+
import crypto2 from "crypto";
|
|
11869
12262
|
var HtmlWriter = class {
|
|
11870
12263
|
async write(html) {
|
|
11871
|
-
const tmpDir =
|
|
11872
|
-
const filename = `rf-report-${
|
|
11873
|
-
this.tempPath =
|
|
11874
|
-
await
|
|
12264
|
+
const tmpDir = os7.tmpdir();
|
|
12265
|
+
const filename = `rf-report-${crypto2.randomBytes(8).toString("hex")}.html`;
|
|
12266
|
+
this.tempPath = path8.join(tmpDir, filename);
|
|
12267
|
+
await fs8.promises.writeFile(this.tempPath, html, "utf8");
|
|
11875
12268
|
return this.tempPath;
|
|
11876
12269
|
}
|
|
11877
12270
|
async cleanup() {
|
|
11878
12271
|
if (this.tempPath) {
|
|
11879
|
-
await
|
|
12272
|
+
await fs8.promises.unlink(this.tempPath).catch(() => {
|
|
11880
12273
|
});
|
|
11881
12274
|
this.tempPath = void 0;
|
|
11882
12275
|
}
|
|
@@ -11910,10 +12303,10 @@ var ChartRenderer = class {
|
|
|
11910
12303
|
// src/pdf/PdfEncryptor.ts
|
|
11911
12304
|
init_esm_shims();
|
|
11912
12305
|
import { execFileSync } from "child_process";
|
|
11913
|
-
import
|
|
11914
|
-
import
|
|
11915
|
-
import
|
|
11916
|
-
import
|
|
12306
|
+
import fs9 from "fs";
|
|
12307
|
+
import path9 from "path";
|
|
12308
|
+
import os8 from "os";
|
|
12309
|
+
import crypto3 from "crypto";
|
|
11917
12310
|
var PdfEncryptor = class {
|
|
11918
12311
|
async encrypt(pdfPath, password) {
|
|
11919
12312
|
if (!password) return;
|
|
@@ -11923,10 +12316,10 @@ var PdfEncryptor = class {
|
|
|
11923
12316
|
);
|
|
11924
12317
|
return;
|
|
11925
12318
|
}
|
|
11926
|
-
const tmpPath =
|
|
11927
|
-
const pwFile =
|
|
12319
|
+
const tmpPath = path9.join(os8.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12320
|
+
const pwFile = path9.join(os8.tmpdir(), `rf-pw-${crypto3.randomBytes(8).toString("hex")}`);
|
|
11928
12321
|
try {
|
|
11929
|
-
|
|
12322
|
+
fs9.writeFileSync(pwFile, `${password}
|
|
11930
12323
|
${password}`, { mode: 384 });
|
|
11931
12324
|
execFileSync("qpdf", [
|
|
11932
12325
|
`--password-file=${pwFile}`,
|
|
@@ -11936,13 +12329,13 @@ ${password}`, { mode: 384 });
|
|
|
11936
12329
|
pdfPath,
|
|
11937
12330
|
tmpPath
|
|
11938
12331
|
], { stdio: "pipe" });
|
|
11939
|
-
|
|
12332
|
+
fs9.renameSync(tmpPath, pdfPath);
|
|
11940
12333
|
logger.info(`PDF encrypted with password.`);
|
|
11941
12334
|
} catch (e) {
|
|
11942
12335
|
logger.warn(`qpdf encryption failed: ${e.message}`);
|
|
11943
12336
|
} finally {
|
|
11944
|
-
if (
|
|
11945
|
-
|
|
12337
|
+
if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
|
|
12338
|
+
fs9.rmSync(pwFile, { force: true });
|
|
11946
12339
|
}
|
|
11947
12340
|
}
|
|
11948
12341
|
isQpdfAvailable() {
|
|
@@ -11957,8 +12350,8 @@ ${password}`, { mode: 384 });
|
|
|
11957
12350
|
|
|
11958
12351
|
// src/filename/FilenameResolver.ts
|
|
11959
12352
|
init_esm_shims();
|
|
11960
|
-
import
|
|
11961
|
-
import
|
|
12353
|
+
import path10 from "path";
|
|
12354
|
+
import fs10 from "fs";
|
|
11962
12355
|
var MAX_BRANCH_LENGTH = 50;
|
|
11963
12356
|
var FilenameResolver = class {
|
|
11964
12357
|
/**
|
|
@@ -11975,8 +12368,8 @@ var FilenameResolver = class {
|
|
|
11975
12368
|
const branch = this.sanitiseBranch(ctx.branch);
|
|
11976
12369
|
const status = ctx.status;
|
|
11977
12370
|
const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
|
|
11978
|
-
const absolute =
|
|
11979
|
-
|
|
12371
|
+
const absolute = path10.isAbsolute(resolved) ? resolved : path10.resolve(process.cwd(), resolved);
|
|
12372
|
+
fs10.mkdirSync(path10.dirname(absolute), { recursive: true });
|
|
11980
12373
|
return absolute;
|
|
11981
12374
|
}
|
|
11982
12375
|
/**
|
|
@@ -11988,7 +12381,7 @@ var FilenameResolver = class {
|
|
|
11988
12381
|
* // → '/out/2026-report-executive.pdf'
|
|
11989
12382
|
*/
|
|
11990
12383
|
injectTemplateSuffix(outputPath, suffix) {
|
|
11991
|
-
const ext =
|
|
12384
|
+
const ext = path10.extname(outputPath);
|
|
11992
12385
|
const base = outputPath.slice(0, outputPath.length - ext.length);
|
|
11993
12386
|
return `${base}-${suffix}${ext}`;
|
|
11994
12387
|
}
|
|
@@ -12046,8 +12439,8 @@ var PRESETS = {
|
|
|
12046
12439
|
|
|
12047
12440
|
// src/pdf/FailurePaginator.ts
|
|
12048
12441
|
init_esm_shims();
|
|
12049
|
-
import
|
|
12050
|
-
import
|
|
12442
|
+
import fs11 from "fs";
|
|
12443
|
+
import path11 from "path";
|
|
12051
12444
|
var FailurePaginator = class {
|
|
12052
12445
|
async paginate(failures, maxInline, pdfOutputPath) {
|
|
12053
12446
|
if (failures.length <= maxInline) {
|
|
@@ -12057,13 +12450,13 @@ var FailurePaginator = class {
|
|
|
12057
12450
|
const overflow = failures.slice(maxInline);
|
|
12058
12451
|
const sidecarPath = this.sidecarPathFor(pdfOutputPath);
|
|
12059
12452
|
try {
|
|
12060
|
-
await
|
|
12453
|
+
await fs11.promises.writeFile(
|
|
12061
12454
|
sidecarPath,
|
|
12062
12455
|
JSON.stringify(this.serialize(overflow), null, 2),
|
|
12063
12456
|
"utf8"
|
|
12064
12457
|
);
|
|
12065
12458
|
logger.info(
|
|
12066
|
-
`Emitted ${overflow.length} overflow failures \u2192 ${
|
|
12459
|
+
`Emitted ${overflow.length} overflow failures \u2192 ${path11.basename(sidecarPath)}`
|
|
12067
12460
|
);
|
|
12068
12461
|
} catch (e) {
|
|
12069
12462
|
logger.warn(`Could not write failure sidecar: ${e.message}`);
|
|
@@ -12072,9 +12465,9 @@ var FailurePaginator = class {
|
|
|
12072
12465
|
return { inline, overflowCount: overflow.length, sidecarPath };
|
|
12073
12466
|
}
|
|
12074
12467
|
sidecarPathFor(pdfPath) {
|
|
12075
|
-
const dir =
|
|
12076
|
-
const base =
|
|
12077
|
-
return
|
|
12468
|
+
const dir = path11.dirname(pdfPath);
|
|
12469
|
+
const base = path11.basename(pdfPath, path11.extname(pdfPath));
|
|
12470
|
+
return path11.join(dir, `${base}-failures.json`);
|
|
12078
12471
|
}
|
|
12079
12472
|
/**
|
|
12080
12473
|
* Drops the inlined image bytes — a 150-entry overflow would otherwise
|
|
@@ -12152,7 +12545,7 @@ var PdfGenerator = class {
|
|
|
12152
12545
|
const sources = this.resolveTemplateSources(options);
|
|
12153
12546
|
const isMulti = sources.length > 1;
|
|
12154
12547
|
for (const source of sources) {
|
|
12155
|
-
if (source.kind === "custom" && !
|
|
12548
|
+
if (source.kind === "custom" && !fs12.existsSync(source.absolutePath)) {
|
|
12156
12549
|
throw new Error(
|
|
12157
12550
|
`[reportforge] Custom template not found: ${source.absolutePath}`
|
|
12158
12551
|
);
|
|
@@ -12183,14 +12576,14 @@ var PdfGenerator = class {
|
|
|
12183
12576
|
await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
|
|
12184
12577
|
const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
|
|
12185
12578
|
if (capBytes > 0) {
|
|
12186
|
-
const sizeBytes = (await
|
|
12579
|
+
const sizeBytes = (await fs12.promises.stat(outputPath)).size;
|
|
12187
12580
|
if (sizeBytes > capBytes) {
|
|
12188
12581
|
const tighter = this.tightenForCap(compression, sizeBytes, capBytes);
|
|
12189
12582
|
logger.warn(
|
|
12190
12583
|
`PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
|
|
12191
12584
|
);
|
|
12192
12585
|
await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
|
|
12193
|
-
const finalSize = (await
|
|
12586
|
+
const finalSize = (await fs12.promises.stat(outputPath)).size;
|
|
12194
12587
|
if (finalSize > capBytes) {
|
|
12195
12588
|
logger.warn(
|
|
12196
12589
|
`PDF still ${fmtMb(finalSize)} after retune. Consider lowering maxInlineFailures or splitting the report.`
|
|
@@ -12203,7 +12596,7 @@ var PdfGenerator = class {
|
|
|
12203
12596
|
if (options.pdfPassword) {
|
|
12204
12597
|
await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
|
|
12205
12598
|
}
|
|
12206
|
-
const finalSizeMb = fmtMb((await
|
|
12599
|
+
const finalSizeMb = fmtMb((await fs12.promises.stat(outputPath)).size);
|
|
12207
12600
|
logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
|
|
12208
12601
|
outputPaths.push(outputPath);
|
|
12209
12602
|
}
|
|
@@ -12216,8 +12609,8 @@ var PdfGenerator = class {
|
|
|
12216
12609
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
12217
12610
|
return [...new Set(options.templatePath)].map((p) => ({
|
|
12218
12611
|
kind: "custom",
|
|
12219
|
-
absolutePath:
|
|
12220
|
-
label:
|
|
12612
|
+
absolutePath: path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p),
|
|
12613
|
+
label: path12.basename(p, ".hbs")
|
|
12221
12614
|
}));
|
|
12222
12615
|
}
|
|
12223
12616
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
@@ -12267,7 +12660,7 @@ var PdfGenerator = class {
|
|
|
12267
12660
|
...data,
|
|
12268
12661
|
failures: paginated.inline,
|
|
12269
12662
|
overflowFailureCount: paginated.overflowCount,
|
|
12270
|
-
overflowSidecarName: paginated.sidecarPath ?
|
|
12663
|
+
overflowSidecarName: paginated.sidecarPath ? path12.basename(paginated.sidecarPath) : void 0
|
|
12271
12664
|
};
|
|
12272
12665
|
for (const f of renderData.failures) f.screenshotBase64 = void 0;
|
|
12273
12666
|
if (options.includeScreenshots !== false) {
|
|
@@ -12287,7 +12680,7 @@ var PdfGenerator = class {
|
|
|
12287
12680
|
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
12288
12681
|
const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
|
|
12289
12682
|
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
12290
|
-
logger.info(`Generating PDF \u2192 ${
|
|
12683
|
+
logger.info(`Generating PDF \u2192 ${path12.relative(process.cwd(), outputPath)}`);
|
|
12291
12684
|
await page.pdf({
|
|
12292
12685
|
path: outputPath,
|
|
12293
12686
|
format: "A4",
|
|
@@ -12304,21 +12697,19 @@ function fmtMb(bytes) {
|
|
|
12304
12697
|
}
|
|
12305
12698
|
|
|
12306
12699
|
// src/reporter.ts
|
|
12307
|
-
import
|
|
12308
|
-
import crypto3 from "crypto";
|
|
12309
|
-
import os6 from "os";
|
|
12700
|
+
import path14 from "path";
|
|
12310
12701
|
|
|
12311
12702
|
// src/history/HistoryManager.ts
|
|
12312
12703
|
init_esm_shims();
|
|
12313
|
-
import
|
|
12314
|
-
import
|
|
12704
|
+
import fs13 from "fs";
|
|
12705
|
+
import path13 from "path";
|
|
12315
12706
|
var HistoryManager = class {
|
|
12316
12707
|
constructor(filePath) {
|
|
12317
12708
|
this.filePath = filePath;
|
|
12318
12709
|
}
|
|
12319
12710
|
read() {
|
|
12320
12711
|
try {
|
|
12321
|
-
const raw =
|
|
12712
|
+
const raw = fs13.readFileSync(this.filePath, "utf8");
|
|
12322
12713
|
const parsed = JSON.parse(raw);
|
|
12323
12714
|
if (parsed.version !== 1) {
|
|
12324
12715
|
logger.warn(`history.version-mismatch: path=${this.filePath} version=${String(parsed.version)}`);
|
|
@@ -12332,12 +12723,12 @@ var HistoryManager = class {
|
|
|
12332
12723
|
}
|
|
12333
12724
|
append(entry, maxSize) {
|
|
12334
12725
|
try {
|
|
12335
|
-
|
|
12726
|
+
fs13.mkdirSync(path13.dirname(this.filePath), { recursive: true });
|
|
12336
12727
|
const next = {
|
|
12337
12728
|
version: 1,
|
|
12338
12729
|
entries: [entry, ...this.read()].slice(0, maxSize)
|
|
12339
12730
|
};
|
|
12340
|
-
|
|
12731
|
+
fs13.writeFileSync(this.filePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
12341
12732
|
return next.entries;
|
|
12342
12733
|
} catch (err) {
|
|
12343
12734
|
logger.warn(`history.write-failed: path=${this.filePath} error=${err.message}`);
|
|
@@ -12678,11 +13069,13 @@ var PdfReporter = class {
|
|
|
12678
13069
|
this.dataCollector = new DataCollector();
|
|
12679
13070
|
this.pdfGenerator = new PdfGenerator();
|
|
12680
13071
|
this.options = parseOptions(rawOptions);
|
|
12681
|
-
const version = true ? "0.
|
|
13072
|
+
const version = true ? "0.4.1" : "0.x";
|
|
12682
13073
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
12683
13074
|
this.licenseClient = new LicenseClient({
|
|
12684
13075
|
licenseKey: this.options.licenseKey,
|
|
12685
|
-
serverUrl: this.options.serverUrl
|
|
13076
|
+
serverUrl: this.options.serverUrl,
|
|
13077
|
+
autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
|
|
13078
|
+
currentModelVersion: MODEL.version
|
|
12686
13079
|
});
|
|
12687
13080
|
}
|
|
12688
13081
|
onBegin(config, suite) {
|
|
@@ -12704,6 +13097,7 @@ var PdfReporter = class {
|
|
|
12704
13097
|
}
|
|
12705
13098
|
}
|
|
12706
13099
|
const reportData = this._buildReportData(collected, licenseInfo);
|
|
13100
|
+
runFailureAnalysis(reportData, this.options, this.cwd);
|
|
12707
13101
|
let pdfPaths = [];
|
|
12708
13102
|
try {
|
|
12709
13103
|
pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
|
|
@@ -12810,7 +13204,7 @@ var PdfReporter = class {
|
|
|
12810
13204
|
}
|
|
12811
13205
|
resolveProjectName() {
|
|
12812
13206
|
try {
|
|
12813
|
-
const pkgPath =
|
|
13207
|
+
const pkgPath = path14.resolve(process.cwd(), "package.json");
|
|
12814
13208
|
const pkg = __require(pkgPath);
|
|
12815
13209
|
return pkg.name ?? "Playwright Tests";
|
|
12816
13210
|
} catch {
|
|
@@ -12860,10 +13254,9 @@ function deriveVerdict(stats) {
|
|
|
12860
13254
|
}
|
|
12861
13255
|
function resolveHistoryPath(options, cwd) {
|
|
12862
13256
|
if (options.historyFile) {
|
|
12863
|
-
return
|
|
13257
|
+
return path14.isAbsolute(options.historyFile) ? options.historyFile : path14.resolve(cwd, options.historyFile);
|
|
12864
13258
|
}
|
|
12865
|
-
|
|
12866
|
-
return path11.join(os6.homedir(), ".reportforge", projectKey, "history.json");
|
|
13259
|
+
return path14.join(resolveProjectDir(options, cwd), "history.json");
|
|
12867
13260
|
}
|
|
12868
13261
|
function flattenSuiteNodes(suites) {
|
|
12869
13262
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|