@nhonh/qabot 1.0.0 → 1.0.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/package.json +1 -1
- package/src/core/constants.js +1 -1
- package/src/reporter/html-builder.js +250 -91
package/package.json
CHANGED
package/src/core/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VERSION } from "../core/constants.js";
|
|
2
2
|
|
|
3
3
|
export class HtmlBuilder {
|
|
4
4
|
render(results, meta) {
|
|
@@ -7,98 +7,186 @@ export class HtmlBuilder {
|
|
|
7
7
|
const failedTests = allTests.filter((t) => t.status === "failed");
|
|
8
8
|
const passedTests = allTests.filter((t) => t.status === "passed");
|
|
9
9
|
const skippedTests = allTests.filter((t) => t.status === "skipped");
|
|
10
|
+
const screenshots = allTests.flatMap((t) =>
|
|
11
|
+
(t.screenshots || []).map((s) => ({ test: t.name, path: s })),
|
|
12
|
+
);
|
|
13
|
+
const maxDuration = Math.max(...allTests.map((t) => t.duration || 0), 1);
|
|
10
14
|
|
|
11
15
|
return `<!DOCTYPE html>
|
|
12
|
-
<html lang="en">
|
|
13
|
-
<
|
|
14
|
-
<meta
|
|
15
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
-
<title>QABot Report - ${esc(meta.feature || "all")} | ${esc(meta.projectName || "")}</title>
|
|
16
|
+
<html lang="en"><head>
|
|
17
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
18
|
+
<title>QABot Report \u2014 ${esc(meta.feature || "all")} | ${esc(meta.projectName || "")}</title>
|
|
17
19
|
<style>
|
|
20
|
+
:root{--bg:#0C0A1A;--bg2:#13102A;--card:#1A1635;--card2:#221E3D;--border:#2D2852;--text:#E8E4F0;--dim:#8B85A0;--v:#A78BFA;--v2:#7C3AED;--v3:#C4B5FD;--g:#34D399;--r:#F87171;--y:#FBBF24;--cyan:#22D3EE;--font:system-ui,-apple-system,BlinkMacSystemFont,sans-serif;--mono:'SF Mono',SFMono-Regular,'JetBrains Mono',Menlo,monospace}
|
|
18
21
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
19
|
-
:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);line-height:1.6}
|
|
23
|
+
.wrap{max-width:1200px;margin:0 auto;padding:2.5rem 2rem}
|
|
24
|
+
a{color:var(--v);text-decoration:none}
|
|
25
|
+
a:hover{text-decoration:underline}
|
|
26
|
+
|
|
27
|
+
.hero{text-align:center;padding:3rem 0 2rem;border-bottom:1px solid var(--border)}
|
|
28
|
+
.hero h1{font-size:1.5rem;font-weight:600;color:var(--v3);letter-spacing:.02em}
|
|
29
|
+
.hero .meta{color:var(--dim);font-size:.85rem;margin-top:.5rem}
|
|
30
|
+
.hero .meta span{margin:0 .5rem}
|
|
31
|
+
.rate-ring{width:120px;height:120px;margin:1.5rem auto;position:relative}
|
|
32
|
+
.rate-ring svg{transform:rotate(-90deg)}
|
|
33
|
+
.rate-ring .rate-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:2rem;font-weight:700}
|
|
34
|
+
|
|
35
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin:2rem 0}
|
|
36
|
+
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;text-align:center;transition:border-color .2s}
|
|
37
|
+
.card:hover{border-color:var(--v)}
|
|
38
|
+
.card .val{font-size:1.75rem;font-weight:700;margin:.25rem 0}
|
|
39
|
+
.card .lbl{font-size:.75rem;color:var(--dim);text-transform:uppercase;letter-spacing:.08em}
|
|
40
|
+
.card.pass .val{color:var(--g)}.card.fail .val{color:var(--r)}.card.skip .val{color:var(--y)}.card.dur .val{color:var(--cyan)}
|
|
41
|
+
|
|
42
|
+
h2{font-size:1.1rem;font-weight:600;color:var(--v3);margin:2.5rem 0 1rem;display:flex;align-items:center;gap:.5rem}
|
|
43
|
+
h2::before{content:'';width:3px;height:1.1rem;background:var(--v2);border-radius:2px}
|
|
44
|
+
|
|
45
|
+
.filters{display:flex;gap:.5rem;margin:1rem 0;flex-wrap:wrap}
|
|
46
|
+
.fbtn{background:var(--card);border:1px solid var(--border);color:var(--dim);padding:.35rem .85rem;border-radius:20px;cursor:pointer;font-size:.8rem;transition:all .2s}
|
|
47
|
+
.fbtn:hover,.fbtn.on{background:var(--v2);border-color:var(--v2);color:#fff}
|
|
48
|
+
.fbtn .count{background:var(--bg);padding:1px 6px;border-radius:10px;font-size:.7rem;margin-left:.3rem}
|
|
49
|
+
.fbtn.on .count{background:rgba(255,255,255,.15)}
|
|
50
|
+
|
|
51
|
+
.tbl{width:100%;border-collapse:separate;border-spacing:0;margin:1rem 0}
|
|
52
|
+
.tbl th{text-align:left;padding:.6rem 1rem;color:var(--dim);font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--bg)}
|
|
53
|
+
.tbl td{padding:.6rem 1rem;border-bottom:1px solid var(--border);vertical-align:top}
|
|
54
|
+
.tbl tr:hover td{background:var(--card)}
|
|
55
|
+
.tbl .idx{color:var(--dim);font-size:.8rem;width:3rem}
|
|
56
|
+
.badge{display:inline-flex;align-items:center;gap:.3rem;padding:2px 10px;border-radius:20px;font-size:.72rem;font-weight:600}
|
|
57
|
+
.badge-passed{background:#34D39918;color:var(--g)}.badge-failed{background:#F8717118;color:var(--r)}.badge-skipped{background:#FBBF2418;color:var(--y)}
|
|
58
|
+
.badge::before{content:'';width:6px;height:6px;border-radius:50%}
|
|
59
|
+
.badge-passed::before{background:var(--g)}.badge-failed::before{background:var(--r)}.badge-skipped::before{background:var(--y)}
|
|
60
|
+
|
|
61
|
+
.dur-bar{display:flex;align-items:center;gap:.5rem;font-size:.8rem;color:var(--dim)}
|
|
62
|
+
.dur-fill{height:4px;border-radius:2px;background:var(--v);min-width:2px;transition:width .3s}
|
|
63
|
+
.test-name{font-weight:500;font-size:.9rem}
|
|
64
|
+
.test-suite{font-size:.8rem;color:var(--dim);margin-top:2px}
|
|
65
|
+
.test-file{font-family:var(--mono);font-size:.75rem;color:var(--dim);margin-top:2px}
|
|
66
|
+
|
|
67
|
+
.detail{background:var(--card);border:1px solid var(--border);border-radius:10px;margin:.75rem 0;overflow:hidden}
|
|
68
|
+
.detail-head{padding:.75rem 1rem;cursor:pointer;display:flex;align-items:center;justify-content:space-between;user-select:none}
|
|
69
|
+
.detail-head:hover{background:var(--card2)}
|
|
70
|
+
.detail-head .arrow{color:var(--dim);font-size:.7rem;transition:transform .2s}
|
|
71
|
+
.detail-head.open .arrow{transform:rotate(180deg)}
|
|
72
|
+
.detail-body{display:none;padding:1rem;border-top:1px solid var(--border)}
|
|
73
|
+
.detail-body.show{display:block}
|
|
74
|
+
|
|
75
|
+
.err-box{background:#1A0A0A;border:1px solid #F8717130;border-radius:8px;padding:1rem;font-family:var(--mono);font-size:.78rem;color:#FCA5A5;white-space:pre-wrap;overflow-x:auto;max-height:250px;overflow-y:auto;line-height:1.5}
|
|
76
|
+
.skip-reason{background:#1A170A;border:1px solid #FBBF2430;border-radius:8px;padding:.75rem 1rem;font-size:.85rem;color:var(--y)}
|
|
77
|
+
.io-section{margin-top:.75rem}
|
|
78
|
+
.io-label{font-size:.75rem;color:var(--dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.3rem}
|
|
79
|
+
.io-box{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.75rem;font-family:var(--mono);font-size:.78rem;max-height:150px;overflow-y:auto;white-space:pre-wrap}
|
|
80
|
+
|
|
81
|
+
.ss-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem;margin:1rem 0}
|
|
82
|
+
.ss-card{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden;transition:border-color .2s}
|
|
83
|
+
.ss-card:hover{border-color:var(--v)}
|
|
84
|
+
.ss-card img{width:100%;display:block;cursor:pointer}
|
|
85
|
+
.ss-card .ss-info{padding:.6rem .8rem;font-size:.8rem;color:var(--dim)}
|
|
86
|
+
|
|
87
|
+
.timeline{margin:1.5rem 0}
|
|
88
|
+
.tl-item{display:flex;align-items:center;gap:.5rem;padding:.4rem 0}
|
|
89
|
+
.tl-bar{flex:1;height:20px;background:var(--bg);border-radius:4px;overflow:hidden;position:relative}
|
|
90
|
+
.tl-fill{height:100%;border-radius:4px;display:flex;align-items:center;padding:0 .5rem;font-size:.7rem;color:#fff;font-weight:500;min-width:fit-content}
|
|
91
|
+
.tl-fill.pass{background:var(--g)}.tl-fill.fail{background:var(--r)}.tl-fill.skip{background:var(--y)}
|
|
92
|
+
.tl-name{width:200px;font-size:.8rem;text-align:right;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
93
|
+
.tl-dur{width:60px;font-size:.75rem;color:var(--dim);text-align:right}
|
|
94
|
+
|
|
95
|
+
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:999;align-items:center;justify-content:center}
|
|
96
|
+
.modal.show{display:flex}
|
|
97
|
+
.modal img{max-width:95vw;max-height:95vh;border-radius:8px}
|
|
98
|
+
.modal-close{position:fixed;top:1rem;right:1.5rem;color:#fff;font-size:2rem;cursor:pointer;z-index:1000}
|
|
99
|
+
|
|
100
|
+
footer{text-align:center;padding:2rem 0 1rem;color:var(--dim);font-size:.8rem;border-top:1px solid var(--border);margin-top:3rem}
|
|
101
|
+
</style></head>
|
|
56
102
|
<body>
|
|
57
|
-
<div class="
|
|
58
|
-
<h1>QABot Test Report</h1>
|
|
59
|
-
<div class="subtitle">${esc(meta.projectName || "")} • Feature: ${esc(meta.feature || "all")} • Env: ${esc(meta.environment || "local")} • ${esc(meta.timestamp?.slice(0, 19) || "")}</div>
|
|
103
|
+
<div class="wrap">
|
|
60
104
|
|
|
61
|
-
<div class="
|
|
62
|
-
<
|
|
63
|
-
<div class="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
<div class="hero">
|
|
106
|
+
<h1>QABot Test Report</h1>
|
|
107
|
+
<div class="meta">
|
|
108
|
+
<span>${esc(meta.projectName || "")}</span> ·
|
|
109
|
+
<span>Feature: <strong>${esc(meta.feature || "all")}</strong></span> ·
|
|
110
|
+
<span>Env: ${esc(meta.environment || "local")}</span> ·
|
|
111
|
+
<span>${esc(meta.timestamp?.slice(0, 16).replace("T", " ") || "")}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="rate-ring">
|
|
114
|
+
<svg width="120" height="120" viewBox="0 0 120 120">
|
|
115
|
+
<circle cx="60" cy="60" r="52" fill="none" stroke="var(--border)" stroke-width="8"/>
|
|
116
|
+
<circle cx="60" cy="60" r="52" fill="none" stroke="${(summary.overallPassRate || 0) >= 80 ? "var(--g)" : (summary.overallPassRate || 0) >= 50 ? "var(--y)" : "var(--r)"}" stroke-width="8" stroke-dasharray="${Math.round((summary.overallPassRate || 0) * 3.267)} 327" stroke-linecap="round"/>
|
|
117
|
+
</svg>
|
|
118
|
+
<div class="rate-text" style="color:${(summary.overallPassRate || 0) >= 80 ? "var(--g)" : (summary.overallPassRate || 0) >= 50 ? "var(--y)" : "var(--r)"}">${summary.overallPassRate || 0}%</div>
|
|
119
|
+
</div>
|
|
68
120
|
</div>
|
|
69
121
|
|
|
70
|
-
<div class="
|
|
122
|
+
<div class="cards">
|
|
123
|
+
<div class="card"><div class="lbl">Total</div><div class="val">${summary.totalTests || 0}</div></div>
|
|
124
|
+
<div class="card pass"><div class="lbl">Passed</div><div class="val">${summary.totalPassed || 0}</div></div>
|
|
125
|
+
<div class="card fail"><div class="lbl">Failed</div><div class="val">${summary.totalFailed || 0}</div></div>
|
|
126
|
+
<div class="card skip"><div class="lbl">Skipped</div><div class="val">${summary.totalSkipped || 0}</div></div>
|
|
127
|
+
<div class="card dur"><div class="lbl">Duration</div><div class="val">${fmtMs(meta.duration || summary.totalDuration || 0)}</div></div>
|
|
128
|
+
</div>
|
|
71
129
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
<h2>Duration Waterfall</h2>
|
|
131
|
+
<div class="timeline">
|
|
132
|
+
${allTests
|
|
133
|
+
.map((t) => {
|
|
134
|
+
const pct =
|
|
135
|
+
maxDuration > 0
|
|
136
|
+
? Math.max(2, Math.round(((t.duration || 0) / maxDuration) * 100))
|
|
137
|
+
: 2;
|
|
138
|
+
return `<div class="tl-item">
|
|
139
|
+
<div class="tl-name">${esc(t.name)}</div>
|
|
140
|
+
<div class="tl-bar"><div class="tl-fill ${t.status}" style="width:${pct}%">${fmtMs(t.duration || 0)}</div></div>
|
|
141
|
+
<div class="tl-dur">${fmtMs(t.duration || 0)}</div>
|
|
142
|
+
</div>`;
|
|
143
|
+
})
|
|
144
|
+
.join("\n")}
|
|
78
145
|
</div>
|
|
79
|
-
`,
|
|
80
|
-
)
|
|
81
|
-
.join("")}
|
|
82
146
|
|
|
83
|
-
<h2>
|
|
147
|
+
<h2>Test Results</h2>
|
|
84
148
|
<div class="filters">
|
|
85
|
-
<button class="
|
|
86
|
-
<button class="
|
|
87
|
-
<button class="
|
|
88
|
-
<button class="
|
|
149
|
+
<button class="fbtn on" onclick="filt('all',this)">All <span class="count">${allTests.length}</span></button>
|
|
150
|
+
<button class="fbtn" onclick="filt('passed',this)">Passed <span class="count">${passedTests.length}</span></button>
|
|
151
|
+
<button class="fbtn" onclick="filt('failed',this)">Failed <span class="count">${failedTests.length}</span></button>
|
|
152
|
+
<button class="fbtn" onclick="filt('skipped',this)">Skipped <span class="count">${skippedTests.length}</span></button>
|
|
89
153
|
</div>
|
|
90
154
|
|
|
91
|
-
<table>
|
|
92
|
-
<thead><tr><th>Status</th><th>Test
|
|
155
|
+
<table class="tbl">
|
|
156
|
+
<thead><tr><th class="idx">#</th><th>Status</th><th>Test</th><th>Duration</th><th>Details</th></tr></thead>
|
|
93
157
|
<tbody>
|
|
94
158
|
${allTests
|
|
95
159
|
.map(
|
|
96
|
-
(t) => `<tr class="
|
|
160
|
+
(t, i) => `<tr class="trow" data-s="${t.status}">
|
|
161
|
+
<td class="idx">${i + 1}</td>
|
|
97
162
|
<td><span class="badge badge-${t.status}">${t.status}</span></td>
|
|
98
|
-
<td
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</
|
|
163
|
+
<td>
|
|
164
|
+
<div class="test-name">${esc(t.name)}</div>
|
|
165
|
+
${t.suite ? `<div class="test-suite">${esc(t.suite)}</div>` : ""}
|
|
166
|
+
${t.file ? `<div class="test-file">${esc(t.file)}</div>` : ""}
|
|
167
|
+
</td>
|
|
168
|
+
<td>
|
|
169
|
+
<div class="dur-bar">
|
|
170
|
+
<div class="dur-fill" style="width:${maxDuration > 0 ? Math.max(2, Math.round(((t.duration || 0) / maxDuration) * 100)) : 2}%"></div>
|
|
171
|
+
${fmtMs(t.duration || 0)}
|
|
172
|
+
</div>
|
|
173
|
+
</td>
|
|
174
|
+
<td>${t.error || t.skipReason || (t.screenshots || []).length > 0 ? `<button class="fbtn" onclick="toggle(this)" style="font-size:.75rem">View</button>` : "—"}</td>
|
|
175
|
+
</tr>
|
|
176
|
+
${
|
|
177
|
+
t.error || t.skipReason || (t.screenshots || []).length > 0
|
|
178
|
+
? `<tr class="trow-detail" data-s="${t.status}" style="display:none">
|
|
179
|
+
<td colspan="5" style="padding:0 1rem 1rem">
|
|
180
|
+
${t.status === "skipped" ? `<div class="skip-reason">\u25B2 Skip Reason: ${esc(t.skipReason || t.error?.message || "No reason provided \u2014 test was marked skip or conditional skip")}</div>` : ""}
|
|
181
|
+
${t.error ? `<div class="io-section"><div class="io-label">Error Message</div><div class="err-box">${esc(t.error.message || "")}</div></div>` : ""}
|
|
182
|
+
${t.error?.stack ? `<div class="io-section"><div class="io-label">Stack Trace</div><div class="io-box">${esc(t.error.stack)}</div></div>` : ""}
|
|
183
|
+
${t.error?.expected ? `<div class="io-section"><div class="io-label">Expected</div><div class="io-box">${esc(String(t.error.expected))}</div></div>` : ""}
|
|
184
|
+
${t.error?.actual ? `<div class="io-section"><div class="io-label">Actual</div><div class="io-box">${esc(String(t.error.actual))}</div></div>` : ""}
|
|
185
|
+
${(t.screenshots || []).length > 0 ? `<div class="io-section"><div class="io-label">Screenshots</div><div class="ss-grid">${t.screenshots.map((s) => `<div class="ss-card"><img src="${esc(s)}" onclick="openImg(this.src)" loading="lazy"/><div class="ss-info">${esc(s.split("/").pop())}</div></div>`).join("")}</div></div>` : ""}
|
|
186
|
+
</td>
|
|
187
|
+
</tr>`
|
|
188
|
+
: ""
|
|
189
|
+
}`,
|
|
102
190
|
)
|
|
103
191
|
.join("\n")}
|
|
104
192
|
</tbody>
|
|
@@ -107,36 +195,101 @@ ${allTests
|
|
|
107
195
|
${
|
|
108
196
|
failedTests.length > 0
|
|
109
197
|
? `
|
|
110
|
-
<h2>Failed
|
|
198
|
+
<h2>Failed Tests \u2014 Detail</h2>
|
|
111
199
|
${failedTests
|
|
112
200
|
.map(
|
|
113
|
-
(t) => `
|
|
114
|
-
<div class="
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</div>
|
|
119
|
-
|
|
201
|
+
(t, i) => `
|
|
202
|
+
<div class="detail">
|
|
203
|
+
<div class="detail-head" onclick="toggleDetail(this)">
|
|
204
|
+
<div><span class="badge badge-failed">FAILED</span> <strong style="margin-left:.5rem">${esc(t.name)}</strong> <span style="color:var(--dim);font-size:.85rem;margin-left:.5rem">${fmtMs(t.duration || 0)}</span></div>
|
|
205
|
+
<span class="arrow">\u25BC</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="detail-body">
|
|
208
|
+
${t.file ? `<div class="test-file" style="margin-bottom:.75rem">${esc(t.file)} • ${esc(t.suite || "")}</div>` : ""}
|
|
209
|
+
${t.error ? `<div class="io-section"><div class="io-label">Error</div><div class="err-box">${esc(t.error.message || "")}</div></div>` : ""}
|
|
210
|
+
${t.error?.stack ? `<div class="io-section"><div class="io-label">Stack Trace</div><div class="io-box">${esc(t.error.stack)}</div></div>` : ""}
|
|
211
|
+
${
|
|
212
|
+
t.error?.expected !== undefined
|
|
213
|
+
? `<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:.75rem">
|
|
214
|
+
<div><div class="io-label">Expected</div><div class="io-box" style="border-color:var(--g)">${esc(String(t.error.expected))}</div></div>
|
|
215
|
+
<div><div class="io-label">Actual</div><div class="io-box" style="border-color:var(--r)">${esc(String(t.error.actual))}</div></div>
|
|
216
|
+
</div>`
|
|
217
|
+
: ""
|
|
218
|
+
}
|
|
219
|
+
${(t.screenshots || []).length > 0 ? `<div class="io-section"><div class="io-label">Screenshots</div><div class="ss-grid">${t.screenshots.map((s) => `<div class="ss-card"><img src="${esc(s)}" onclick="openImg(this.src)" loading="lazy"/></div>`).join("")}</div></div>` : ""}
|
|
220
|
+
</div>
|
|
221
|
+
</div>`,
|
|
120
222
|
)
|
|
121
223
|
.join("")}
|
|
122
224
|
`
|
|
123
225
|
: ""
|
|
124
226
|
}
|
|
125
227
|
|
|
126
|
-
|
|
228
|
+
${
|
|
229
|
+
skippedTests.length > 0
|
|
230
|
+
? `
|
|
231
|
+
<h2>Skipped Tests \u2014 Reasons</h2>
|
|
232
|
+
<table class="tbl"><thead><tr><th>#</th><th>Test</th><th>Reason</th></tr></thead><tbody>
|
|
233
|
+
${skippedTests
|
|
234
|
+
.map(
|
|
235
|
+
(t, i) => `<tr>
|
|
236
|
+
<td class="idx">${i + 1}</td>
|
|
237
|
+
<td><div class="test-name">${esc(t.name)}</div></td>
|
|
238
|
+
<td><div class="skip-reason" style="display:inline-block">${esc(t.skipReason || t.error?.message || "Marked as skip / conditional skip / prerequisite failed")}</div></td>
|
|
239
|
+
</tr>`,
|
|
240
|
+
)
|
|
241
|
+
.join("\n")}
|
|
242
|
+
</tbody></table>
|
|
243
|
+
`
|
|
244
|
+
: ""
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
${
|
|
248
|
+
screenshots.length > 0
|
|
249
|
+
? `
|
|
250
|
+
<h2>Screenshots Gallery</h2>
|
|
251
|
+
<div class="ss-grid">
|
|
252
|
+
${screenshots.map((s) => `<div class="ss-card"><img src="${esc(s.path)}" onclick="openImg(this.src)" loading="lazy"/><div class="ss-info">${esc(s.test)}</div></div>`).join("")}
|
|
253
|
+
</div>
|
|
254
|
+
`
|
|
255
|
+
: ""
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
<footer>Generated by QABot v${VERSION} · ${new Date().toISOString().slice(0, 19).replace("T", " ")}</footer>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="modal" id="modal" onclick="this.classList.remove('show')">
|
|
262
|
+
<span class="modal-close" onclick="document.getElementById('modal').classList.remove('show')">×</span>
|
|
263
|
+
<img id="modal-img" src="" />
|
|
127
264
|
</div>
|
|
128
265
|
|
|
129
266
|
<script>
|
|
130
|
-
function
|
|
131
|
-
document.querySelectorAll('.
|
|
132
|
-
|
|
133
|
-
document.querySelectorAll('.
|
|
134
|
-
r.style.display=
|
|
267
|
+
function filt(s,btn){
|
|
268
|
+
document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('on'));
|
|
269
|
+
btn.classList.add('on');
|
|
270
|
+
document.querySelectorAll('.trow,.trow-detail').forEach(r=>{
|
|
271
|
+
r.style.display=s==='all'||r.dataset.s===s?'':'none';
|
|
135
272
|
});
|
|
136
273
|
}
|
|
274
|
+
function toggle(btn){
|
|
275
|
+
const tr=btn.closest('tr');
|
|
276
|
+
const next=tr.nextElementSibling;
|
|
277
|
+
if(next&&next.classList.contains('trow-detail')){
|
|
278
|
+
next.style.display=next.style.display==='none'?'':'none';
|
|
279
|
+
btn.textContent=next.style.display==='none'?'View':'Hide';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function toggleDetail(el){
|
|
283
|
+
el.classList.toggle('open');
|
|
284
|
+
const body=el.nextElementSibling;
|
|
285
|
+
body.classList.toggle('show');
|
|
286
|
+
}
|
|
287
|
+
function openImg(src){
|
|
288
|
+
document.getElementById('modal-img').src=src;
|
|
289
|
+
document.getElementById('modal').classList.add('show');
|
|
290
|
+
}
|
|
137
291
|
</script>
|
|
138
|
-
</body
|
|
139
|
-
</html>`;
|
|
292
|
+
</body></html>`;
|
|
140
293
|
}
|
|
141
294
|
}
|
|
142
295
|
|
|
@@ -147,3 +300,9 @@ function esc(str) {
|
|
|
147
300
|
.replace(/>/g, ">")
|
|
148
301
|
.replace(/"/g, """);
|
|
149
302
|
}
|
|
303
|
+
|
|
304
|
+
function fmtMs(ms) {
|
|
305
|
+
if (ms < 1000) return `${ms}ms`;
|
|
306
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
307
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
308
|
+
}
|