@reportforge/playwright-pdf 0.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/CHANGELOG.md +285 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/ci-templates/azure-devops/azure-pipelines.yml +58 -0
- package/ci-templates/bitbucket/bitbucket-pipelines.yml +42 -0
- package/ci-templates/github-actions/basic-workflow.yml +44 -0
- package/ci-templates/github-actions/matrix-workflow.yml +69 -0
- package/ci-templates/gitlab-ci/.gitlab-ci.yml +42 -0
- package/ci-templates/jenkins/Jenkinsfile +54 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +12566 -0
- package/dist/index.mjs +12567 -0
- package/dist/templates/layouts/detailed.hbs +38 -0
- package/dist/templates/layouts/executive.hbs +35 -0
- package/dist/templates/layouts/minimal.hbs +29 -0
- package/dist/templates/partials/charts.hbs +306 -0
- package/dist/templates/partials/ci-environment.hbs +56 -0
- package/dist/templates/partials/cover-page.hbs +46 -0
- package/dist/templates/partials/defect-log.hbs +27 -0
- package/dist/templates/partials/executive-summary.hbs +60 -0
- package/dist/templates/partials/failure-deep-dive.hbs +61 -0
- package/dist/templates/partials/head.hbs +17 -0
- package/dist/templates/partials/release-gate.hbs +63 -0
- package/dist/templates/partials/requirements-matrix.hbs +35 -0
- package/dist/templates/partials/slow-tests.hbs +27 -0
- package/dist/templates/partials/suite-breakdown.hbs +58 -0
- package/dist/templates/partials/watermark.hbs +3 -0
- package/dist/templates/styles/base.css +234 -0
- package/dist/templates/styles/detailed.css +311 -0
- package/dist/templates/styles/executive.css +334 -0
- package/dist/templates/styles/minimal.css +208 -0
- package/package.json +99 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{{> head}}
|
|
2
|
+
|
|
3
|
+
{{> watermark}}
|
|
4
|
+
|
|
5
|
+
<div class="report-header">
|
|
6
|
+
{{#if meta.branding.logoBase64}}
|
|
7
|
+
<img class="report-header__logo" src="{{meta.branding.logoBase64}}" alt="Logo">
|
|
8
|
+
{{/if}}
|
|
9
|
+
<div>
|
|
10
|
+
<div class="report-header__title">{{meta.title}}</div>
|
|
11
|
+
<div class="report-header__meta">{{meta.projectName}} · {{formatDate meta.generatedAt}}</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
{{> executive-summary options=(obj showPassRate=true) }}
|
|
16
|
+
|
|
17
|
+
{{> charts options=(obj showCharts=true) }}
|
|
18
|
+
|
|
19
|
+
<div class="page-group">
|
|
20
|
+
{{> requirements-matrix }}
|
|
21
|
+
{{> ci-environment options=(obj showFullEnvironment=true) }}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{{> suite-breakdown options=(obj showRetries=true) }}
|
|
25
|
+
|
|
26
|
+
{{> failure-deep-dive options=(obj showFullFailures=true showStackTraces=true) }}
|
|
27
|
+
|
|
28
|
+
{{> slow-tests limit=10 showProject=true}}
|
|
29
|
+
|
|
30
|
+
{{> defect-log }}
|
|
31
|
+
|
|
32
|
+
<div class="report-footer">
|
|
33
|
+
<span>Generated by ReportForge</span>
|
|
34
|
+
<span>{{formatDate meta.generatedAt}}</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{{> head}}
|
|
2
|
+
|
|
3
|
+
{{> watermark}}
|
|
4
|
+
|
|
5
|
+
{{> cover-page}}
|
|
6
|
+
|
|
7
|
+
<div class="report-header">
|
|
8
|
+
<div class="report-header__left">
|
|
9
|
+
{{#if meta.branding.logoBase64}}
|
|
10
|
+
<img class="report-header__logo" src="{{meta.branding.logoBase64}}" alt="Logo">
|
|
11
|
+
{{/if}}
|
|
12
|
+
<div class="report-header__title">{{meta.title}}</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="report-header__date">{{formatDate meta.generatedAt}}</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
{{> release-gate}}
|
|
18
|
+
|
|
19
|
+
{{> executive-summary options=(obj showPassRate=true) }}
|
|
20
|
+
|
|
21
|
+
{{> charts options=(obj showCharts=true) }}
|
|
22
|
+
|
|
23
|
+
{{> slow-tests limit=5 showProject=false}}
|
|
24
|
+
|
|
25
|
+
{{> failure-deep-dive options=(obj showFullFailures=false showStackTraces=false) }}
|
|
26
|
+
|
|
27
|
+
{{> ci-environment options=(obj showFullEnvironment=false) }}
|
|
28
|
+
|
|
29
|
+
<div class="report-footer">
|
|
30
|
+
<span>{{meta.projectName}} · Playwright Test Report</span>
|
|
31
|
+
<span>Generated by ReportForge · {{formatDate meta.generatedAt}}</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{{> head}}
|
|
2
|
+
|
|
3
|
+
{{> watermark}}
|
|
4
|
+
|
|
5
|
+
<div class="report-header">
|
|
6
|
+
{{#if meta.branding.logoBase64}}
|
|
7
|
+
<img class="report-header__logo" src="{{meta.branding.logoBase64}}" alt="Logo">
|
|
8
|
+
{{/if}}
|
|
9
|
+
<div>
|
|
10
|
+
<div class="report-header__title">{{meta.title}}</div>
|
|
11
|
+
<div class="report-header__meta">{{meta.projectName}} · {{formatDate meta.generatedAt}}</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
{{> executive-summary options=(obj showPassRate=true showCharts=false) }}
|
|
16
|
+
|
|
17
|
+
{{> suite-breakdown options=(obj showRetries=true) }}
|
|
18
|
+
|
|
19
|
+
{{> failure-deep-dive options=(obj showFullFailures=true showStackTraces=true) }}
|
|
20
|
+
|
|
21
|
+
{{> ci-environment options=(obj showFullEnvironment=false) }}
|
|
22
|
+
|
|
23
|
+
<div class="report-footer">
|
|
24
|
+
<span>Generated by ReportForge</span>
|
|
25
|
+
<span>{{formatDate meta.generatedAt}}</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
{{#if options.showCharts}}
|
|
2
|
+
<div class="section charts-grid">
|
|
3
|
+
<div class="chart-card">
|
|
4
|
+
<h3>Pass Rate</h3>
|
|
5
|
+
<canvas id="passRateChart" width="186" height="186"></canvas>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="chart-card">
|
|
8
|
+
<h3>Suite Results</h3>
|
|
9
|
+
<canvas id="suitesBarChart" width="390" height="186"></canvas>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
{{!-- Trend card: detailed + executive. Minimal skipped (no charts at all).
|
|
14
|
+
In multi-template runs generateAll overrides meta.template per render pass. --}}
|
|
15
|
+
{{#unless (eq meta.template 'minimal')}}
|
|
16
|
+
{{#if charts.trend}}
|
|
17
|
+
<div class="trend-card">
|
|
18
|
+
<div class="trend-card__header">
|
|
19
|
+
<span class="trend-card__title">Pass rate trend — last {{charts.trend.entries.length}} runs</span>
|
|
20
|
+
{{!-- Delta badge: falsy guard handles null (< 2 entries) and 0 (identical runs). --}}
|
|
21
|
+
{{#if charts.trend.delta}}
|
|
22
|
+
<span class="delta {{#if (gt charts.trend.delta 0)}}delta-up{{else}}delta-down{{/if}}">
|
|
23
|
+
{{#if (gt charts.trend.delta 0)}}↑{{else}}↓{{/if}} {{absFixed charts.trend.delta}}% vs previous run
|
|
24
|
+
</span>
|
|
25
|
+
{{/if}}
|
|
26
|
+
</div>
|
|
27
|
+
<canvas id="trendChart" width="680" height="160"></canvas>
|
|
28
|
+
{{!-- Run history table is populated by JS below; placeholder must exist before script runs. --}}
|
|
29
|
+
<div class="trend-run-history" id="run-history-table"></div>
|
|
30
|
+
</div>
|
|
31
|
+
{{/if}}
|
|
32
|
+
{{/unless}}
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
(function() {
|
|
36
|
+
var remaining = 0;
|
|
37
|
+
function chartDone() { if (--remaining === 0) window.__chartsReady = true; }
|
|
38
|
+
|
|
39
|
+
var passed = {{charts.passRate.passed}};
|
|
40
|
+
var failed = {{charts.passRate.failed}};
|
|
41
|
+
var skipped = {{charts.passRate.skipped}};
|
|
42
|
+
var flaky = {{charts.passRate.flaky}};
|
|
43
|
+
var total = passed + failed + skipped + flaky;
|
|
44
|
+
var passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
|
45
|
+
|
|
46
|
+
/* ── Centre-text plugin for the doughnut ─────────────────────────────── */
|
|
47
|
+
var centerTextPlugin = {
|
|
48
|
+
id: 'centerText',
|
|
49
|
+
afterDatasetsDraw: function(chart) {
|
|
50
|
+
if (chart.canvas.id !== 'passRateChart') return;
|
|
51
|
+
var ctx = chart.ctx;
|
|
52
|
+
var cx = (chart.chartArea.left + chart.chartArea.right) / 2;
|
|
53
|
+
var cy = (chart.chartArea.top + chart.chartArea.bottom) / 2;
|
|
54
|
+
ctx.save();
|
|
55
|
+
ctx.textAlign = 'center';
|
|
56
|
+
ctx.textBaseline = 'middle';
|
|
57
|
+
/* large percentage */
|
|
58
|
+
ctx.font = '700 28px "Source Serif Pro", Georgia, serif';
|
|
59
|
+
ctx.fillStyle = '#1F1F1C';
|
|
60
|
+
ctx.fillText(passRate + '%', cx, cy - 7);
|
|
61
|
+
/* label below */
|
|
62
|
+
ctx.font = '600 9px Inter, sans-serif';
|
|
63
|
+
ctx.fillStyle = '#8C8B82';
|
|
64
|
+
ctx.fillText('PASS RATE', cx, cy + 13);
|
|
65
|
+
ctx.restore();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
Chart.register(centerTextPlugin);
|
|
69
|
+
|
|
70
|
+
/* ── Pass rate doughnut ──────────────────────────────────────────────── */
|
|
71
|
+
remaining += 1;
|
|
72
|
+
new Chart(document.getElementById('passRateChart'), {
|
|
73
|
+
type: 'doughnut',
|
|
74
|
+
data: {
|
|
75
|
+
labels: ['Passed', 'Failed', 'Skipped', 'Flaky'],
|
|
76
|
+
datasets: [{
|
|
77
|
+
data: [passed, failed, skipped, flaky],
|
|
78
|
+
backgroundColor: ['#3F7D58', '#C3553C', '#7A786E', '#D49452'],
|
|
79
|
+
borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
|
|
80
|
+
borderWidth: 2,
|
|
81
|
+
hoverOffset: 4
|
|
82
|
+
}]
|
|
83
|
+
},
|
|
84
|
+
options: {
|
|
85
|
+
responsive: false,
|
|
86
|
+
cutout: '68%',
|
|
87
|
+
plugins: {
|
|
88
|
+
legend: {
|
|
89
|
+
position: 'bottom',
|
|
90
|
+
labels: {
|
|
91
|
+
font: { size: 9, family: 'Inter, sans-serif' },
|
|
92
|
+
color: '#5A5A52', padding: 10, boxWidth: 10, boxHeight: 10
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
animation: { onComplete: chartDone }
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/* ── Horizontal suite results bar ────────────────────────────────────── */
|
|
101
|
+
var suiteLabels = [{{{jsonArray charts.suiteResults 'label'}}}];
|
|
102
|
+
var suitePassed = [{{{jsonArray charts.suiteResults 'passed'}}}];
|
|
103
|
+
var suiteFailed = [{{{jsonArray charts.suiteResults 'failed'}}}];
|
|
104
|
+
var suiteSkipped = [{{{jsonArray charts.suiteResults 'skipped'}}}];
|
|
105
|
+
|
|
106
|
+
remaining += 1;
|
|
107
|
+
new Chart(document.getElementById('suitesBarChart'), {
|
|
108
|
+
type: 'bar',
|
|
109
|
+
data: {
|
|
110
|
+
labels: suiteLabels,
|
|
111
|
+
datasets: [
|
|
112
|
+
{ label: 'Passed', data: suitePassed, backgroundColor: '#3F7D58', borderRadius: 3 },
|
|
113
|
+
{ label: 'Failed', data: suiteFailed, backgroundColor: '#C3553C', borderRadius: 3 },
|
|
114
|
+
{ label: 'Skipped', data: suiteSkipped, backgroundColor: '#7A786E', borderRadius: 3 }
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
options: {
|
|
118
|
+
indexAxis: 'y',
|
|
119
|
+
responsive: false,
|
|
120
|
+
scales: {
|
|
121
|
+
x: {
|
|
122
|
+
stacked: true,
|
|
123
|
+
ticks: { font: { size: 9, family: 'Inter, sans-serif' }, color: '#5A5A52', stepSize: 1 },
|
|
124
|
+
grid: { color: 'rgba(31,31,28,0.06)' },
|
|
125
|
+
border: { color: '#D6D4C8' }
|
|
126
|
+
},
|
|
127
|
+
y: {
|
|
128
|
+
stacked: true,
|
|
129
|
+
ticks: { font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C' },
|
|
130
|
+
grid: { display: false },
|
|
131
|
+
border: { color: '#D6D4C8' }
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
plugins: {
|
|
135
|
+
legend: {
|
|
136
|
+
position: 'bottom',
|
|
137
|
+
labels: {
|
|
138
|
+
font: { size: 9, family: 'Inter, sans-serif' }, color: '#5A5A52',
|
|
139
|
+
padding: 12, boxWidth: 10, boxHeight: 10
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
animation: { onComplete: chartDone }
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
{{#unless (eq meta.template 'minimal')}}{{#if charts.trend}}
|
|
148
|
+
// entries are newest-first in storage; reverse for oldest-left (chronological) display
|
|
149
|
+
var trendRaw = {{{safeJsonData charts.trend.entries}}};
|
|
150
|
+
var trendData = trendRaw.slice().reverse();
|
|
151
|
+
var trendMonths = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
152
|
+
|
|
153
|
+
var trendLabels = trendData.map(function(e) {
|
|
154
|
+
var d = new Date(e.timestamp);
|
|
155
|
+
return trendMonths[d.getMonth()] + ' ' + d.getDate() + ' ' +
|
|
156
|
+
String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
157
|
+
});
|
|
158
|
+
var trendRates = trendData.map(function(e) { return e.passRate; });
|
|
159
|
+
|
|
160
|
+
// Points colored by verdict for instant visual distinction across runs
|
|
161
|
+
var trendPointColors = trendData.map(function(e) {
|
|
162
|
+
return e.verdict === 'passed' ? '#3F7D58' :
|
|
163
|
+
e.verdict === 'failed' ? '#C3553C' :
|
|
164
|
+
e.verdict === 'partial' ? '#CA8A04' : '#7A786E';
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Smart y-axis: pad above/below the actual data range so variation is readable
|
|
168
|
+
// rather than a flat line at the top of a 0–100 axis when all runs pass at 96%.
|
|
169
|
+
var minRate = Math.min.apply(null, trendRates);
|
|
170
|
+
var maxRate = Math.max.apply(null, trendRates);
|
|
171
|
+
var yPad = Math.max(8, Math.round((maxRate - minRate) * 0.4));
|
|
172
|
+
var yMin = Math.max(0, minRate - yPad);
|
|
173
|
+
var yMax = Math.min(100, maxRate + yPad);
|
|
174
|
+
|
|
175
|
+
// Data-label plugin: renders pass-rate % above each point.
|
|
176
|
+
// Canvas ID guard prevents firing on the doughnut and bar charts.
|
|
177
|
+
var trendLabelPlugin = {
|
|
178
|
+
id: 'trendLabels',
|
|
179
|
+
afterDatasetsDraw: function(chart) {
|
|
180
|
+
if (chart.canvas.id !== 'trendChart') return;
|
|
181
|
+
var ctx2d = chart.ctx;
|
|
182
|
+
var meta = chart.getDatasetMeta(0);
|
|
183
|
+
var ds = chart.data.datasets[0];
|
|
184
|
+
meta.data.forEach(function(pt, i) {
|
|
185
|
+
ctx2d.save();
|
|
186
|
+
ctx2d.font = '700 8.5px Inter, sans-serif';
|
|
187
|
+
ctx2d.fillStyle = trendPointColors[i];
|
|
188
|
+
ctx2d.textAlign = 'center';
|
|
189
|
+
ctx2d.textBaseline = 'bottom';
|
|
190
|
+
ctx2d.fillText(ds.data[i] + '%', pt.x, pt.y - 7);
|
|
191
|
+
ctx2d.restore();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
Chart.register(trendLabelPlugin);
|
|
196
|
+
|
|
197
|
+
var trendCanvas = document.getElementById('trendChart');
|
|
198
|
+
var trendCtx2d = trendCanvas.getContext('2d');
|
|
199
|
+
var trendGrad = trendCtx2d.createLinearGradient(0, 0, 0, 160);
|
|
200
|
+
trendGrad.addColorStop(0, 'rgba(204,120,92,0.22)');
|
|
201
|
+
trendGrad.addColorStop(1, 'rgba(204,120,92,0.02)');
|
|
202
|
+
|
|
203
|
+
remaining += 1;
|
|
204
|
+
new Chart(trendCanvas, {
|
|
205
|
+
type: 'line',
|
|
206
|
+
data: {
|
|
207
|
+
labels: trendLabels,
|
|
208
|
+
datasets: [{
|
|
209
|
+
data: trendRates,
|
|
210
|
+
borderColor: '#CC785C',
|
|
211
|
+
backgroundColor: trendGrad,
|
|
212
|
+
fill: true,
|
|
213
|
+
tension: 0.35,
|
|
214
|
+
pointRadius: 5,
|
|
215
|
+
pointBackgroundColor: trendPointColors,
|
|
216
|
+
pointBorderColor: '#FFFFFF',
|
|
217
|
+
pointBorderWidth: 2,
|
|
218
|
+
borderWidth: 2,
|
|
219
|
+
}]
|
|
220
|
+
},
|
|
221
|
+
options: {
|
|
222
|
+
animation: { onComplete: chartDone },
|
|
223
|
+
responsive: false,
|
|
224
|
+
layout: { padding: { top: 22, right: 10, bottom: 4, left: 4 } },
|
|
225
|
+
scales: {
|
|
226
|
+
y: {
|
|
227
|
+
min: yMin,
|
|
228
|
+
max: yMax,
|
|
229
|
+
ticks: {
|
|
230
|
+
callback: function(v) { return v + '%'; },
|
|
231
|
+
font: { size: 8.5, family: 'Inter, sans-serif' },
|
|
232
|
+
color: '#8C8B82',
|
|
233
|
+
count: 5,
|
|
234
|
+
},
|
|
235
|
+
grid: { color: 'rgba(31,31,28,0.07)' },
|
|
236
|
+
border: { color: 'transparent' },
|
|
237
|
+
},
|
|
238
|
+
x: {
|
|
239
|
+
ticks: {
|
|
240
|
+
font: { size: 8, family: 'Inter, sans-serif' },
|
|
241
|
+
color: '#8C8B82',
|
|
242
|
+
maxRotation: 0,
|
|
243
|
+
},
|
|
244
|
+
grid: { display: false },
|
|
245
|
+
border: { color: '#E5E3D8' },
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
plugins: { legend: { display: false } },
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Run history table — oldest-first (top→bottom matches chart left→right).
|
|
253
|
+
// Built via DOM methods (not innerHTML) so no HTML-injection risk.
|
|
254
|
+
var histEl = document.getElementById('run-history-table');
|
|
255
|
+
if (histEl) {
|
|
256
|
+
trendData.forEach(function(e) {
|
|
257
|
+
var d = new Date(e.timestamp);
|
|
258
|
+
var dateStr = trendMonths[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() +
|
|
259
|
+
' · ' +
|
|
260
|
+
String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
261
|
+
var durStr = e.duration >= 60000
|
|
262
|
+
? Math.floor(e.duration / 60000) + 'm ' + Math.round((e.duration % 60000) / 1000) + 's'
|
|
263
|
+
: (e.duration / 1000).toFixed(1) + 's';
|
|
264
|
+
var dotCls = e.verdict === 'passed' ? 'verdict-passed' :
|
|
265
|
+
e.verdict === 'failed' ? 'verdict-failed' :
|
|
266
|
+
e.verdict === 'partial' ? 'verdict-partial' : 'verdict-unknown';
|
|
267
|
+
var rateCls = e.verdict === 'passed' ? 'rh-rate--pass' :
|
|
268
|
+
e.verdict === 'failed' ? 'rh-rate--fail' :
|
|
269
|
+
e.verdict === 'partial' ? 'rh-rate--partial' : '';
|
|
270
|
+
|
|
271
|
+
var row = document.createElement('div');
|
|
272
|
+
row.className = 'rh-item rh-item--' + e.verdict;
|
|
273
|
+
|
|
274
|
+
var dot = document.createElement('span');
|
|
275
|
+
dot.className = 'rh-dot verdict-dot ' + dotCls;
|
|
276
|
+
row.appendChild(dot);
|
|
277
|
+
|
|
278
|
+
var dateEl = document.createElement('span');
|
|
279
|
+
dateEl.className = 'rh-date';
|
|
280
|
+
dateEl.textContent = dateStr;
|
|
281
|
+
row.appendChild(dateEl);
|
|
282
|
+
|
|
283
|
+
var rateEl = document.createElement('span');
|
|
284
|
+
rateEl.className = 'rh-rate' + (rateCls ? ' ' + rateCls : '');
|
|
285
|
+
rateEl.textContent = e.passRate + '%';
|
|
286
|
+
row.appendChild(rateEl);
|
|
287
|
+
|
|
288
|
+
var testsEl = document.createElement('span');
|
|
289
|
+
testsEl.className = 'rh-tests';
|
|
290
|
+
testsEl.textContent = e.total + ' tests';
|
|
291
|
+
row.appendChild(testsEl);
|
|
292
|
+
|
|
293
|
+
var durEl = document.createElement('span');
|
|
294
|
+
durEl.className = 'rh-dur';
|
|
295
|
+
durEl.textContent = durStr;
|
|
296
|
+
row.appendChild(durEl);
|
|
297
|
+
|
|
298
|
+
histEl.appendChild(row);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
{{/if}}{{/unless}}
|
|
302
|
+
})();
|
|
303
|
+
</script>
|
|
304
|
+
{{else}}
|
|
305
|
+
<script>window.__chartsReady = true;</script>
|
|
306
|
+
{{/if}}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<div class="section">
|
|
2
|
+
<h2>Environment & CI</h2>
|
|
3
|
+
{{#if options.showFullEnvironment}}
|
|
4
|
+
<table class="env-table">
|
|
5
|
+
<tbody>
|
|
6
|
+
<tr><td>CI Provider</td><td class="env-value">{{environment.ciProvider}}</td></tr>
|
|
7
|
+
<tr><td>Branch</td> <td class="env-value">{{environment.branch}}</td></tr>
|
|
8
|
+
<tr><td>Commit</td> <td class="env-value">{{environment.commit}}</td></tr>
|
|
9
|
+
{{#if environment.buildUrl}}
|
|
10
|
+
<tr><td>Build URL</td> <td class="env-value"><a href="{{environment.buildUrl}}" style="color:var(--primary,#CC785C);">{{environment.buildUrl}}</a></td></tr>
|
|
11
|
+
{{/if}}
|
|
12
|
+
<tr><td>Browsers</td> <td class="env-value">{{join environment.browsers ", "}}</td></tr>
|
|
13
|
+
<tr><td>OS</td> <td class="env-value">{{environment.os}}</td></tr>
|
|
14
|
+
<tr><td>Node.js</td> <td class="env-value">{{environment.nodeVersion}}</td></tr>
|
|
15
|
+
<tr><td>Playwright</td> <td class="env-value">v{{environment.playwrightVersion}}</td></tr>
|
|
16
|
+
<tr><td>Projects</td> <td class="env-value">{{environment.projectCount}}</td></tr>
|
|
17
|
+
<tr><td>Workers</td> <td class="env-value">{{environment.workerCount}}</td></tr>
|
|
18
|
+
</tbody>
|
|
19
|
+
</table>
|
|
20
|
+
{{else}}
|
|
21
|
+
<div class="env-grid">
|
|
22
|
+
<div class="env-item">
|
|
23
|
+
<span class="env-item__key">Branch</span>
|
|
24
|
+
<span class="env-item__val">{{environment.branch}}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="env-item">
|
|
27
|
+
<span class="env-item__key">Commit</span>
|
|
28
|
+
<span class="env-item__val">{{environment.commit}}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="env-item">
|
|
31
|
+
<span class="env-item__key">Browsers</span>
|
|
32
|
+
<span class="env-item__val">{{join environment.browsers ", "}}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="env-item">
|
|
35
|
+
<span class="env-item__key">CI</span>
|
|
36
|
+
<span class="env-item__val">{{environment.ciProvider}}</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="env-item">
|
|
39
|
+
<span class="env-item__key">Node</span>
|
|
40
|
+
<span class="env-item__val">{{environment.nodeVersion}}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="env-item">
|
|
43
|
+
<span class="env-item__key">Playwright</span>
|
|
44
|
+
<span class="env-item__val">v{{environment.playwrightVersion}}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="env-item">
|
|
47
|
+
<span class="env-item__key">Projects</span>
|
|
48
|
+
<span class="env-item__val">{{environment.projectCount}}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="env-item">
|
|
51
|
+
<span class="env-item__key">Workers</span>
|
|
52
|
+
<span class="env-item__val">{{environment.workerCount}}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
{{/if}}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<div class="cover-page">
|
|
2
|
+
{{#if meta.branding.logoBase64}}
|
|
3
|
+
<img class="cover-page__logo" src="{{meta.branding.logoBase64}}" alt="Logo">
|
|
4
|
+
{{/if}}
|
|
5
|
+
|
|
6
|
+
<div class="cover-page__title">{{meta.title}}</div>
|
|
7
|
+
<div class="cover-page__subtitle">{{meta.projectName}}</div>
|
|
8
|
+
|
|
9
|
+
<div class="cover-page__divider"></div>
|
|
10
|
+
|
|
11
|
+
<span class="cover-page__verdict cover-page__verdict--{{stats.verdict}}">
|
|
12
|
+
{{upperCase stats.verdict}}
|
|
13
|
+
</span>
|
|
14
|
+
|
|
15
|
+
<div class="cover-page__stats-strip">
|
|
16
|
+
<div class="cover-page__stat">
|
|
17
|
+
<div class="cover-page__stat-val">{{stats.total}}</div>
|
|
18
|
+
<div class="cover-page__stat-lbl">Tests</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="cover-page__stat">
|
|
21
|
+
<div class="cover-page__stat-val" style="color:var(--pass);">{{stats.passed}}</div>
|
|
22
|
+
<div class="cover-page__stat-lbl">Passed</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="cover-page__stat">
|
|
25
|
+
<div class="cover-page__stat-val" style="color:var(--fail);">{{stats.failed}}</div>
|
|
26
|
+
<div class="cover-page__stat-lbl">Failed</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="cover-page__stat">
|
|
29
|
+
<div class="cover-page__stat-val" style="color:var(--pass);">{{stats.passRate}}%</div>
|
|
30
|
+
<div class="cover-page__stat-lbl">Pass Rate</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="cover-page__stat">
|
|
33
|
+
<div class="cover-page__stat-val">{{formatDuration stats.duration}}</div>
|
|
34
|
+
<div class="cover-page__stat-lbl">Duration</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="cover-page__meta">
|
|
39
|
+
Generated {{formatDate meta.generatedAt}}
|
|
40
|
+
</div>
|
|
41
|
+
<div class="cover-page__meta">
|
|
42
|
+
Branch: <strong style="color:var(--text-1);">{{environment.branch}}</strong>
|
|
43
|
+
·
|
|
44
|
+
Commit: <strong style="color:var(--text-1);">{{environment.commit}}</strong>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{#if failures.length}}
|
|
2
|
+
<div class="section">
|
|
3
|
+
<h2>Defect Log</h2>
|
|
4
|
+
<table>
|
|
5
|
+
<thead>
|
|
6
|
+
<tr>
|
|
7
|
+
<th style="width:58px;">ID</th>
|
|
8
|
+
<th>Title</th>
|
|
9
|
+
<th style="width:100px;">Suite</th>
|
|
10
|
+
<th style="width:80px;">Severity</th>
|
|
11
|
+
<th style="width:55px;text-align:center;">Retries</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
{{#each failures}}
|
|
16
|
+
<tr class="no-break">
|
|
17
|
+
<td class="defect-id">DEF-{{addOne @index}}</td>
|
|
18
|
+
<td style="font-weight:500;">{{testTitle}}</td>
|
|
19
|
+
<td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{lastItem suitePath}}</td>
|
|
20
|
+
<td><span class="severity--{{severity}}">{{upperCase severity}}</span></td>
|
|
21
|
+
<td style="text-align:center;color:var(--text-3);">{{retryCount}}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
{{/each}}
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
</div>
|
|
27
|
+
{{/if}}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<div class="section">
|
|
2
|
+
<span class="verdict verdict--{{stats.verdict}}">
|
|
3
|
+
{{upperCase stats.verdict}}
|
|
4
|
+
</span>
|
|
5
|
+
|
|
6
|
+
<div class="kpi-strip">
|
|
7
|
+
<div class="kpi-card kpi-card--total">
|
|
8
|
+
<div class="kpi-card__value">{{stats.total}}</div>
|
|
9
|
+
<div class="kpi-card__label">Total</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="kpi-card kpi-card--pass">
|
|
12
|
+
<div class="kpi-card__value">{{stats.passed}}</div>
|
|
13
|
+
<div class="kpi-card__label">Passed</div>
|
|
14
|
+
{{#if options.showPassRate}}
|
|
15
|
+
<div class="kpi-card__rate">{{stats.passRate}}%</div>
|
|
16
|
+
{{/if}}
|
|
17
|
+
</div>
|
|
18
|
+
<div class="kpi-card kpi-card--fail">
|
|
19
|
+
<div class="kpi-card__value">{{stats.failed}}</div>
|
|
20
|
+
<div class="kpi-card__label">Failed</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="kpi-card kpi-card--skip">
|
|
23
|
+
<div class="kpi-card__value">{{stats.skipped}}</div>
|
|
24
|
+
<div class="kpi-card__label">Skipped</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="kpi-card kpi-card--flaky">
|
|
27
|
+
<div class="kpi-card__value">{{stats.flaky}}</div>
|
|
28
|
+
<div class="kpi-card__label">Flaky</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="summary-row">
|
|
33
|
+
<div class="summary-row__item">
|
|
34
|
+
<span class="summary-row__label">Duration</span>
|
|
35
|
+
<span class="summary-row__value">{{formatDuration stats.duration}}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="summary-row__item">
|
|
38
|
+
<span class="summary-row__label">Pass Rate</span>
|
|
39
|
+
<span class="summary-row__value" style="color:var(--pass);">{{stats.passRate}}%</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="summary-row__item">
|
|
42
|
+
<span class="summary-row__label">Generated</span>
|
|
43
|
+
<span class="summary-row__value">{{formatDate meta.generatedAt}}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="summary-row__item">
|
|
46
|
+
<span class="summary-row__label">Project</span>
|
|
47
|
+
<span class="summary-row__value">{{meta.projectName}}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{{#if stats.flaky}}
|
|
52
|
+
<div class="flaky-callout">
|
|
53
|
+
<span class="flaky-callout__icon">⚠</span>
|
|
54
|
+
<div class="flaky-callout__body">
|
|
55
|
+
<span class="flaky-callout__title">{{stats.flaky}} Flaky Test{{#if (gt stats.flaky 1)}}s{{/if}} Detected</span>
|
|
56
|
+
<span class="flaky-callout__text">These tests passed on retry. Results may vary across re-runs. Investigate before shipping to production.</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
{{/if}}
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{{#if failures.length}}
|
|
2
|
+
<div class="section page-break">
|
|
3
|
+
<h2>Failures ({{failures.length}}{{#if overflowFailureCount}} shown · {{overflowFailureCount}} more in sidecar{{/if}})</h2>
|
|
4
|
+
{{#if overflowFailureCount}}
|
|
5
|
+
<div class="overflow-banner">
|
|
6
|
+
Showing the first {{failures.length}} failures. {{overflowFailureCount}} additional
|
|
7
|
+
failure{{#if (gt overflowFailureCount 1)}}s are{{else}} is{{/if}} available in
|
|
8
|
+
<code>{{overflowSidecarName}}</code> alongside this report.
|
|
9
|
+
</div>
|
|
10
|
+
{{/if}}
|
|
11
|
+
{{#each failures}}
|
|
12
|
+
<div class="failure-item no-break">
|
|
13
|
+
{{#if ../options.showFullFailures}}
|
|
14
|
+
|
|
15
|
+
<div class="failure-item__header">
|
|
16
|
+
<div>
|
|
17
|
+
<div class="failure-item__title">{{testTitle}}</div>
|
|
18
|
+
<div class="failure-item__path">{{join suitePath " › "}}</div>
|
|
19
|
+
</div>
|
|
20
|
+
<span class="badge badge--failed severity--{{severity}}" style="white-space:nowrap;">
|
|
21
|
+
{{upperCase severity}}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="failure-item__message">{{error.message}}</div>
|
|
26
|
+
|
|
27
|
+
{{#if error.stack}}
|
|
28
|
+
{{#if ../options.showStackTraces}}
|
|
29
|
+
<pre>{{error.stack}}</pre>
|
|
30
|
+
{{/if}}
|
|
31
|
+
{{/if}}
|
|
32
|
+
|
|
33
|
+
{{#if screenshotBase64}}
|
|
34
|
+
<img class="screenshot" src="{{screenshotBase64}}" alt="Screenshot at failure">
|
|
35
|
+
{{/if}}
|
|
36
|
+
|
|
37
|
+
{{#if retryHistory.length}}
|
|
38
|
+
<div class="retry-history">
|
|
39
|
+
Retry history:
|
|
40
|
+
{{#each retryHistory}}
|
|
41
|
+
<span class="retry-attempt retry-attempt--{{status}}">
|
|
42
|
+
#{{attempt}} {{status}} · {{formatDuration duration}}
|
|
43
|
+
</span>
|
|
44
|
+
{{/each}}
|
|
45
|
+
</div>
|
|
46
|
+
{{/if}}
|
|
47
|
+
|
|
48
|
+
{{else}}
|
|
49
|
+
{{!-- Executive: title + suite only --}}
|
|
50
|
+
<div class="failure-summary-item">
|
|
51
|
+
<div>
|
|
52
|
+
<div class="failure-summary-item__title">{{testTitle}}</div>
|
|
53
|
+
<div class="failure-summary-item__suite">{{join suitePath " › "}}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<span class="badge badge--failed">FAILED</span>
|
|
56
|
+
</div>
|
|
57
|
+
{{/if}}
|
|
58
|
+
</div>
|
|
59
|
+
{{/each}}
|
|
60
|
+
</div>
|
|
61
|
+
{{/if}}
|