@reportforge/playwright-pdf 0.4.1 → 0.5.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 +35 -0
- package/README.md +4 -0
- package/dist/demo/cli.js +50 -7
- package/dist/index.js +105 -47
- package/dist/index.mjs +105 -47
- package/dist/templates/layouts/detailed.hbs +5 -4
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/charts.hbs +30 -16
- package/dist/templates/partials/cover-page.hbs +1 -1
- package/dist/templates/partials/executive-summary.hbs +5 -1
- package/dist/templates/partials/failure-deep-dive.hbs +2 -2
- package/dist/templates/partials/requirements-matrix.hbs +1 -1
- package/dist/templates/partials/slow-tests.hbs +2 -2
- package/dist/templates/partials/suite-breakdown.hbs +2 -1
- package/dist/templates/styles/base.css +244 -6
- package/dist/templates/styles/detailed.css +6 -159
- package/dist/templates/styles/executive.css +3 -76
- package/dist/templates/styles/minimal.css +3 -3
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -10994,11 +10994,33 @@ var STRENGTH = {
|
|
|
10994
10994
|
strongMinMargin: 0.25
|
|
10995
10995
|
};
|
|
10996
10996
|
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
10997
|
-
var
|
|
10997
|
+
var OVERRIDE_MARGIN = 999;
|
|
10998
10998
|
|
|
10999
10999
|
// src/analysis/FailureClusterer.ts
|
|
11000
11000
|
var MAX_TESTS_PER_CLUSTER = 20;
|
|
11001
11001
|
var MAX_MESSAGE_CHARS = 200;
|
|
11002
|
+
function errorHeader(message) {
|
|
11003
|
+
const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
|
|
11004
|
+
return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
|
|
11005
|
+
}
|
|
11006
|
+
function isAssertionTimeout(message) {
|
|
11007
|
+
return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
|
|
11008
|
+
}
|
|
11009
|
+
function isNetwork(message) {
|
|
11010
|
+
return /net::ERR_(CONNECTION|NAME_NOT_RESOLVED|INTERNET_DISCONNECTED|SSL|TIMED_OUT|ADDRESS|NETWORK_CHANGED|EMPTY_RESPONSE)|\bECONN(?:REFUSED|RESET)\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|socket hang up|fetch failed/.test(errorHeader(message));
|
|
11011
|
+
}
|
|
11012
|
+
function isNavigation(message) {
|
|
11013
|
+
return /net::ERR_(?:ABORTED|TOO_MANY_REDIRECTS)|navigation interrupted|interrupted by (?:a|another) navigation|because of a navigation|no history entry|page crashed|Execution context was destroyed|frame was detached/.test(errorHeader(message));
|
|
11014
|
+
}
|
|
11015
|
+
function isLocatorNotFound(message) {
|
|
11016
|
+
return /strict mode violation|resolved to 0 element|element not found|not attached to the DOM|No node found for selector|no element matches|Unable to find an element/i.test(errorHeader(message));
|
|
11017
|
+
}
|
|
11018
|
+
var MESSAGE_RULES = [
|
|
11019
|
+
[isAssertionTimeout, "assertion"],
|
|
11020
|
+
[isNavigation, "navigation"],
|
|
11021
|
+
[isNetwork, "network"],
|
|
11022
|
+
[isLocatorNotFound, "locator-not-found"]
|
|
11023
|
+
];
|
|
11002
11024
|
function strengthOf(margin) {
|
|
11003
11025
|
if (margin >= STRENGTH.strongMinMargin) return "strong";
|
|
11004
11026
|
if (margin >= STRENGTH.moderateMinMargin) return "moderate";
|
|
@@ -11021,11 +11043,15 @@ function representativeMessage(message) {
|
|
|
11021
11043
|
return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
|
|
11022
11044
|
}
|
|
11023
11045
|
function classifyOne(f, model) {
|
|
11046
|
+
const frame = topFrame(f.error.stack);
|
|
11024
11047
|
if (f.retryHistory.some((r) => r.status === "passed")) {
|
|
11025
|
-
return { category: "flaky-by-retry", margin:
|
|
11048
|
+
return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
|
|
11049
|
+
}
|
|
11050
|
+
for (const [match, category] of MESSAGE_RULES) {
|
|
11051
|
+
if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
|
|
11026
11052
|
}
|
|
11027
11053
|
const { label, margin } = classify(model, classifyInput(f));
|
|
11028
|
-
return { category: label, margin, failure: f, frame
|
|
11054
|
+
return { category: label, margin, failure: f, frame };
|
|
11029
11055
|
}
|
|
11030
11056
|
function classifyAll(failures, model) {
|
|
11031
11057
|
return failures.map((f) => classifyOne(f, model));
|
|
@@ -11234,11 +11260,13 @@ init_esm_shims();
|
|
|
11234
11260
|
|
|
11235
11261
|
// src/collector/stats-utils.ts
|
|
11236
11262
|
init_esm_shims();
|
|
11263
|
+
var TOP_SUITES_FOR_CHART = 10;
|
|
11237
11264
|
function statsFromTests(tests) {
|
|
11238
11265
|
return {
|
|
11239
11266
|
total: tests.length,
|
|
11240
11267
|
passed: tests.filter((t) => t.status === "passed").length,
|
|
11241
|
-
failed: tests.filter((t) => t.status === "failed"
|
|
11268
|
+
failed: tests.filter((t) => t.status === "failed").length,
|
|
11269
|
+
timedOut: tests.filter((t) => t.status === "timedOut").length,
|
|
11242
11270
|
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11243
11271
|
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11244
11272
|
};
|
|
@@ -11249,12 +11277,32 @@ function aggregateStats(parts) {
|
|
|
11249
11277
|
total: acc.total + s.total,
|
|
11250
11278
|
passed: acc.passed + s.passed,
|
|
11251
11279
|
failed: acc.failed + s.failed,
|
|
11280
|
+
timedOut: acc.timedOut + s.timedOut,
|
|
11252
11281
|
skipped: acc.skipped + s.skipped,
|
|
11253
11282
|
flaky: acc.flaky + s.flaky
|
|
11254
11283
|
}),
|
|
11255
|
-
{ total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
|
|
11284
|
+
{ total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
|
|
11256
11285
|
);
|
|
11257
11286
|
}
|
|
11287
|
+
function toRow(s) {
|
|
11288
|
+
return {
|
|
11289
|
+
label: s.title,
|
|
11290
|
+
passed: s.stats.passed,
|
|
11291
|
+
failed: s.stats.failed,
|
|
11292
|
+
timedOut: s.stats.timedOut,
|
|
11293
|
+
skipped: s.stats.skipped
|
|
11294
|
+
};
|
|
11295
|
+
}
|
|
11296
|
+
function buildSuiteResults(projects, limit) {
|
|
11297
|
+
const top = projects.flatMap((p) => p.suites);
|
|
11298
|
+
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11299
|
+
const file = top[0];
|
|
11300
|
+
const fs14 = statsFromTests(file.tests);
|
|
11301
|
+
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
|
|
11302
|
+
return [...rootRow, ...file.suites.map(toRow)].slice(0, limit);
|
|
11303
|
+
}
|
|
11304
|
+
return top.map(toRow).slice(0, limit);
|
|
11305
|
+
}
|
|
11258
11306
|
|
|
11259
11307
|
// src/collector/SuiteWalker.ts
|
|
11260
11308
|
var SuiteWalker = class {
|
|
@@ -11314,6 +11362,18 @@ var SuiteWalker = class {
|
|
|
11314
11362
|
}
|
|
11315
11363
|
};
|
|
11316
11364
|
|
|
11365
|
+
// src/collector/severity.ts
|
|
11366
|
+
init_esm_shims();
|
|
11367
|
+
var SEVERITY_RANK = {
|
|
11368
|
+
critical: 0,
|
|
11369
|
+
high: 1,
|
|
11370
|
+
medium: 2,
|
|
11371
|
+
low: 3
|
|
11372
|
+
};
|
|
11373
|
+
function sortBySeverity(failures) {
|
|
11374
|
+
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
11375
|
+
}
|
|
11376
|
+
|
|
11317
11377
|
// src/utils/env.ts
|
|
11318
11378
|
init_esm_shims();
|
|
11319
11379
|
import { execSync } from "child_process";
|
|
@@ -11403,7 +11463,6 @@ function gitCommit() {
|
|
|
11403
11463
|
}
|
|
11404
11464
|
|
|
11405
11465
|
// src/collector/DataCollector.ts
|
|
11406
|
-
var TOP_SUITES_FOR_CHART = 10;
|
|
11407
11466
|
var DataCollector = class {
|
|
11408
11467
|
constructor() {
|
|
11409
11468
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
@@ -11440,13 +11499,13 @@ var DataCollector = class {
|
|
|
11440
11499
|
finalize(fullResult) {
|
|
11441
11500
|
const walker = new SuiteWalker();
|
|
11442
11501
|
const projects = walker.build(this.rootSuite, this.resultMap);
|
|
11443
|
-
const stats = this.computeStats(
|
|
11502
|
+
const stats = this.computeStats(fullResult);
|
|
11444
11503
|
const environment = this.buildEnvironment();
|
|
11445
11504
|
const charts = this.buildChartData(stats, projects);
|
|
11446
11505
|
return {
|
|
11447
11506
|
projects,
|
|
11448
11507
|
stats,
|
|
11449
|
-
failures:
|
|
11508
|
+
failures: sortBySeverity(this.failureRecords),
|
|
11450
11509
|
environment,
|
|
11451
11510
|
charts
|
|
11452
11511
|
};
|
|
@@ -11490,13 +11549,16 @@ var DataCollector = class {
|
|
|
11490
11549
|
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11491
11550
|
return "medium";
|
|
11492
11551
|
}
|
|
11493
|
-
computeStats(
|
|
11552
|
+
computeStats(fullResult) {
|
|
11494
11553
|
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11495
11554
|
for (const [, result] of this.resultMap) {
|
|
11496
11555
|
total++;
|
|
11497
11556
|
switch (result.status) {
|
|
11557
|
+
// A test that passes on a retry (retry > 0) is flaky, NOT a clean pass.
|
|
11558
|
+
// Counting it in `passed` too would double-count it against `flaky` and
|
|
11559
|
+
// push passRate over 100%.
|
|
11498
11560
|
case "passed":
|
|
11499
|
-
passed++;
|
|
11561
|
+
result.retry > 0 ? flaky++ : passed++;
|
|
11500
11562
|
break;
|
|
11501
11563
|
case "failed":
|
|
11502
11564
|
failed++;
|
|
@@ -11513,13 +11575,6 @@ var DataCollector = class {
|
|
|
11513
11575
|
break;
|
|
11514
11576
|
}
|
|
11515
11577
|
}
|
|
11516
|
-
for (const project of projects) {
|
|
11517
|
-
for (const suite of this.flattenSuites(project.suites)) {
|
|
11518
|
-
for (const test of suite.tests) {
|
|
11519
|
-
if (test.status === "flaky") flaky++;
|
|
11520
|
-
}
|
|
11521
|
-
}
|
|
11522
|
-
}
|
|
11523
11578
|
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
11524
11579
|
const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
|
|
11525
11580
|
return {
|
|
@@ -11565,27 +11620,18 @@ var DataCollector = class {
|
|
|
11565
11620
|
};
|
|
11566
11621
|
}
|
|
11567
11622
|
buildChartData(stats, projects) {
|
|
11568
|
-
const suiteResults = projects
|
|
11569
|
-
(p) => p.suites.map((s) => ({
|
|
11570
|
-
label: s.title,
|
|
11571
|
-
passed: s.stats.passed,
|
|
11572
|
-
failed: s.stats.failed,
|
|
11573
|
-
skipped: s.stats.skipped
|
|
11574
|
-
}))
|
|
11575
|
-
).slice(0, TOP_SUITES_FOR_CHART);
|
|
11623
|
+
const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
|
|
11576
11624
|
return {
|
|
11577
11625
|
passRate: {
|
|
11578
11626
|
passed: stats.passed,
|
|
11579
11627
|
failed: stats.failed,
|
|
11628
|
+
timedOut: stats.timedOut,
|
|
11580
11629
|
skipped: stats.skipped,
|
|
11581
11630
|
flaky: stats.flaky
|
|
11582
11631
|
},
|
|
11583
11632
|
suiteResults
|
|
11584
11633
|
};
|
|
11585
11634
|
}
|
|
11586
|
-
flattenSuites(suites) {
|
|
11587
|
-
return suites.flatMap((s) => [s, ...this.flattenSuites(s.suites)]);
|
|
11588
|
-
}
|
|
11589
11635
|
};
|
|
11590
11636
|
|
|
11591
11637
|
// src/collector/ShardMerger.ts
|
|
@@ -11593,15 +11639,14 @@ init_esm_shims();
|
|
|
11593
11639
|
import { readFileSync } from "fs";
|
|
11594
11640
|
import { globSync } from "glob";
|
|
11595
11641
|
import { createRequire } from "module";
|
|
11596
|
-
var TOP_SUITES_FOR_CHART2 = 10;
|
|
11597
11642
|
var ShardMerger = class {
|
|
11598
11643
|
merge(filePaths) {
|
|
11599
11644
|
if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
|
|
11600
11645
|
const resolved = this.resolveShardPaths(filePaths);
|
|
11601
11646
|
const reports = this.loadReports(resolved);
|
|
11602
11647
|
const projects = this.mergeProjects(reports);
|
|
11603
|
-
const stats = this.mergeStats(reports);
|
|
11604
|
-
const failures = this.extractFailures(reports);
|
|
11648
|
+
const stats = this.mergeStats(reports, projects);
|
|
11649
|
+
const failures = sortBySeverity(this.extractFailures(reports));
|
|
11605
11650
|
const environment = this.buildEnvironment(reports[0]);
|
|
11606
11651
|
const charts = this.buildCharts(stats, projects);
|
|
11607
11652
|
return { projects, stats, failures, environment, charts };
|
|
@@ -11708,7 +11753,7 @@ var ShardMerger = class {
|
|
|
11708
11753
|
id: spec.id,
|
|
11709
11754
|
title: spec.title,
|
|
11710
11755
|
fullTitle: [...suitePath, spec.title].join(" > "),
|
|
11711
|
-
status: this.resolveStatus(test),
|
|
11756
|
+
status: this.resolveStatus(test, lastResult),
|
|
11712
11757
|
duration: lastResult?.duration ?? 0,
|
|
11713
11758
|
retryCount: test ? test.results.length - 1 : 0,
|
|
11714
11759
|
tags: spec.tags ?? [],
|
|
@@ -11716,8 +11761,9 @@ var ShardMerger = class {
|
|
|
11716
11761
|
location: `${spec.file}:${spec.line}`
|
|
11717
11762
|
};
|
|
11718
11763
|
}
|
|
11719
|
-
resolveStatus(test) {
|
|
11764
|
+
resolveStatus(test, lastResult) {
|
|
11720
11765
|
if (!test) return "skipped";
|
|
11766
|
+
if (lastResult?.status === "timedOut") return "timedOut";
|
|
11721
11767
|
switch (test.status) {
|
|
11722
11768
|
case "expected":
|
|
11723
11769
|
return "passed";
|
|
@@ -11731,7 +11777,7 @@ var ShardMerger = class {
|
|
|
11731
11777
|
return "failed";
|
|
11732
11778
|
}
|
|
11733
11779
|
}
|
|
11734
|
-
mergeStats(reports) {
|
|
11780
|
+
mergeStats(reports, projects) {
|
|
11735
11781
|
let expected = 0, unexpected = 0, flaky = 0, skipped = 0, maxDuration = 0;
|
|
11736
11782
|
for (const r of reports) {
|
|
11737
11783
|
expected += r.stats.expected;
|
|
@@ -11742,13 +11788,14 @@ var ShardMerger = class {
|
|
|
11742
11788
|
}
|
|
11743
11789
|
const total = expected + unexpected + flaky + skipped;
|
|
11744
11790
|
const passRate = total > 0 ? Math.round((expected + flaky) / total * 100) : 0;
|
|
11791
|
+
const timedOut = projects.reduce((n, p) => n + p.stats.timedOut, 0);
|
|
11745
11792
|
return {
|
|
11746
11793
|
total,
|
|
11747
11794
|
passed: expected,
|
|
11748
|
-
failed: unexpected,
|
|
11795
|
+
failed: Math.max(0, unexpected - timedOut),
|
|
11749
11796
|
skipped,
|
|
11750
11797
|
flaky,
|
|
11751
|
-
timedOut
|
|
11798
|
+
timedOut,
|
|
11752
11799
|
duration: maxDuration,
|
|
11753
11800
|
passRate,
|
|
11754
11801
|
verdict: unexpected > 0 ? "failed" : "passed"
|
|
@@ -11826,18 +11873,13 @@ var ShardMerger = class {
|
|
|
11826
11873
|
};
|
|
11827
11874
|
}
|
|
11828
11875
|
buildCharts(stats, projects) {
|
|
11829
|
-
const suiteResults = projects
|
|
11830
|
-
(p) => p.suites.map((s) => ({
|
|
11831
|
-
label: s.title,
|
|
11832
|
-
passed: s.stats.passed,
|
|
11833
|
-
failed: s.stats.failed,
|
|
11834
|
-
skipped: s.stats.skipped
|
|
11835
|
-
}))
|
|
11836
|
-
).slice(0, TOP_SUITES_FOR_CHART2);
|
|
11876
|
+
const suiteResults = buildSuiteResults(projects, TOP_SUITES_FOR_CHART);
|
|
11837
11877
|
return {
|
|
11838
11878
|
passRate: {
|
|
11839
11879
|
passed: stats.passed,
|
|
11840
11880
|
failed: stats.failed,
|
|
11881
|
+
timedOut: stats.timedOut,
|
|
11882
|
+
// always 0 in shard JSON (no separate timedOut count)
|
|
11841
11883
|
skipped: stats.skipped,
|
|
11842
11884
|
flaky: stats.flaky
|
|
11843
11885
|
},
|
|
@@ -11869,6 +11911,9 @@ function formatDuration(ms) {
|
|
|
11869
11911
|
}
|
|
11870
11912
|
|
|
11871
11913
|
// src/templates/engine.ts
|
|
11914
|
+
function jsonForInlineScript(value) {
|
|
11915
|
+
return JSON.stringify(value).replace(/<\//g, "<\\/");
|
|
11916
|
+
}
|
|
11872
11917
|
var TemplateEngine = class {
|
|
11873
11918
|
constructor() {
|
|
11874
11919
|
this.chartjsInline = "";
|
|
@@ -12052,6 +12097,19 @@ var TemplateEngine = class {
|
|
|
12052
12097
|
"upperCase",
|
|
12053
12098
|
(s) => typeof s === "string" ? s.toUpperCase() : s
|
|
12054
12099
|
);
|
|
12100
|
+
this.handlebars.registerHelper("verdictLabel", (v) => {
|
|
12101
|
+
const map = {
|
|
12102
|
+
passed: "PASSED",
|
|
12103
|
+
failed: "FAILED",
|
|
12104
|
+
timedout: "TIMED OUT",
|
|
12105
|
+
interrupted: "INTERRUPTED"
|
|
12106
|
+
};
|
|
12107
|
+
return map[v] ?? (typeof v === "string" ? v.toUpperCase() : v);
|
|
12108
|
+
});
|
|
12109
|
+
this.handlebars.registerHelper("coverageBand", (pct) => {
|
|
12110
|
+
const n = Number(pct);
|
|
12111
|
+
return n >= 80 ? "high" : n >= 50 ? "mid" : "low";
|
|
12112
|
+
});
|
|
12055
12113
|
this.handlebars.registerHelper("addOne", (n) => (n ?? 0) + 1);
|
|
12056
12114
|
this.handlebars.registerHelper(
|
|
12057
12115
|
"join",
|
|
@@ -12071,7 +12129,7 @@ var TemplateEngine = class {
|
|
|
12071
12129
|
if (!Array.isArray(arr)) return "";
|
|
12072
12130
|
const values = arr.map((item) => {
|
|
12073
12131
|
const v = item[key];
|
|
12074
|
-
return typeof v === "string" ?
|
|
12132
|
+
return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
|
|
12075
12133
|
});
|
|
12076
12134
|
return new import_handlebars.default.SafeString(values.join(","));
|
|
12077
12135
|
}
|
|
@@ -12082,7 +12140,7 @@ var TemplateEngine = class {
|
|
|
12082
12140
|
this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
|
|
12083
12141
|
this.handlebars.registerHelper(
|
|
12084
12142
|
"safeJsonData",
|
|
12085
|
-
(value) => new import_handlebars.default.SafeString(
|
|
12143
|
+
(value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
|
|
12086
12144
|
);
|
|
12087
12145
|
this.handlebars.registerHelper(
|
|
12088
12146
|
"reverse",
|
|
@@ -13069,7 +13127,7 @@ var PdfReporter = class {
|
|
|
13069
13127
|
this.dataCollector = new DataCollector();
|
|
13070
13128
|
this.pdfGenerator = new PdfGenerator();
|
|
13071
13129
|
this.options = parseOptions(rawOptions);
|
|
13072
|
-
const version = true ? "0.
|
|
13130
|
+
const version = true ? "0.5.1" : "0.x";
|
|
13073
13131
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13074
13132
|
this.licenseClient = new LicenseClient({
|
|
13075
13133
|
licenseKey: this.options.licenseKey,
|
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
+
{{> release-gate}}
|
|
16
|
+
|
|
15
17
|
{{> executive-summary options=(obj showPassRate=true) }}
|
|
16
18
|
|
|
17
19
|
{{> charts options=(obj showCharts=true showTrend=options.showTrend) }}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</div>
|
|
21
|
+
{{> requirements-matrix }}
|
|
22
|
+
|
|
23
|
+
{{> ci-environment options=(obj showFullEnvironment=true) }}
|
|
23
24
|
|
|
24
25
|
{{> suite-breakdown options=(obj showRetries=true) }}
|
|
25
26
|
|
|
@@ -71,12 +71,14 @@
|
|
|
71
71
|
var remaining = 0;
|
|
72
72
|
function chartDone() { if (--remaining === 0) window.__chartsReady = true; }
|
|
73
73
|
|
|
74
|
-
var passed
|
|
75
|
-
var failed
|
|
76
|
-
var
|
|
77
|
-
var
|
|
78
|
-
var
|
|
79
|
-
|
|
74
|
+
var passed = {{charts.passRate.passed}};
|
|
75
|
+
var failed = {{charts.passRate.failed}};
|
|
76
|
+
var timedOut = {{charts.passRate.timedOut}};
|
|
77
|
+
var skipped = {{charts.passRate.skipped}};
|
|
78
|
+
var flaky = {{charts.passRate.flaky}};
|
|
79
|
+
// Pass rate is the canonical server-side stat (flaky counts as pass, denominator
|
|
80
|
+
// is every test) so the doughnut centre can never disagree with the KPI strip.
|
|
81
|
+
var passRate = {{stats.passRate}};
|
|
80
82
|
|
|
81
83
|
/* ── Centre-text plugin for the doughnut ─────────────────────────────── */
|
|
82
84
|
var centerTextPlugin = {
|
|
@@ -107,11 +109,11 @@
|
|
|
107
109
|
new Chart(document.getElementById('passRateChart'), {
|
|
108
110
|
type: 'doughnut',
|
|
109
111
|
data: {
|
|
110
|
-
labels: ['Passed', 'Failed', 'Skipped', 'Flaky'],
|
|
112
|
+
labels: ['Passed', 'Failed', 'Timed Out', 'Skipped', 'Flaky'],
|
|
111
113
|
datasets: [{
|
|
112
|
-
data: [passed, failed, skipped, flaky],
|
|
113
|
-
backgroundColor: ['#3F7D58', '#C3553C', '#7A786E', '#D49452'],
|
|
114
|
-
borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
|
|
114
|
+
data: [passed, failed, timedOut, skipped, flaky],
|
|
115
|
+
backgroundColor: ['#3F7D58', '#C3553C', '#B5803A', '#7A786E', '#D49452'],
|
|
116
|
+
borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
|
|
115
117
|
borderWidth: 2,
|
|
116
118
|
hoverOffset: 4
|
|
117
119
|
}]
|
|
@@ -135,8 +137,9 @@
|
|
|
135
137
|
/* ── Horizontal suite results bar ────────────────────────────────────── */
|
|
136
138
|
var suiteLabels = [{{{jsonArray charts.suiteResults 'label'}}}];
|
|
137
139
|
var suitePassed = [{{{jsonArray charts.suiteResults 'passed'}}}];
|
|
138
|
-
var suiteFailed
|
|
139
|
-
var
|
|
140
|
+
var suiteFailed = [{{{jsonArray charts.suiteResults 'failed'}}}];
|
|
141
|
+
var suiteTimedOut = [{{{jsonArray charts.suiteResults 'timedOut'}}}];
|
|
142
|
+
var suiteSkipped = [{{{jsonArray charts.suiteResults 'skipped'}}}];
|
|
140
143
|
|
|
141
144
|
remaining += 1;
|
|
142
145
|
new Chart(document.getElementById('suitesBarChart'), {
|
|
@@ -144,9 +147,10 @@
|
|
|
144
147
|
data: {
|
|
145
148
|
labels: suiteLabels,
|
|
146
149
|
datasets: [
|
|
147
|
-
{ label: 'Passed',
|
|
148
|
-
{ label: 'Failed',
|
|
149
|
-
{ label: '
|
|
150
|
+
{ label: 'Passed', data: suitePassed, backgroundColor: '#3F7D58', borderRadius: 3 },
|
|
151
|
+
{ label: 'Failed', data: suiteFailed, backgroundColor: '#C3553C', borderRadius: 3 },
|
|
152
|
+
{ label: 'Timed Out', data: suiteTimedOut, backgroundColor: '#B5803A', borderRadius: 3 },
|
|
153
|
+
{ label: 'Skipped', data: suiteSkipped, backgroundColor: '#7A786E', borderRadius: 3 }
|
|
150
154
|
]
|
|
151
155
|
},
|
|
152
156
|
options: {
|
|
@@ -161,7 +165,17 @@
|
|
|
161
165
|
},
|
|
162
166
|
y: {
|
|
163
167
|
stacked: true,
|
|
164
|
-
|
|
168
|
+
// Widen the label gutter so the longest label isn't clipped at the canvas edge.
|
|
169
|
+
afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width += 8; } },
|
|
170
|
+
ticks: {
|
|
171
|
+
font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C', autoSkip: false,
|
|
172
|
+
// Truncate long suite / describe labels so they fit the axis gutter
|
|
173
|
+
// instead of clipping at the canvas edge.
|
|
174
|
+
callback: function(value) {
|
|
175
|
+
var l = this.getLabelForValue(value);
|
|
176
|
+
return l.length > 24 ? l.slice(0, 23) + '…' : l;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
165
179
|
grid: { display: false },
|
|
166
180
|
border: { color: '#D6D4C8' }
|
|
167
181
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="section">
|
|
2
2
|
<span class="verdict verdict--{{stats.verdict}}">
|
|
3
|
-
{{
|
|
3
|
+
{{verdictLabel stats.verdict}}
|
|
4
4
|
</span>
|
|
5
5
|
|
|
6
6
|
<div class="kpi-strip">
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<div class="kpi-card__value">{{stats.failed}}</div>
|
|
20
20
|
<div class="kpi-card__label">Failed</div>
|
|
21
21
|
</div>
|
|
22
|
+
<div class="kpi-card kpi-card--timedout">
|
|
23
|
+
<div class="kpi-card__value">{{stats.timedOut}}</div>
|
|
24
|
+
<div class="kpi-card__label">Timed Out</div>
|
|
25
|
+
</div>
|
|
22
26
|
<div class="kpi-card kpi-card--skip">
|
|
23
27
|
<div class="kpi-card__value">{{stats.skipped}}</div>
|
|
24
28
|
<div class="kpi-card__label">Skipped</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{{#if failures.length}}
|
|
2
|
-
<div class="section
|
|
2
|
+
<div class="section">
|
|
3
3
|
<h2>Failures ({{failures.length}}{{#if overflowFailureCount}} shown · {{overflowFailureCount}} more in sidecar{{/if}})</h2>
|
|
4
4
|
{{#if overflowFailureCount}}
|
|
5
5
|
<div class="overflow-banner">
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
{{/if}}
|
|
11
11
|
{{#each failures}}
|
|
12
|
-
<div class="failure-item no-break">
|
|
12
|
+
<div class="failure-item failure-item--{{severity}} no-break">
|
|
13
13
|
{{#if ../options.showFullFailures}}
|
|
14
14
|
|
|
15
15
|
<div class="failure-item__header">
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<td style="color:{{#if failed}}var(--fail){{else}}var(--pass){{/if}};font-weight:600;">{{failed}}</td>
|
|
24
24
|
<td>
|
|
25
25
|
<div class="coverage-bar">
|
|
26
|
-
<div class="coverage-bar__fill" style="width:{{coveragePct}}%;"></div>
|
|
26
|
+
<div class="coverage-bar__fill coverage-bar__fill--{{coverageBand coveragePct}}" style="width:{{coveragePct}}%;"></div>
|
|
27
27
|
</div>
|
|
28
28
|
<span style="font-size:9.5px;color:var(--text-3);">{{coveragePct}}%</span>
|
|
29
29
|
</td>
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
<tr class="no-break">
|
|
17
17
|
<td style="font-weight:500;">{{title}}</td>
|
|
18
18
|
<td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{suiteTitle}}</td>
|
|
19
|
-
{{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);">{{project}}</td>{{/if}}
|
|
19
|
+
{{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);white-space:nowrap;">{{project}}</td>{{/if}}
|
|
20
20
|
<td style="font-family:'JetBrains Mono',monospace;font-size:9.5px;color:var(--text-2);text-align:right;">{{durationFormatted}}</td>
|
|
21
|
-
<td
|
|
21
|
+
<td>{{statusBadge status}}</td>
|
|
22
22
|
</tr>
|
|
23
23
|
{{/each}}
|
|
24
24
|
</tbody>
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<span class="suite-stats">
|
|
16
16
|
<span class="badge badge--passed">{{stats.passed}}</span>
|
|
17
17
|
{{#if stats.failed}}<span class="badge badge--failed">{{stats.failed}}</span>{{/if}}
|
|
18
|
+
{{#if stats.timedOut}}<span class="badge badge--timedout">{{stats.timedOut}}</span>{{/if}}
|
|
18
19
|
{{#if stats.skipped}}<span class="badge badge--skipped">{{stats.skipped}}</span>{{/if}}
|
|
19
20
|
{{#if stats.flaky}}<span class="badge badge--flaky">{{stats.flaky}}</span>{{/if}}
|
|
20
21
|
</span>
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
</div>
|
|
42
43
|
{{/if}}
|
|
43
44
|
</td>
|
|
44
|
-
<td
|
|
45
|
+
<td>{{statusBadge status}}</td>
|
|
45
46
|
<td style="color:var(--text-2);font-family:'JetBrains Mono',monospace;font-size:9.5px;">{{formatDuration duration}}</td>
|
|
46
47
|
{{#if ../../options.showRetries}}<td style="color:var(--text-3);text-align:center;">{{retryCount}}</td>{{/if}}
|
|
47
48
|
</tr>
|