ortoni-report 1.0.8 → 1.1.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 +54 -0
- package/dist/css/pico.css +2802 -0
- package/dist/icon/32.png +0 -0
- package/dist/ortoni-report.d.ts +8 -1
- package/dist/ortoni-report.js +50 -19
- package/dist/ortoni-report.mjs +50 -19
- package/dist/report-template.hbs +141 -61
- package/package.json +13 -6
- package/readme.md +62 -15
- package/.prettierignore +0 -1
- package/report-template.hbs +0 -251
package/dist/icon/32.png
ADDED
|
Binary file
|
package/dist/ortoni-report.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
|
+
interface ReporterConfig {
|
|
4
|
+
projectName?: string;
|
|
5
|
+
authorName?: string;
|
|
6
|
+
testType?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
declare class OrtoniReport implements Reporter {
|
|
4
10
|
private results;
|
|
5
11
|
private groupedResults;
|
|
6
12
|
private suiteName;
|
|
7
|
-
|
|
13
|
+
private config;
|
|
14
|
+
constructor(config?: ReporterConfig);
|
|
8
15
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
9
16
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
10
17
|
onTestEnd(test: TestCase, result: TestResult): void;
|
package/dist/ortoni-report.js
CHANGED
|
@@ -34,31 +34,57 @@ __export(ortoni_report_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(ortoni_report_exports);
|
|
36
36
|
var import_fs = __toESM(require("fs"));
|
|
37
|
-
var
|
|
37
|
+
var import_path2 = __toESM(require("path"));
|
|
38
38
|
var import_handlebars = __toESM(require("handlebars"));
|
|
39
39
|
var import_safe = __toESM(require("colors/safe"));
|
|
40
|
+
|
|
41
|
+
// src/utils/time.ts
|
|
42
|
+
var import_path = __toESM(require("path"));
|
|
43
|
+
function msToTime(duration) {
|
|
44
|
+
const seconds = Math.floor(duration / 1e3 % 60);
|
|
45
|
+
const minutes = Math.floor(duration / (1e3 * 60) % 60);
|
|
46
|
+
const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
|
|
47
|
+
const parts = [];
|
|
48
|
+
if (hours > 0)
|
|
49
|
+
parts.push(hours + "h");
|
|
50
|
+
if (minutes > 0)
|
|
51
|
+
parts.push(minutes + "m");
|
|
52
|
+
if (seconds > 0 || parts.length === 0)
|
|
53
|
+
parts.push(seconds + "s");
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
56
|
+
function normalizeFilePath(filePath) {
|
|
57
|
+
const normalizedPath = import_path.default.normalize(filePath);
|
|
58
|
+
return import_path.default.basename(normalizedPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/ortoni-report.ts
|
|
40
62
|
var OrtoniReport = class {
|
|
41
|
-
constructor() {
|
|
63
|
+
constructor(config = {}) {
|
|
42
64
|
this.results = [];
|
|
65
|
+
this.config = config;
|
|
43
66
|
}
|
|
44
67
|
onBegin(config, suite) {
|
|
45
68
|
this.results = [];
|
|
46
|
-
const screenshotsDir =
|
|
47
|
-
if (
|
|
48
|
-
import_fs.default.
|
|
69
|
+
const screenshotsDir = import_path2.default.resolve(process.cwd(), "screenshots");
|
|
70
|
+
if (import_fs.default.existsSync(screenshotsDir)) {
|
|
71
|
+
import_fs.default.rmSync(screenshotsDir, { recursive: true, force: true });
|
|
49
72
|
}
|
|
73
|
+
import_fs.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
50
74
|
}
|
|
51
75
|
onTestBegin(test, result) {
|
|
52
76
|
}
|
|
53
77
|
onTestEnd(test, result) {
|
|
54
78
|
const testResult = {
|
|
79
|
+
totalDuration: "",
|
|
55
80
|
projectName: test.titlePath()[1],
|
|
56
81
|
// Get the project name
|
|
57
82
|
suite: test.titlePath()[3],
|
|
58
83
|
// Adjust the index based on your suite hierarchy
|
|
59
84
|
title: test.title,
|
|
60
85
|
status: result.status,
|
|
61
|
-
|
|
86
|
+
flaky: test.outcome(),
|
|
87
|
+
duration: msToTime(result.duration),
|
|
62
88
|
errors: result.errors.map((e) => import_safe.default.strip(e.message || e.toString())),
|
|
63
89
|
steps: result.steps.map((step) => ({
|
|
64
90
|
title: step.title,
|
|
@@ -68,24 +94,25 @@ var OrtoniReport = class {
|
|
|
68
94
|
})),
|
|
69
95
|
logs: import_safe.default.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
|
|
70
96
|
screenshotPath: null,
|
|
71
|
-
filePath: test.titlePath()[2]
|
|
97
|
+
filePath: normalizeFilePath(test.titlePath()[2])
|
|
72
98
|
};
|
|
73
99
|
if (result.attachments) {
|
|
74
|
-
const screenshotsDir =
|
|
100
|
+
const screenshotsDir = import_path2.default.resolve(process.cwd(), "screenshots", test.id);
|
|
75
101
|
if (!import_fs.default.existsSync(screenshotsDir)) {
|
|
76
|
-
import_fs.default.mkdirSync(screenshotsDir);
|
|
102
|
+
import_fs.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
77
103
|
}
|
|
78
104
|
const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
|
|
79
105
|
if (screenshot && screenshot.path) {
|
|
80
106
|
const screenshotContent = import_fs.default.readFileSync(screenshot.path, "base64");
|
|
81
|
-
const screenshotFileName =
|
|
82
|
-
import_fs.default.writeFileSync(
|
|
107
|
+
const screenshotFileName = import_path2.default.join("screenshots", test.id, import_path2.default.basename(screenshot.path));
|
|
108
|
+
import_fs.default.writeFileSync(import_path2.default.resolve(process.cwd(), screenshotFileName), screenshotContent, "base64");
|
|
83
109
|
testResult.screenshotPath = screenshotFileName;
|
|
84
110
|
}
|
|
85
111
|
}
|
|
86
112
|
this.results.push(testResult);
|
|
87
113
|
}
|
|
88
114
|
onEnd(result) {
|
|
115
|
+
this.results[0].totalDuration = msToTime(result.duration);
|
|
89
116
|
this.groupedResults = this.results.reduce((acc, result2, index) => {
|
|
90
117
|
const filePath = result2.filePath;
|
|
91
118
|
const suiteName = result2.suite;
|
|
@@ -105,26 +132,30 @@ var OrtoniReport = class {
|
|
|
105
132
|
import_handlebars.default.registerHelper("json", function(context) {
|
|
106
133
|
return safeStringify(context);
|
|
107
134
|
});
|
|
108
|
-
import_handlebars.default.registerHelper("splitSuiteName", function(suiteName) {
|
|
109
|
-
return suiteName.split(" - ");
|
|
110
|
-
});
|
|
111
135
|
const html = this.generateHTML();
|
|
112
|
-
const outputPath =
|
|
136
|
+
const outputPath = import_path2.default.resolve(process.cwd(), "ortoni-report.html");
|
|
113
137
|
import_fs.default.writeFileSync(outputPath, html);
|
|
114
138
|
console.log(`Ortoni HTML report generated at ${outputPath}`);
|
|
115
139
|
}
|
|
116
140
|
generateHTML() {
|
|
117
|
-
const templateSource = import_fs.default.readFileSync(
|
|
141
|
+
const templateSource = import_fs.default.readFileSync(import_path2.default.resolve(__dirname, "report-template.hbs"), "utf-8");
|
|
118
142
|
const template = import_handlebars.default.compile(templateSource);
|
|
119
143
|
const data = {
|
|
144
|
+
totalDuration: this.results[0].totalDuration,
|
|
120
145
|
suiteName: this.suiteName,
|
|
121
146
|
results: this.results,
|
|
122
147
|
passCount: this.results.filter((r) => r.status === "passed").length,
|
|
123
|
-
failCount: this.results.filter((r) => r.status === "failed").length,
|
|
148
|
+
failCount: this.results.filter((r) => r.status === "failed" || r.status === "timedOut").length,
|
|
124
149
|
skipCount: this.results.filter((r) => r.status === "skipped").length,
|
|
125
|
-
|
|
150
|
+
flakyCount: this.results.filter((r) => r.flaky === "flaky").length,
|
|
126
151
|
totalCount: this.results.length,
|
|
127
|
-
groupedResults: this.groupedResults
|
|
152
|
+
groupedResults: this.groupedResults,
|
|
153
|
+
projectName: this.config.projectName,
|
|
154
|
+
// Include project name
|
|
155
|
+
authorName: this.config.authorName,
|
|
156
|
+
// Include author name
|
|
157
|
+
testType: this.config.testType
|
|
158
|
+
// Include test type
|
|
128
159
|
};
|
|
129
160
|
return template(data);
|
|
130
161
|
}
|
package/dist/ortoni-report.mjs
CHANGED
|
@@ -1,30 +1,56 @@
|
|
|
1
1
|
// src/ortoni-report.ts
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import
|
|
3
|
+
import path2 from "path";
|
|
4
4
|
import Handlebars from "handlebars";
|
|
5
5
|
import colors from "colors/safe";
|
|
6
|
+
|
|
7
|
+
// src/utils/time.ts
|
|
8
|
+
import path from "path";
|
|
9
|
+
function msToTime(duration) {
|
|
10
|
+
const seconds = Math.floor(duration / 1e3 % 60);
|
|
11
|
+
const minutes = Math.floor(duration / (1e3 * 60) % 60);
|
|
12
|
+
const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (hours > 0)
|
|
15
|
+
parts.push(hours + "h");
|
|
16
|
+
if (minutes > 0)
|
|
17
|
+
parts.push(minutes + "m");
|
|
18
|
+
if (seconds > 0 || parts.length === 0)
|
|
19
|
+
parts.push(seconds + "s");
|
|
20
|
+
return parts.join(" ");
|
|
21
|
+
}
|
|
22
|
+
function normalizeFilePath(filePath) {
|
|
23
|
+
const normalizedPath = path.normalize(filePath);
|
|
24
|
+
return path.basename(normalizedPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/ortoni-report.ts
|
|
6
28
|
var OrtoniReport = class {
|
|
7
|
-
constructor() {
|
|
29
|
+
constructor(config = {}) {
|
|
8
30
|
this.results = [];
|
|
31
|
+
this.config = config;
|
|
9
32
|
}
|
|
10
33
|
onBegin(config, suite) {
|
|
11
34
|
this.results = [];
|
|
12
|
-
const screenshotsDir =
|
|
13
|
-
if (
|
|
14
|
-
fs.
|
|
35
|
+
const screenshotsDir = path2.resolve(process.cwd(), "screenshots");
|
|
36
|
+
if (fs.existsSync(screenshotsDir)) {
|
|
37
|
+
fs.rmSync(screenshotsDir, { recursive: true, force: true });
|
|
15
38
|
}
|
|
39
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
16
40
|
}
|
|
17
41
|
onTestBegin(test, result) {
|
|
18
42
|
}
|
|
19
43
|
onTestEnd(test, result) {
|
|
20
44
|
const testResult = {
|
|
45
|
+
totalDuration: "",
|
|
21
46
|
projectName: test.titlePath()[1],
|
|
22
47
|
// Get the project name
|
|
23
48
|
suite: test.titlePath()[3],
|
|
24
49
|
// Adjust the index based on your suite hierarchy
|
|
25
50
|
title: test.title,
|
|
26
51
|
status: result.status,
|
|
27
|
-
|
|
52
|
+
flaky: test.outcome(),
|
|
53
|
+
duration: msToTime(result.duration),
|
|
28
54
|
errors: result.errors.map((e) => colors.strip(e.message || e.toString())),
|
|
29
55
|
steps: result.steps.map((step) => ({
|
|
30
56
|
title: step.title,
|
|
@@ -34,24 +60,25 @@ var OrtoniReport = class {
|
|
|
34
60
|
})),
|
|
35
61
|
logs: colors.strip(result.stdout.concat(result.stderr).map((log) => log).join("\n")),
|
|
36
62
|
screenshotPath: null,
|
|
37
|
-
filePath: test.titlePath()[2]
|
|
63
|
+
filePath: normalizeFilePath(test.titlePath()[2])
|
|
38
64
|
};
|
|
39
65
|
if (result.attachments) {
|
|
40
|
-
const screenshotsDir =
|
|
66
|
+
const screenshotsDir = path2.resolve(process.cwd(), "screenshots", test.id);
|
|
41
67
|
if (!fs.existsSync(screenshotsDir)) {
|
|
42
|
-
fs.mkdirSync(screenshotsDir);
|
|
68
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
43
69
|
}
|
|
44
70
|
const screenshot = result.attachments.find((attachment) => attachment.name === "screenshot");
|
|
45
71
|
if (screenshot && screenshot.path) {
|
|
46
72
|
const screenshotContent = fs.readFileSync(screenshot.path, "base64");
|
|
47
|
-
const screenshotFileName =
|
|
48
|
-
fs.writeFileSync(
|
|
73
|
+
const screenshotFileName = path2.join("screenshots", test.id, path2.basename(screenshot.path));
|
|
74
|
+
fs.writeFileSync(path2.resolve(process.cwd(), screenshotFileName), screenshotContent, "base64");
|
|
49
75
|
testResult.screenshotPath = screenshotFileName;
|
|
50
76
|
}
|
|
51
77
|
}
|
|
52
78
|
this.results.push(testResult);
|
|
53
79
|
}
|
|
54
80
|
onEnd(result) {
|
|
81
|
+
this.results[0].totalDuration = msToTime(result.duration);
|
|
55
82
|
this.groupedResults = this.results.reduce((acc, result2, index) => {
|
|
56
83
|
const filePath = result2.filePath;
|
|
57
84
|
const suiteName = result2.suite;
|
|
@@ -71,26 +98,30 @@ var OrtoniReport = class {
|
|
|
71
98
|
Handlebars.registerHelper("json", function(context) {
|
|
72
99
|
return safeStringify(context);
|
|
73
100
|
});
|
|
74
|
-
Handlebars.registerHelper("splitSuiteName", function(suiteName) {
|
|
75
|
-
return suiteName.split(" - ");
|
|
76
|
-
});
|
|
77
101
|
const html = this.generateHTML();
|
|
78
|
-
const outputPath =
|
|
102
|
+
const outputPath = path2.resolve(process.cwd(), "ortoni-report.html");
|
|
79
103
|
fs.writeFileSync(outputPath, html);
|
|
80
104
|
console.log(`Ortoni HTML report generated at ${outputPath}`);
|
|
81
105
|
}
|
|
82
106
|
generateHTML() {
|
|
83
|
-
const templateSource = fs.readFileSync(
|
|
107
|
+
const templateSource = fs.readFileSync(path2.resolve(__dirname, "report-template.hbs"), "utf-8");
|
|
84
108
|
const template = Handlebars.compile(templateSource);
|
|
85
109
|
const data = {
|
|
110
|
+
totalDuration: this.results[0].totalDuration,
|
|
86
111
|
suiteName: this.suiteName,
|
|
87
112
|
results: this.results,
|
|
88
113
|
passCount: this.results.filter((r) => r.status === "passed").length,
|
|
89
|
-
failCount: this.results.filter((r) => r.status === "failed").length,
|
|
114
|
+
failCount: this.results.filter((r) => r.status === "failed" || r.status === "timedOut").length,
|
|
90
115
|
skipCount: this.results.filter((r) => r.status === "skipped").length,
|
|
91
|
-
|
|
116
|
+
flakyCount: this.results.filter((r) => r.flaky === "flaky").length,
|
|
92
117
|
totalCount: this.results.length,
|
|
93
|
-
groupedResults: this.groupedResults
|
|
118
|
+
groupedResults: this.groupedResults,
|
|
119
|
+
projectName: this.config.projectName,
|
|
120
|
+
// Include project name
|
|
121
|
+
authorName: this.config.authorName,
|
|
122
|
+
// Include author name
|
|
123
|
+
testType: this.config.testType
|
|
124
|
+
// Include test type
|
|
94
125
|
};
|
|
95
126
|
return template(data);
|
|
96
127
|
}
|
package/dist/report-template.hbs
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>Playwright Test Report</title>
|
|
8
|
-
<link rel="
|
|
9
|
-
<link rel="stylesheet" href="
|
|
8
|
+
<link rel="icon" href="node_modules\ortoni-report\src\icon\32.png" type="image/x-icon">
|
|
9
|
+
<link rel="stylesheet" href="node_modules\ortoni-report\src\css\pico.css">
|
|
10
10
|
<style>
|
|
11
|
+
body {
|
|
12
|
+
zoom: 0.9;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
main.container {
|
|
12
16
|
display: grid;
|
|
13
17
|
grid-template-columns: 1fr 2fr;
|
|
@@ -24,27 +28,31 @@
|
|
|
24
28
|
justify-content: space-evenly;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
.highlight {
|
|
32
|
+
background-color: var(--pico-primary-background);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
.text-success {
|
|
28
36
|
color: #28a745;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
.text-
|
|
39
|
+
.text-failure {
|
|
32
40
|
color: #dc3545;
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
.text-skip {
|
|
44
|
+
color: #d5d4a1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.text-flaky {
|
|
48
|
+
color: #d5d4a1;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
.sidebar {
|
|
36
52
|
border-right: 1px solid #ddd;
|
|
37
53
|
padding-right: 10px;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
.card {
|
|
41
|
-
padding: 1rem;
|
|
42
|
-
border: 1px solid #ddd;
|
|
43
|
-
border-radius: 0.5rem;
|
|
44
|
-
background-color: #fff;
|
|
45
|
-
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
56
|
pre {
|
|
49
57
|
background-color: #f8f9fa;
|
|
50
58
|
padding: 1rem;
|
|
@@ -62,21 +70,21 @@
|
|
|
62
70
|
<body>
|
|
63
71
|
<header class="container">
|
|
64
72
|
<div class="header">
|
|
73
|
+
{{!-- Custom Project Name --}}
|
|
65
74
|
<div>
|
|
66
|
-
<h1>
|
|
75
|
+
{{#if projectName}}<h1>{{projectName}}</h1>{{/if}}
|
|
67
76
|
</div>
|
|
77
|
+
{{!-- Dummy for now --}}
|
|
68
78
|
<div>
|
|
69
|
-
<
|
|
70
|
-
<input name="search" type="search" placeholder="Search" />
|
|
71
|
-
<input type="submit" value="Search" />
|
|
72
|
-
</form>
|
|
79
|
+
<input name="search" type="search" placeholder="Search by test title" />
|
|
73
80
|
</div>
|
|
74
81
|
</div>
|
|
75
82
|
</header>
|
|
76
83
|
<main class="container">
|
|
84
|
+
{{!-- Test Scripts --}}
|
|
77
85
|
<aside class="sidebar">
|
|
78
86
|
<h2>Tests</h2>
|
|
79
|
-
<div
|
|
87
|
+
<div>
|
|
80
88
|
{{#each groupedResults}}
|
|
81
89
|
<details>
|
|
82
90
|
<summary>{{@key}}</summary>
|
|
@@ -104,15 +112,13 @@
|
|
|
104
112
|
{{/each}}
|
|
105
113
|
</div>
|
|
106
114
|
</aside>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
115
|
<section>
|
|
116
|
+
{{!-- Overall summar --}}
|
|
111
117
|
<div id="summary">
|
|
112
118
|
<section class="grid">
|
|
113
119
|
<div>
|
|
114
120
|
<article>
|
|
115
|
-
<header>
|
|
121
|
+
<header>All Tests</header>
|
|
116
122
|
<p>{{totalCount}}</p>
|
|
117
123
|
</article>
|
|
118
124
|
</div>
|
|
@@ -125,7 +131,7 @@
|
|
|
125
131
|
<div>
|
|
126
132
|
<article>
|
|
127
133
|
<header>Failed</header>
|
|
128
|
-
<p class="text-
|
|
134
|
+
<p class="text-failure">{{failCount}}</p>
|
|
129
135
|
</article>
|
|
130
136
|
</div>
|
|
131
137
|
</section>
|
|
@@ -133,24 +139,34 @@
|
|
|
133
139
|
<div>
|
|
134
140
|
<article>
|
|
135
141
|
<header>Skipped</header>
|
|
136
|
-
<p>{{skipCount}}</p>
|
|
142
|
+
<p class="text-skip">{{skipCount}}</p>
|
|
137
143
|
</article>
|
|
138
144
|
</div>
|
|
139
145
|
<div>
|
|
140
146
|
<article>
|
|
141
|
-
<header>
|
|
142
|
-
<p class="text-
|
|
147
|
+
<header>Flaky</header>
|
|
148
|
+
<p class="text-flaky">{{flakyCount}}</p>
|
|
143
149
|
</article>
|
|
144
150
|
</div>
|
|
145
151
|
</section>
|
|
152
|
+
{{!-- Suite details with chart --}}
|
|
146
153
|
<section>
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
<
|
|
150
|
-
|
|
154
|
+
<article>
|
|
155
|
+
<header>Suite</header>
|
|
156
|
+
<div class="grid">
|
|
157
|
+
<div>
|
|
158
|
+
{{#if authorName}}<h4>Author: {{authorName}}</h4>{{/if}}
|
|
159
|
+
{{#if testType}}<h4>Test Type: {{testType}}</h4>{{/if}}
|
|
160
|
+
{{#if totalDuration}}<h4>Duration: {{totalDuration}}</h4>{{/if}}
|
|
161
|
+
</div>
|
|
162
|
+
<div class="chart-container">
|
|
163
|
+
<canvas id="testChart"></canvas>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</article>
|
|
151
167
|
</section>
|
|
152
168
|
</div>
|
|
153
|
-
|
|
169
|
+
{{!-- Test details --}}
|
|
154
170
|
<div id="testDetails" style="display: none;">
|
|
155
171
|
<!-- Back button should be outside the dynamic content -->
|
|
156
172
|
<button class="back-button" onclick="showSummary()">Back to Summary</button>
|
|
@@ -161,16 +177,33 @@
|
|
|
161
177
|
|
|
162
178
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
|
|
163
179
|
<script>
|
|
180
|
+
function escapeHtml(unsafe) {
|
|
181
|
+
return unsafe.replace(/[&<"']/g, function (match) {
|
|
182
|
+
const escapeMap = {
|
|
183
|
+
'&': '&',
|
|
184
|
+
'<': '<',
|
|
185
|
+
'>': '>',
|
|
186
|
+
'"': '"',
|
|
187
|
+
"'": '''
|
|
188
|
+
};
|
|
189
|
+
return escapeMap[match];
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
document.addEventListener('DOMContentLoaded', () => {
|
|
165
|
-
|
|
194
|
+
const testData = {{{ json results }}};
|
|
166
195
|
const testDetails = document.getElementById('testDetails');
|
|
167
196
|
const summary = document.getElementById('summary');
|
|
168
197
|
const backButton = document.querySelector('.back-button');
|
|
198
|
+
let highlightedItem = null;
|
|
169
199
|
|
|
170
200
|
function showSummary() {
|
|
171
201
|
summary.style.display = 'block';
|
|
172
202
|
testDetails.style.display = 'none';
|
|
173
203
|
backButton.style.display = 'none';
|
|
204
|
+
if (highlightedItem) {
|
|
205
|
+
highlightedItem.classList.remove('highlight');
|
|
206
|
+
}
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
window.showSummary = showSummary;
|
|
@@ -181,39 +214,94 @@
|
|
|
181
214
|
backButton.style.display = 'block';
|
|
182
215
|
testDetails.innerHTML = `
|
|
183
216
|
<button class="back-button" style="display: block" onclick="showSummary()">Back to Summary</button>
|
|
184
|
-
<
|
|
217
|
+
<h3>${test.title}</h3>
|
|
185
218
|
<div class="grid">
|
|
186
219
|
<div>
|
|
187
|
-
<
|
|
188
|
-
<p class="${test.status === 'passed' ? 'text-success' : 'text-
|
|
189
|
-
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
<h3>Errors</h3>
|
|
193
|
-
<pre>${test.errors.join('\n')}</pre>
|
|
194
|
-
` : ''}
|
|
220
|
+
<h4>Status</h4>
|
|
221
|
+
<p class="${test.status === 'passed' ? 'text-success' : 'text-failure'}">${test.status.toUpperCase()}</p>
|
|
222
|
+
${test.duration != '0s' ? `
|
|
223
|
+
<h4>Duration</h4>
|
|
224
|
+
<p>${test.duration}</p>` : ""}
|
|
195
225
|
</div>
|
|
196
226
|
<div>
|
|
197
227
|
${test.screenshotPath ? `
|
|
198
|
-
<
|
|
228
|
+
<h4>Screenshot</h4>
|
|
199
229
|
<img src="${test.screenshotPath}" alt="Screenshot">
|
|
200
230
|
` : ''}
|
|
201
|
-
${test.logs ? `
|
|
202
|
-
<h3>Logs</h3>
|
|
203
|
-
<pre>${test.logs}</pre>
|
|
204
|
-
` : ''}
|
|
205
231
|
</div>
|
|
206
232
|
</div>
|
|
233
|
+
<div>
|
|
234
|
+
${test.errors.length ? `
|
|
235
|
+
<h4>Errors</h4>
|
|
236
|
+
<div class="grid">
|
|
237
|
+
<pre>${escapeHtml(test.errors.join('\n'))}</pre></div>
|
|
238
|
+
` : ''}
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
${test.logs ? `
|
|
242
|
+
<h4>Logs</h4>
|
|
243
|
+
<div class="grid">
|
|
244
|
+
<pre>${escapeHtml(test.logs)}</pre></div>
|
|
245
|
+
` : ''}
|
|
246
|
+
</div>
|
|
207
247
|
`;
|
|
208
248
|
}
|
|
209
249
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
function attachEventListeners() {
|
|
251
|
+
const testItems = document.querySelectorAll('[data-test-id]');
|
|
252
|
+
testItems.forEach(item => {
|
|
253
|
+
item.addEventListener('click', () => {
|
|
254
|
+
const testId = item.getAttribute('data-test-id');
|
|
255
|
+
const test = testData[testId];
|
|
256
|
+
displayTestDetails(test);
|
|
257
|
+
if (highlightedItem) {
|
|
258
|
+
highlightedItem.classList.remove('highlight');
|
|
259
|
+
}
|
|
260
|
+
item.classList.add('highlight');
|
|
261
|
+
highlightedItem = item;
|
|
262
|
+
});
|
|
216
263
|
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
attachEventListeners(); // Attach event listeners initially
|
|
267
|
+
|
|
268
|
+
const searchInput = document.querySelector('input[name="search"]');
|
|
269
|
+
const detailsElements = document.querySelectorAll('details');
|
|
270
|
+
|
|
271
|
+
searchInput.addEventListener('input', () => {
|
|
272
|
+
const searchTerm = searchInput.value.toLowerCase();
|
|
273
|
+
const testItems = document.querySelectorAll('[data-test-id]');
|
|
274
|
+
|
|
275
|
+
if (searchTerm) {
|
|
276
|
+
detailsElements.forEach(detail => {
|
|
277
|
+
detail.open = false; // Collapse all details initially
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
testItems.forEach(item => {
|
|
281
|
+
const testTitle = item.textContent.toLowerCase();
|
|
282
|
+
if (testTitle.includes(searchTerm)) {
|
|
283
|
+
item.style.display = 'block'; // Show matching test item
|
|
284
|
+
|
|
285
|
+
let parent = item.parentElement;
|
|
286
|
+
while (parent && parent.tagName !== 'ASIDE') {
|
|
287
|
+
if (parent.tagName === 'DETAILS') {
|
|
288
|
+
parent.open = true; // Expand parent details elements
|
|
289
|
+
}
|
|
290
|
+
parent = parent.parentElement;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
item.style.display = 'none'; // Hide non-matching test item
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
297
|
+
testItems.forEach(item => {
|
|
298
|
+
item.style.display = 'block'; // Show all test items
|
|
299
|
+
});
|
|
300
|
+
detailsElements.forEach(detail => {
|
|
301
|
+
detail.open = false; // Collapse all details elements
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
attachEventListeners(); // Reattach event listeners after filtering
|
|
217
305
|
});
|
|
218
306
|
|
|
219
307
|
const ctx = document.getElementById('testChart').getContext('2d');
|
|
@@ -223,23 +311,15 @@
|
|
|
223
311
|
labels: ['Passed', 'Failed', 'Skipped'],
|
|
224
312
|
datasets: [{
|
|
225
313
|
data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}],
|
|
226
|
-
backgroundColor: ['#28a745', '#dc3545', '#
|
|
314
|
+
backgroundColor: ['#28a745', '#dc3545', '#d5d4a1']
|
|
227
315
|
}]
|
|
228
316
|
},
|
|
229
317
|
options: {
|
|
230
318
|
responsive: true,
|
|
231
319
|
maintainAspectRatio: false,
|
|
232
320
|
plugins: {
|
|
233
|
-
title: {
|
|
234
|
-
display: true,
|
|
235
|
-
text: 'Overall',
|
|
236
|
-
font: {
|
|
237
|
-
size: 18,
|
|
238
|
-
weight: 'bold'
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
321
|
legend: {
|
|
242
|
-
position: '
|
|
322
|
+
position: 'bottom'
|
|
243
323
|
}
|
|
244
324
|
}
|
|
245
325
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ortoni-report",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Playwright Report By LetCode with Koushik",
|
|
5
5
|
"scripts": {
|
|
6
|
+
"test": "npx playwright test",
|
|
6
7
|
"build": "tsup",
|
|
7
8
|
"release": "npm publish",
|
|
8
9
|
"lint": "tsc"
|
|
9
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
10
16
|
"repository": {
|
|
11
17
|
"type": "git",
|
|
12
18
|
"url": "git+https://github.com/ortoniKC/ortoni-report"
|
|
@@ -19,20 +25,21 @@
|
|
|
19
25
|
"ortoni"
|
|
20
26
|
],
|
|
21
27
|
"author": "Koushik Chatterjee (LetCode with Koushik)",
|
|
22
|
-
"license": "
|
|
28
|
+
"license": "GPL-3.0-only",
|
|
23
29
|
"bugs": {
|
|
24
30
|
"url": "https://github.com/ortoniKC/ortoni-report/issues"
|
|
25
31
|
},
|
|
26
32
|
"homepage": "https://github.com/ortoniKC/ortoni-report#readme",
|
|
27
|
-
|
|
28
|
-
"hiq": "^4.2.3",
|
|
33
|
+
"dependencies": {
|
|
29
34
|
"colors": "^1.4.0",
|
|
30
|
-
"handlebars": "^4.7.8"
|
|
35
|
+
"handlebars": "^4.7.8",
|
|
36
|
+
"hiq": "^4.2.3",
|
|
37
|
+
"rimraf": "^5.0.7"
|
|
31
38
|
},
|
|
32
39
|
"devDependencies": {
|
|
40
|
+
"@changesets/cli": "^2.26.0",
|
|
33
41
|
"@playwright/test": "^1.44.1",
|
|
34
42
|
"@types/node": "^20.14.2",
|
|
35
|
-
"@changesets/cli": "^2.26.0",
|
|
36
43
|
"tsup": "^6.5.0",
|
|
37
44
|
"typescript": "^4.9.4"
|
|
38
45
|
},
|