@reportforge/playwright-pdf 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +6 -0
- package/dist/cli/export-feedback.js +87 -0
- package/dist/demo/cli.js +11912 -0
- package/dist/index.d.mts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +503 -116
- package/dist/index.mjs +580 -193
- 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 +10 -2
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,10 +10838,14 @@ 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",
|
|
10743
10847
|
key: normalizedKey,
|
|
10848
|
+
jwt: result.jwt,
|
|
10744
10849
|
expiry: new Date(payload.exp * 1e3),
|
|
10745
10850
|
machinesUsed: result.machinesUsed,
|
|
10746
10851
|
machineLimit: result.machineLimit
|
|
@@ -10772,13 +10877,15 @@ var LicenseClient = class {
|
|
|
10772
10877
|
return this.post("/api/license/activate", {
|
|
10773
10878
|
licenseKey,
|
|
10774
10879
|
fingerprint: this.fingerprint.hash,
|
|
10775
|
-
label: this.fingerprint.label
|
|
10880
|
+
label: this.fingerprint.label,
|
|
10881
|
+
modelVersion: this.modelVersionForServer
|
|
10776
10882
|
});
|
|
10777
10883
|
}
|
|
10778
10884
|
async refresh(jwt) {
|
|
10779
10885
|
return this.post("/api/license/refresh", {
|
|
10780
10886
|
jwt,
|
|
10781
|
-
label: this.fingerprint.label
|
|
10887
|
+
label: this.fingerprint.label,
|
|
10888
|
+
modelVersion: this.modelVersionForServer
|
|
10782
10889
|
});
|
|
10783
10890
|
}
|
|
10784
10891
|
async post(route, body) {
|
|
@@ -10823,6 +10930,7 @@ function toLicenseInfo(payload, cached, normalizedKey, source) {
|
|
|
10823
10930
|
plan: "subscription",
|
|
10824
10931
|
source,
|
|
10825
10932
|
key: normalizedKey,
|
|
10933
|
+
jwt: cached.jwt,
|
|
10826
10934
|
expiry: new Date(payload.exp * 1e3),
|
|
10827
10935
|
// Surface the snapshot from the last activate/refresh. Undefined for
|
|
10828
10936
|
// caches written by older reporter versions before these fields were
|
|
@@ -10835,11 +10943,307 @@ function normalizeUrl(url) {
|
|
|
10835
10943
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
10836
10944
|
}
|
|
10837
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
|
+
|
|
10838
11216
|
// src/collector/DataCollector.ts
|
|
10839
11217
|
init_esm_shims();
|
|
10840
11218
|
|
|
10841
11219
|
// src/collector/SuiteWalker.ts
|
|
10842
11220
|
init_esm_shims();
|
|
11221
|
+
|
|
11222
|
+
// src/collector/stats-utils.ts
|
|
11223
|
+
init_esm_shims();
|
|
11224
|
+
function statsFromTests(tests) {
|
|
11225
|
+
return {
|
|
11226
|
+
total: tests.length,
|
|
11227
|
+
passed: tests.filter((t) => t.status === "passed").length,
|
|
11228
|
+
failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
|
|
11229
|
+
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11230
|
+
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11231
|
+
};
|
|
11232
|
+
}
|
|
11233
|
+
function aggregateStats(parts) {
|
|
11234
|
+
return parts.reduce(
|
|
11235
|
+
(acc, s) => ({
|
|
11236
|
+
total: acc.total + s.total,
|
|
11237
|
+
passed: acc.passed + s.passed,
|
|
11238
|
+
failed: acc.failed + s.failed,
|
|
11239
|
+
skipped: acc.skipped + s.skipped,
|
|
11240
|
+
flaky: acc.flaky + s.flaky
|
|
11241
|
+
}),
|
|
11242
|
+
{ total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
|
|
11243
|
+
);
|
|
11244
|
+
}
|
|
11245
|
+
|
|
11246
|
+
// src/collector/SuiteWalker.ts
|
|
10843
11247
|
var SuiteWalker = class {
|
|
10844
11248
|
build(rootSuite, resultMap) {
|
|
10845
11249
|
return rootSuite.suites.map(
|
|
@@ -10850,15 +11254,15 @@ var SuiteWalker = class {
|
|
|
10850
11254
|
const suites = projectSuite.suites.map(
|
|
10851
11255
|
(s) => this.buildSuite(s, resultMap)
|
|
10852
11256
|
);
|
|
10853
|
-
const stats =
|
|
11257
|
+
const stats = aggregateStats(suites.map((s) => s.stats));
|
|
10854
11258
|
return { name: projectSuite.title || "default", suites, stats };
|
|
10855
11259
|
}
|
|
10856
11260
|
buildSuite(suite, resultMap) {
|
|
10857
11261
|
const childSuites = suite.suites.map((s) => this.buildSuite(s, resultMap));
|
|
10858
11262
|
const tests = suite.tests.map((t) => this.buildTest(t, resultMap));
|
|
10859
|
-
const stats =
|
|
11263
|
+
const stats = aggregateStats([
|
|
10860
11264
|
...childSuites.map((s) => s.stats),
|
|
10861
|
-
|
|
11265
|
+
statsFromTests(tests)
|
|
10862
11266
|
]);
|
|
10863
11267
|
return {
|
|
10864
11268
|
title: suite.title,
|
|
@@ -10895,27 +11299,6 @@ var SuiteWalker = class {
|
|
|
10895
11299
|
if (actual === "failed" && expected === "failed") return "passed";
|
|
10896
11300
|
return "failed";
|
|
10897
11301
|
}
|
|
10898
|
-
statsFromTests(tests) {
|
|
10899
|
-
return {
|
|
10900
|
-
total: tests.length,
|
|
10901
|
-
passed: tests.filter((t) => t.status === "passed").length,
|
|
10902
|
-
failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
|
|
10903
|
-
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
10904
|
-
flaky: tests.filter((t) => t.status === "flaky").length
|
|
10905
|
-
};
|
|
10906
|
-
}
|
|
10907
|
-
aggregateStats(parts) {
|
|
10908
|
-
return parts.reduce(
|
|
10909
|
-
(acc, s) => ({
|
|
10910
|
-
total: acc.total + s.total,
|
|
10911
|
-
passed: acc.passed + s.passed,
|
|
10912
|
-
failed: acc.failed + s.failed,
|
|
10913
|
-
skipped: acc.skipped + s.skipped,
|
|
10914
|
-
flaky: acc.flaky + s.flaky
|
|
10915
|
-
}),
|
|
10916
|
-
{ total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
|
|
10917
|
-
);
|
|
10918
|
-
}
|
|
10919
11302
|
};
|
|
10920
11303
|
|
|
10921
11304
|
// src/utils/env.ts
|
|
@@ -11251,7 +11634,7 @@ var ShardMerger = class {
|
|
|
11251
11634
|
}
|
|
11252
11635
|
return Array.from(projectMap.entries()).map(([name, projectSuites]) => {
|
|
11253
11636
|
const suites = this.mergeFileSuites(projectSuites);
|
|
11254
|
-
const stats =
|
|
11637
|
+
const stats = aggregateStats(suites.map((s) => s.stats));
|
|
11255
11638
|
return { name, suites, stats };
|
|
11256
11639
|
});
|
|
11257
11640
|
}
|
|
@@ -11272,9 +11655,9 @@ var ShardMerger = class {
|
|
|
11272
11655
|
const suitePath = [ref.title ?? ""];
|
|
11273
11656
|
const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
|
|
11274
11657
|
const childSuiteNodes = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
|
|
11275
|
-
const stats =
|
|
11658
|
+
const stats = aggregateStats([
|
|
11276
11659
|
...childSuiteNodes.map((s) => s.stats),
|
|
11277
|
-
|
|
11660
|
+
statsFromTests(tests)
|
|
11278
11661
|
]);
|
|
11279
11662
|
return {
|
|
11280
11663
|
title: ref.title ?? ref.file ?? "unknown",
|
|
@@ -11291,9 +11674,9 @@ var ShardMerger = class {
|
|
|
11291
11674
|
const suitePath = [...parentPath, suite.title];
|
|
11292
11675
|
const tests = suite.specs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
|
|
11293
11676
|
const childSuites = this.buildDescribeSuites(suite.suites ?? [], projectName, suitePath);
|
|
11294
|
-
const stats =
|
|
11677
|
+
const stats = aggregateStats([
|
|
11295
11678
|
...childSuites.map((s) => s.stats),
|
|
11296
|
-
|
|
11679
|
+
statsFromTests(tests)
|
|
11297
11680
|
]);
|
|
11298
11681
|
return {
|
|
11299
11682
|
title: suite.title,
|
|
@@ -11448,39 +11831,18 @@ var ShardMerger = class {
|
|
|
11448
11831
|
suiteResults
|
|
11449
11832
|
};
|
|
11450
11833
|
}
|
|
11451
|
-
statsFromTests(tests) {
|
|
11452
|
-
return {
|
|
11453
|
-
total: tests.length,
|
|
11454
|
-
passed: tests.filter((t) => t.status === "passed").length,
|
|
11455
|
-
failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
|
|
11456
|
-
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11457
|
-
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11458
|
-
};
|
|
11459
|
-
}
|
|
11460
|
-
aggregateStats(parts) {
|
|
11461
|
-
return parts.reduce(
|
|
11462
|
-
(acc, s) => ({
|
|
11463
|
-
total: acc.total + s.total,
|
|
11464
|
-
passed: acc.passed + s.passed,
|
|
11465
|
-
failed: acc.failed + s.failed,
|
|
11466
|
-
skipped: acc.skipped + s.skipped,
|
|
11467
|
-
flaky: acc.flaky + s.flaky
|
|
11468
|
-
}),
|
|
11469
|
-
{ total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
|
|
11470
|
-
);
|
|
11471
|
-
}
|
|
11472
11834
|
};
|
|
11473
11835
|
|
|
11474
11836
|
// src/pdf/PdfGenerator.ts
|
|
11475
11837
|
init_esm_shims();
|
|
11476
|
-
import
|
|
11477
|
-
import
|
|
11838
|
+
import fs12 from "fs";
|
|
11839
|
+
import path12 from "path";
|
|
11478
11840
|
|
|
11479
11841
|
// src/templates/engine.ts
|
|
11480
11842
|
init_esm_shims();
|
|
11481
11843
|
var import_handlebars = __toESM(require_lib());
|
|
11482
|
-
import
|
|
11483
|
-
import
|
|
11844
|
+
import fs5 from "fs";
|
|
11845
|
+
import path7 from "path";
|
|
11484
11846
|
|
|
11485
11847
|
// src/utils/duration.ts
|
|
11486
11848
|
init_esm_shims();
|
|
@@ -11500,9 +11862,9 @@ var TemplateEngine = class {
|
|
|
11500
11862
|
this.fontsBundleCss = "";
|
|
11501
11863
|
this.customTemplateCache = /* @__PURE__ */ new Map();
|
|
11502
11864
|
this.handlebars = import_handlebars.default.create();
|
|
11503
|
-
this.templatesDir =
|
|
11504
|
-
if (!
|
|
11505
|
-
this.templatesDir =
|
|
11865
|
+
this.templatesDir = path7.resolve(__dirname, "templates");
|
|
11866
|
+
if (!fs5.existsSync(this.templatesDir)) {
|
|
11867
|
+
this.templatesDir = path7.resolve(__dirname, "../templates");
|
|
11506
11868
|
}
|
|
11507
11869
|
this.registerHelpers();
|
|
11508
11870
|
this.registerPartials();
|
|
@@ -11519,21 +11881,21 @@ var TemplateEngine = class {
|
|
|
11519
11881
|
this.chartjsInline = inline;
|
|
11520
11882
|
}
|
|
11521
11883
|
render(data, template) {
|
|
11522
|
-
const layoutPath =
|
|
11523
|
-
if (!
|
|
11884
|
+
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
11885
|
+
if (!fs5.existsSync(layoutPath)) {
|
|
11524
11886
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
11525
11887
|
}
|
|
11526
|
-
const layoutSource =
|
|
11888
|
+
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
11527
11889
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
11528
11890
|
const ctx = this.buildContext(data, template);
|
|
11529
11891
|
return compiledLayout(ctx);
|
|
11530
11892
|
}
|
|
11531
11893
|
renderFromPath(data, filePath) {
|
|
11532
11894
|
if (!this.customTemplateCache.has(filePath)) {
|
|
11533
|
-
if (!
|
|
11895
|
+
if (!fs5.existsSync(filePath)) {
|
|
11534
11896
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
11535
11897
|
}
|
|
11536
|
-
const source =
|
|
11898
|
+
const source = fs5.readFileSync(filePath, "utf8");
|
|
11537
11899
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
11538
11900
|
}
|
|
11539
11901
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
@@ -11646,17 +12008,17 @@ var TemplateEngine = class {
|
|
|
11646
12008
|
return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
11647
12009
|
}
|
|
11648
12010
|
readStyle(filename) {
|
|
11649
|
-
const stylePath =
|
|
11650
|
-
if (!
|
|
11651
|
-
return
|
|
12011
|
+
const stylePath = path7.join(this.templatesDir, "styles", filename);
|
|
12012
|
+
if (!fs5.existsSync(stylePath)) return "";
|
|
12013
|
+
return fs5.readFileSync(stylePath, "utf8");
|
|
11652
12014
|
}
|
|
11653
12015
|
registerPartials() {
|
|
11654
|
-
const partialsDir =
|
|
11655
|
-
if (!
|
|
11656
|
-
for (const file of
|
|
12016
|
+
const partialsDir = path7.join(this.templatesDir, "partials");
|
|
12017
|
+
if (!fs5.existsSync(partialsDir)) return;
|
|
12018
|
+
for (const file of fs5.readdirSync(partialsDir)) {
|
|
11657
12019
|
if (!file.endsWith(".hbs")) continue;
|
|
11658
|
-
const name =
|
|
11659
|
-
const src =
|
|
12020
|
+
const name = path7.basename(file, ".hbs");
|
|
12021
|
+
const src = fs5.readFileSync(path7.join(partialsDir, file), "utf8");
|
|
11660
12022
|
this.handlebars.registerPartial(name, src);
|
|
11661
12023
|
}
|
|
11662
12024
|
}
|
|
@@ -11719,6 +12081,10 @@ var TemplateEngine = class {
|
|
|
11719
12081
|
);
|
|
11720
12082
|
this.handlebars.registerHelper("lt", (a, b) => a < b);
|
|
11721
12083
|
this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
|
|
12084
|
+
this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
|
|
12085
|
+
this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
|
|
12086
|
+
this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
|
|
12087
|
+
this.handlebars.registerHelper("subtract", (a, b) => (a ?? 0) - (b ?? 0));
|
|
11722
12088
|
this.handlebars.registerHelper("statusBadge", (status) => {
|
|
11723
12089
|
const map = {
|
|
11724
12090
|
passed: "badge--passed",
|
|
@@ -11740,7 +12106,7 @@ var TemplateEngine = class {
|
|
|
11740
12106
|
|
|
11741
12107
|
// src/screenshots/ScreenshotEmbedder.ts
|
|
11742
12108
|
init_esm_shims();
|
|
11743
|
-
import
|
|
12109
|
+
import fs6 from "fs";
|
|
11744
12110
|
import sharp from "sharp";
|
|
11745
12111
|
var ScreenshotEmbedder = class {
|
|
11746
12112
|
async embedAll(failures, opts) {
|
|
@@ -11749,7 +12115,7 @@ var ScreenshotEmbedder = class {
|
|
|
11749
12115
|
async embed(failure, opts) {
|
|
11750
12116
|
if (!failure.screenshotPath && !failure.screenshotBuffer) return;
|
|
11751
12117
|
try {
|
|
11752
|
-
const raw = failure.screenshotBuffer ?? await
|
|
12118
|
+
const raw = failure.screenshotBuffer ?? await fs6.promises.readFile(failure.screenshotPath);
|
|
11753
12119
|
const { buffer, mime } = opts.reencode ? await this.recompress(raw, opts) : { buffer: raw, mime: this.detectMimeType(raw) };
|
|
11754
12120
|
failure.screenshotBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
11755
12121
|
} catch (e) {
|
|
@@ -11761,7 +12127,7 @@ var ScreenshotEmbedder = class {
|
|
|
11761
12127
|
}
|
|
11762
12128
|
async embedLogoFile(logoPath) {
|
|
11763
12129
|
try {
|
|
11764
|
-
const buffer = await
|
|
12130
|
+
const buffer = await fs6.promises.readFile(logoPath);
|
|
11765
12131
|
const ext = logoPath.split(".").pop()?.toLowerCase();
|
|
11766
12132
|
let mime = "image/png";
|
|
11767
12133
|
if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
|
|
@@ -11806,7 +12172,7 @@ var ScreenshotEmbedder = class {
|
|
|
11806
12172
|
// src/pdf/BrowserManager.ts
|
|
11807
12173
|
init_esm_shims();
|
|
11808
12174
|
import { execSync as execSync2 } from "child_process";
|
|
11809
|
-
import
|
|
12175
|
+
import fs7 from "fs";
|
|
11810
12176
|
var BrowserManager = class {
|
|
11811
12177
|
async launch(executablePath) {
|
|
11812
12178
|
const puppeteer = __require("puppeteer-core");
|
|
@@ -11834,13 +12200,13 @@ var BrowserManager = class {
|
|
|
11834
12200
|
}
|
|
11835
12201
|
}
|
|
11836
12202
|
resolveChromePath(explicit, puppeteer) {
|
|
11837
|
-
if (explicit &&
|
|
12203
|
+
if (explicit && fs7.existsSync(explicit)) return explicit;
|
|
11838
12204
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
11839
|
-
if (envPath &&
|
|
12205
|
+
if (envPath && fs7.existsSync(envPath)) return envPath;
|
|
11840
12206
|
try {
|
|
11841
12207
|
const chromeFinder = require_lib2();
|
|
11842
12208
|
const found = chromeFinder();
|
|
11843
|
-
if (found &&
|
|
12209
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11844
12210
|
} catch {
|
|
11845
12211
|
}
|
|
11846
12212
|
const linuxPaths = [
|
|
@@ -11851,19 +12217,19 @@ var BrowserManager = class {
|
|
|
11851
12217
|
"/snap/bin/chromium"
|
|
11852
12218
|
];
|
|
11853
12219
|
for (const p of linuxPaths) {
|
|
11854
|
-
if (
|
|
12220
|
+
if (fs7.existsSync(p)) return p;
|
|
11855
12221
|
}
|
|
11856
12222
|
try {
|
|
11857
12223
|
const found = execSync2("which google-chrome chromium chromium-browser 2>/dev/null", {
|
|
11858
12224
|
stdio: "pipe"
|
|
11859
12225
|
}).toString().trim().split("\n")[0];
|
|
11860
|
-
if (found &&
|
|
12226
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11861
12227
|
} catch {
|
|
11862
12228
|
}
|
|
11863
12229
|
if (puppeteer.executablePath) {
|
|
11864
12230
|
try {
|
|
11865
12231
|
const bundled = puppeteer.executablePath();
|
|
11866
|
-
if (bundled &&
|
|
12232
|
+
if (bundled && fs7.existsSync(bundled)) return bundled;
|
|
11867
12233
|
} catch {
|
|
11868
12234
|
}
|
|
11869
12235
|
}
|
|
@@ -11876,21 +12242,21 @@ var BrowserManager = class {
|
|
|
11876
12242
|
|
|
11877
12243
|
// src/pdf/HtmlWriter.ts
|
|
11878
12244
|
init_esm_shims();
|
|
11879
|
-
import
|
|
11880
|
-
import
|
|
11881
|
-
import
|
|
11882
|
-
import
|
|
12245
|
+
import fs8 from "fs";
|
|
12246
|
+
import os7 from "os";
|
|
12247
|
+
import path8 from "path";
|
|
12248
|
+
import crypto2 from "crypto";
|
|
11883
12249
|
var HtmlWriter = class {
|
|
11884
12250
|
async write(html) {
|
|
11885
|
-
const tmpDir =
|
|
11886
|
-
const filename = `rf-report-${
|
|
11887
|
-
this.tempPath =
|
|
11888
|
-
await
|
|
12251
|
+
const tmpDir = os7.tmpdir();
|
|
12252
|
+
const filename = `rf-report-${crypto2.randomBytes(8).toString("hex")}.html`;
|
|
12253
|
+
this.tempPath = path8.join(tmpDir, filename);
|
|
12254
|
+
await fs8.promises.writeFile(this.tempPath, html, "utf8");
|
|
11889
12255
|
return this.tempPath;
|
|
11890
12256
|
}
|
|
11891
12257
|
async cleanup() {
|
|
11892
12258
|
if (this.tempPath) {
|
|
11893
|
-
await
|
|
12259
|
+
await fs8.promises.unlink(this.tempPath).catch(() => {
|
|
11894
12260
|
});
|
|
11895
12261
|
this.tempPath = void 0;
|
|
11896
12262
|
}
|
|
@@ -11924,10 +12290,10 @@ var ChartRenderer = class {
|
|
|
11924
12290
|
// src/pdf/PdfEncryptor.ts
|
|
11925
12291
|
init_esm_shims();
|
|
11926
12292
|
import { execFileSync } from "child_process";
|
|
11927
|
-
import
|
|
11928
|
-
import
|
|
11929
|
-
import
|
|
11930
|
-
import
|
|
12293
|
+
import fs9 from "fs";
|
|
12294
|
+
import path9 from "path";
|
|
12295
|
+
import os8 from "os";
|
|
12296
|
+
import crypto3 from "crypto";
|
|
11931
12297
|
var PdfEncryptor = class {
|
|
11932
12298
|
async encrypt(pdfPath, password) {
|
|
11933
12299
|
if (!password) return;
|
|
@@ -11937,10 +12303,10 @@ var PdfEncryptor = class {
|
|
|
11937
12303
|
);
|
|
11938
12304
|
return;
|
|
11939
12305
|
}
|
|
11940
|
-
const tmpPath =
|
|
11941
|
-
const pwFile =
|
|
12306
|
+
const tmpPath = path9.join(os8.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12307
|
+
const pwFile = path9.join(os8.tmpdir(), `rf-pw-${crypto3.randomBytes(8).toString("hex")}`);
|
|
11942
12308
|
try {
|
|
11943
|
-
|
|
12309
|
+
fs9.writeFileSync(pwFile, `${password}
|
|
11944
12310
|
${password}`, { mode: 384 });
|
|
11945
12311
|
execFileSync("qpdf", [
|
|
11946
12312
|
`--password-file=${pwFile}`,
|
|
@@ -11950,13 +12316,13 @@ ${password}`, { mode: 384 });
|
|
|
11950
12316
|
pdfPath,
|
|
11951
12317
|
tmpPath
|
|
11952
12318
|
], { stdio: "pipe" });
|
|
11953
|
-
|
|
12319
|
+
fs9.renameSync(tmpPath, pdfPath);
|
|
11954
12320
|
logger.info(`PDF encrypted with password.`);
|
|
11955
12321
|
} catch (e) {
|
|
11956
12322
|
logger.warn(`qpdf encryption failed: ${e.message}`);
|
|
11957
12323
|
} finally {
|
|
11958
|
-
if (
|
|
11959
|
-
|
|
12324
|
+
if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
|
|
12325
|
+
fs9.rmSync(pwFile, { force: true });
|
|
11960
12326
|
}
|
|
11961
12327
|
}
|
|
11962
12328
|
isQpdfAvailable() {
|
|
@@ -11971,8 +12337,8 @@ ${password}`, { mode: 384 });
|
|
|
11971
12337
|
|
|
11972
12338
|
// src/filename/FilenameResolver.ts
|
|
11973
12339
|
init_esm_shims();
|
|
11974
|
-
import
|
|
11975
|
-
import
|
|
12340
|
+
import path10 from "path";
|
|
12341
|
+
import fs10 from "fs";
|
|
11976
12342
|
var MAX_BRANCH_LENGTH = 50;
|
|
11977
12343
|
var FilenameResolver = class {
|
|
11978
12344
|
/**
|
|
@@ -11989,8 +12355,8 @@ var FilenameResolver = class {
|
|
|
11989
12355
|
const branch = this.sanitiseBranch(ctx.branch);
|
|
11990
12356
|
const status = ctx.status;
|
|
11991
12357
|
const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
|
|
11992
|
-
const absolute =
|
|
11993
|
-
|
|
12358
|
+
const absolute = path10.isAbsolute(resolved) ? resolved : path10.resolve(process.cwd(), resolved);
|
|
12359
|
+
fs10.mkdirSync(path10.dirname(absolute), { recursive: true });
|
|
11994
12360
|
return absolute;
|
|
11995
12361
|
}
|
|
11996
12362
|
/**
|
|
@@ -12002,7 +12368,7 @@ var FilenameResolver = class {
|
|
|
12002
12368
|
* // → '/out/2026-report-executive.pdf'
|
|
12003
12369
|
*/
|
|
12004
12370
|
injectTemplateSuffix(outputPath, suffix) {
|
|
12005
|
-
const ext =
|
|
12371
|
+
const ext = path10.extname(outputPath);
|
|
12006
12372
|
const base = outputPath.slice(0, outputPath.length - ext.length);
|
|
12007
12373
|
return `${base}-${suffix}${ext}`;
|
|
12008
12374
|
}
|
|
@@ -12060,8 +12426,8 @@ var PRESETS = {
|
|
|
12060
12426
|
|
|
12061
12427
|
// src/pdf/FailurePaginator.ts
|
|
12062
12428
|
init_esm_shims();
|
|
12063
|
-
import
|
|
12064
|
-
import
|
|
12429
|
+
import fs11 from "fs";
|
|
12430
|
+
import path11 from "path";
|
|
12065
12431
|
var FailurePaginator = class {
|
|
12066
12432
|
async paginate(failures, maxInline, pdfOutputPath) {
|
|
12067
12433
|
if (failures.length <= maxInline) {
|
|
@@ -12071,13 +12437,13 @@ var FailurePaginator = class {
|
|
|
12071
12437
|
const overflow = failures.slice(maxInline);
|
|
12072
12438
|
const sidecarPath = this.sidecarPathFor(pdfOutputPath);
|
|
12073
12439
|
try {
|
|
12074
|
-
await
|
|
12440
|
+
await fs11.promises.writeFile(
|
|
12075
12441
|
sidecarPath,
|
|
12076
12442
|
JSON.stringify(this.serialize(overflow), null, 2),
|
|
12077
12443
|
"utf8"
|
|
12078
12444
|
);
|
|
12079
12445
|
logger.info(
|
|
12080
|
-
`Emitted ${overflow.length} overflow failures \u2192 ${
|
|
12446
|
+
`Emitted ${overflow.length} overflow failures \u2192 ${path11.basename(sidecarPath)}`
|
|
12081
12447
|
);
|
|
12082
12448
|
} catch (e) {
|
|
12083
12449
|
logger.warn(`Could not write failure sidecar: ${e.message}`);
|
|
@@ -12086,9 +12452,9 @@ var FailurePaginator = class {
|
|
|
12086
12452
|
return { inline, overflowCount: overflow.length, sidecarPath };
|
|
12087
12453
|
}
|
|
12088
12454
|
sidecarPathFor(pdfPath) {
|
|
12089
|
-
const dir =
|
|
12090
|
-
const base =
|
|
12091
|
-
return
|
|
12455
|
+
const dir = path11.dirname(pdfPath);
|
|
12456
|
+
const base = path11.basename(pdfPath, path11.extname(pdfPath));
|
|
12457
|
+
return path11.join(dir, `${base}-failures.json`);
|
|
12092
12458
|
}
|
|
12093
12459
|
/**
|
|
12094
12460
|
* Drops the inlined image bytes — a 150-entry overflow would otherwise
|
|
@@ -12138,14 +12504,35 @@ var PdfGenerator = class {
|
|
|
12138
12504
|
*
|
|
12139
12505
|
* @param data - Assembled `ReportData` (projects, stats, failures, charts).
|
|
12140
12506
|
* @param options - Validated reporter options.
|
|
12141
|
-
* @param
|
|
12507
|
+
* @param licenseInfo - Verified license. JWT must be present and valid for
|
|
12508
|
+
* non-demo sources; `source: 'demo'` is the only allowed JWT-free path.
|
|
12142
12509
|
* @returns Absolute paths of written PDF files.
|
|
12143
12510
|
*/
|
|
12144
|
-
async generateAll(data, options,
|
|
12511
|
+
async generateAll(data, options, licenseInfo) {
|
|
12512
|
+
if (false) {
|
|
12513
|
+
if (licenseInfo.expiry <= /* @__PURE__ */ new Date()) {
|
|
12514
|
+
throw new Error("[reportforge] Demo license expired \u2014 PDF generation skipped.");
|
|
12515
|
+
}
|
|
12516
|
+
} else {
|
|
12517
|
+
if (!licenseInfo.jwt) {
|
|
12518
|
+
throw new Error("[reportforge] License JWT missing \u2014 PDF generation skipped.");
|
|
12519
|
+
}
|
|
12520
|
+
if (!LICENSE_PUBLIC_KEY_PEM) {
|
|
12521
|
+
throw new Error("[reportforge] LICENSE_PUBLIC_KEY_PEM missing \u2014 cannot verify license JWT.");
|
|
12522
|
+
}
|
|
12523
|
+
try {
|
|
12524
|
+
const payload = new JwtVerifier(LICENSE_PUBLIC_KEY_PEM).verify(licenseInfo.jwt);
|
|
12525
|
+
if (payload.exp * 1e3 <= Date.now()) {
|
|
12526
|
+
throw new Error("JWT expired");
|
|
12527
|
+
}
|
|
12528
|
+
} catch (err) {
|
|
12529
|
+
throw new Error(`[reportforge] License JWT verification failed: ${err.message}`);
|
|
12530
|
+
}
|
|
12531
|
+
}
|
|
12145
12532
|
const sources = this.resolveTemplateSources(options);
|
|
12146
12533
|
const isMulti = sources.length > 1;
|
|
12147
12534
|
for (const source of sources) {
|
|
12148
|
-
if (source.kind === "custom" && !
|
|
12535
|
+
if (source.kind === "custom" && !fs12.existsSync(source.absolutePath)) {
|
|
12149
12536
|
throw new Error(
|
|
12150
12537
|
`[reportforge] Custom template not found: ${source.absolutePath}`
|
|
12151
12538
|
);
|
|
@@ -12176,14 +12563,14 @@ var PdfGenerator = class {
|
|
|
12176
12563
|
await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
|
|
12177
12564
|
const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
|
|
12178
12565
|
if (capBytes > 0) {
|
|
12179
|
-
const sizeBytes = (await
|
|
12566
|
+
const sizeBytes = (await fs12.promises.stat(outputPath)).size;
|
|
12180
12567
|
if (sizeBytes > capBytes) {
|
|
12181
12568
|
const tighter = this.tightenForCap(compression, sizeBytes, capBytes);
|
|
12182
12569
|
logger.warn(
|
|
12183
12570
|
`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}).`
|
|
12184
12571
|
);
|
|
12185
12572
|
await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
|
|
12186
|
-
const finalSize = (await
|
|
12573
|
+
const finalSize = (await fs12.promises.stat(outputPath)).size;
|
|
12187
12574
|
if (finalSize > capBytes) {
|
|
12188
12575
|
logger.warn(
|
|
12189
12576
|
`PDF still ${fmtMb(finalSize)} after retune. Consider lowering maxInlineFailures or splitting the report.`
|
|
@@ -12196,7 +12583,7 @@ var PdfGenerator = class {
|
|
|
12196
12583
|
if (options.pdfPassword) {
|
|
12197
12584
|
await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
|
|
12198
12585
|
}
|
|
12199
|
-
const finalSizeMb = fmtMb((await
|
|
12586
|
+
const finalSizeMb = fmtMb((await fs12.promises.stat(outputPath)).size);
|
|
12200
12587
|
logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
|
|
12201
12588
|
outputPaths.push(outputPath);
|
|
12202
12589
|
}
|
|
@@ -12209,8 +12596,8 @@ var PdfGenerator = class {
|
|
|
12209
12596
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
12210
12597
|
return [...new Set(options.templatePath)].map((p) => ({
|
|
12211
12598
|
kind: "custom",
|
|
12212
|
-
absolutePath:
|
|
12213
|
-
label:
|
|
12599
|
+
absolutePath: path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p),
|
|
12600
|
+
label: path12.basename(p, ".hbs")
|
|
12214
12601
|
}));
|
|
12215
12602
|
}
|
|
12216
12603
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
@@ -12260,7 +12647,7 @@ var PdfGenerator = class {
|
|
|
12260
12647
|
...data,
|
|
12261
12648
|
failures: paginated.inline,
|
|
12262
12649
|
overflowFailureCount: paginated.overflowCount,
|
|
12263
|
-
overflowSidecarName: paginated.sidecarPath ?
|
|
12650
|
+
overflowSidecarName: paginated.sidecarPath ? path12.basename(paginated.sidecarPath) : void 0
|
|
12264
12651
|
};
|
|
12265
12652
|
for (const f of renderData.failures) f.screenshotBase64 = void 0;
|
|
12266
12653
|
if (options.includeScreenshots !== false) {
|
|
@@ -12280,7 +12667,7 @@ var PdfGenerator = class {
|
|
|
12280
12667
|
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
12281
12668
|
const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
|
|
12282
12669
|
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
12283
|
-
logger.info(`Generating PDF \u2192 ${
|
|
12670
|
+
logger.info(`Generating PDF \u2192 ${path12.relative(process.cwd(), outputPath)}`);
|
|
12284
12671
|
await page.pdf({
|
|
12285
12672
|
path: outputPath,
|
|
12286
12673
|
format: "A4",
|
|
@@ -12297,21 +12684,19 @@ function fmtMb(bytes) {
|
|
|
12297
12684
|
}
|
|
12298
12685
|
|
|
12299
12686
|
// src/reporter.ts
|
|
12300
|
-
import
|
|
12301
|
-
import crypto3 from "crypto";
|
|
12302
|
-
import os6 from "os";
|
|
12687
|
+
import path14 from "path";
|
|
12303
12688
|
|
|
12304
12689
|
// src/history/HistoryManager.ts
|
|
12305
12690
|
init_esm_shims();
|
|
12306
|
-
import
|
|
12307
|
-
import
|
|
12691
|
+
import fs13 from "fs";
|
|
12692
|
+
import path13 from "path";
|
|
12308
12693
|
var HistoryManager = class {
|
|
12309
12694
|
constructor(filePath) {
|
|
12310
12695
|
this.filePath = filePath;
|
|
12311
12696
|
}
|
|
12312
12697
|
read() {
|
|
12313
12698
|
try {
|
|
12314
|
-
const raw =
|
|
12699
|
+
const raw = fs13.readFileSync(this.filePath, "utf8");
|
|
12315
12700
|
const parsed = JSON.parse(raw);
|
|
12316
12701
|
if (parsed.version !== 1) {
|
|
12317
12702
|
logger.warn(`history.version-mismatch: path=${this.filePath} version=${String(parsed.version)}`);
|
|
@@ -12325,12 +12710,12 @@ var HistoryManager = class {
|
|
|
12325
12710
|
}
|
|
12326
12711
|
append(entry, maxSize) {
|
|
12327
12712
|
try {
|
|
12328
|
-
|
|
12713
|
+
fs13.mkdirSync(path13.dirname(this.filePath), { recursive: true });
|
|
12329
12714
|
const next = {
|
|
12330
12715
|
version: 1,
|
|
12331
12716
|
entries: [entry, ...this.read()].slice(0, maxSize)
|
|
12332
12717
|
};
|
|
12333
|
-
|
|
12718
|
+
fs13.writeFileSync(this.filePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
12334
12719
|
return next.entries;
|
|
12335
12720
|
} catch (err) {
|
|
12336
12721
|
logger.warn(`history.write-failed: path=${this.filePath} error=${err.message}`);
|
|
@@ -12671,11 +13056,13 @@ var PdfReporter = class {
|
|
|
12671
13056
|
this.dataCollector = new DataCollector();
|
|
12672
13057
|
this.pdfGenerator = new PdfGenerator();
|
|
12673
13058
|
this.options = parseOptions(rawOptions);
|
|
12674
|
-
const version = true ? "0.
|
|
13059
|
+
const version = true ? "0.4.0" : "0.x";
|
|
12675
13060
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
12676
13061
|
this.licenseClient = new LicenseClient({
|
|
12677
13062
|
licenseKey: this.options.licenseKey,
|
|
12678
|
-
serverUrl: this.options.serverUrl
|
|
13063
|
+
serverUrl: this.options.serverUrl,
|
|
13064
|
+
autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
|
|
13065
|
+
currentModelVersion: MODEL.version
|
|
12679
13066
|
});
|
|
12680
13067
|
}
|
|
12681
13068
|
onBegin(config, suite) {
|
|
@@ -12697,6 +13084,7 @@ var PdfReporter = class {
|
|
|
12697
13084
|
}
|
|
12698
13085
|
}
|
|
12699
13086
|
const reportData = this._buildReportData(collected, licenseInfo);
|
|
13087
|
+
runFailureAnalysis(reportData, this.options, this.cwd);
|
|
12700
13088
|
let pdfPaths = [];
|
|
12701
13089
|
try {
|
|
12702
13090
|
pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
|
|
@@ -12803,7 +13191,7 @@ var PdfReporter = class {
|
|
|
12803
13191
|
}
|
|
12804
13192
|
resolveProjectName() {
|
|
12805
13193
|
try {
|
|
12806
|
-
const pkgPath =
|
|
13194
|
+
const pkgPath = path14.resolve(process.cwd(), "package.json");
|
|
12807
13195
|
const pkg = __require(pkgPath);
|
|
12808
13196
|
return pkg.name ?? "Playwright Tests";
|
|
12809
13197
|
} catch {
|
|
@@ -12853,10 +13241,9 @@ function deriveVerdict(stats) {
|
|
|
12853
13241
|
}
|
|
12854
13242
|
function resolveHistoryPath(options, cwd) {
|
|
12855
13243
|
if (options.historyFile) {
|
|
12856
|
-
return
|
|
13244
|
+
return path14.isAbsolute(options.historyFile) ? options.historyFile : path14.resolve(cwd, options.historyFile);
|
|
12857
13245
|
}
|
|
12858
|
-
|
|
12859
|
-
return path11.join(os6.homedir(), ".reportforge", projectKey, "history.json");
|
|
13246
|
+
return path14.join(resolveProjectDir(options, cwd), "history.json");
|
|
12860
13247
|
}
|
|
12861
13248
|
function flattenSuiteNodes(suites) {
|
|
12862
13249
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|