k6-modern-reporter 1.0.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/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/assets/checks.jpeg +0 -0
- package/assets/metrics.jpeg +0 -0
- package/assets/overview.jpeg +0 -0
- package/assets/thresholds.jpeg +0 -0
- package/k6-modern-reporter.js +1291 -0
- package/package.json +28 -0
- package/reports/test-reporter-2026-01-31T16-39-41.613Z.html +847 -0
- package/test-reporter.ts +59 -0
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern K6 HTML Reporter with Modern UI
|
|
3
|
+
*
|
|
4
|
+
* This module generates beautiful, modern HTML reports for k6 performance tests.
|
|
5
|
+
* Features include:
|
|
6
|
+
* - Responsive design with gradient UI
|
|
7
|
+
* - Interactive tabs for different sections
|
|
8
|
+
* - Visual charts and progress bars
|
|
9
|
+
* - Detailed metrics, checks, and threshold reporting
|
|
10
|
+
* - Support for custom test information
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main entry point for generating HTML reports
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} data - The k6 test results data object containing metrics, checks, thresholds, etc.
|
|
17
|
+
* @param {Object} options - Configuration options for the report
|
|
18
|
+
* @param {string} options.title - Main title for the report (defaults to current timestamp)
|
|
19
|
+
* @param {string} options.subtitle - Subtitle or endpoint description
|
|
20
|
+
* @param {string} options.httpMethod - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
21
|
+
* @param {Object} options.additionalInfo - Key-value pairs of additional test information to display
|
|
22
|
+
* @param {boolean} options.debug - If true, logs the raw k6 data to console
|
|
23
|
+
* @returns {string} Complete HTML document as a string
|
|
24
|
+
*/
|
|
25
|
+
export function htmlReport(data, options = {}) {
|
|
26
|
+
// Extract options with default values
|
|
27
|
+
const title = options.title || new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
28
|
+
const subtitle = options.subtitle || '';
|
|
29
|
+
const httpMethod = options.httpMethod || '';
|
|
30
|
+
const additionalInfo = options.additionalInfo || {};
|
|
31
|
+
const debug = options.debug || false;
|
|
32
|
+
|
|
33
|
+
console.log("[k6-reporter-modern] Generating modern HTML summary report");
|
|
34
|
+
|
|
35
|
+
// Debug mode: print raw k6 data to console for troubleshooting
|
|
36
|
+
if (debug) {
|
|
37
|
+
console.log(JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Calculate summary statistics from k6 metrics data
|
|
41
|
+
const stats = calculateStats(data);
|
|
42
|
+
|
|
43
|
+
// Generate and return the complete HTML report
|
|
44
|
+
return generateModernHTML(data, title, subtitle, httpMethod, additionalInfo, stats);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates the complete HTML document structure
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} data - The k6 test results data
|
|
51
|
+
* @param {string} title - Report title
|
|
52
|
+
* @param {string} subtitle - Report subtitle/endpoint
|
|
53
|
+
* @param {string} httpMethod - HTTP method for the API call
|
|
54
|
+
* @param {Object} additionalInfo - Additional test configuration info
|
|
55
|
+
* @param {Object} stats - Pre-calculated statistics
|
|
56
|
+
* @returns {string} Complete HTML document
|
|
57
|
+
*/
|
|
58
|
+
function generateModernHTML(data, title, subtitle, httpMethod, additionalInfo, stats) {
|
|
59
|
+
// Determine overall test status (pass/fail) based on errors, check failures, and threshold failures
|
|
60
|
+
const testStatus = stats.failedRequests === 0 && stats.checkFailures === 0 && stats.thresholdFailures === 0;
|
|
61
|
+
|
|
62
|
+
return `
|
|
63
|
+
<!DOCTYPE html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="UTF-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
68
|
+
<title>K6 Performance Test Report - ${escapeHtml(title)}</title>
|
|
69
|
+
<!-- Font Awesome for icons -->
|
|
70
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
71
|
+
<style>
|
|
72
|
+
/* ========================================
|
|
73
|
+
GLOBAL STYLES
|
|
74
|
+
======================================== */
|
|
75
|
+
|
|
76
|
+
/* Reset default browser styles */
|
|
77
|
+
* {
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 0;
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Main body styling with gradient background */
|
|
84
|
+
body {
|
|
85
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
86
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
87
|
+
color: #333;
|
|
88
|
+
line-height: 1.6;
|
|
89
|
+
padding: 20px;
|
|
90
|
+
min-height: 100vh;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Main container with white background and shadow */
|
|
94
|
+
.container {
|
|
95
|
+
max-width: 1400px;
|
|
96
|
+
margin: 0 auto;
|
|
97
|
+
background: white;
|
|
98
|
+
border-radius: 20px;
|
|
99
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ========================================
|
|
104
|
+
HEADER SECTION
|
|
105
|
+
======================================== */
|
|
106
|
+
|
|
107
|
+
/* Header with dynamic gradient based on test pass/fail status */
|
|
108
|
+
.header {
|
|
109
|
+
background: ${testStatus ? 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)' : 'linear-gradient(135deg, #eb3349 0%, #f45c43 100%)'};
|
|
110
|
+
color: white;
|
|
111
|
+
padding: 40px;
|
|
112
|
+
position: relative;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Animated background effect in header */
|
|
117
|
+
.header::before {
|
|
118
|
+
content: '';
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: -50%;
|
|
121
|
+
right: -50%;
|
|
122
|
+
width: 200%;
|
|
123
|
+
height: 200%;
|
|
124
|
+
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
125
|
+
animation: pulse 15s ease-in-out infinite;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Pulse animation for header background */
|
|
129
|
+
@keyframes pulse {
|
|
130
|
+
0%, 100% { transform: scale(1); opacity: 0.5; }
|
|
131
|
+
50% { transform: scale(1.1); opacity: 0.8; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Header content positioned above the animated background */
|
|
135
|
+
.header-content {
|
|
136
|
+
position: relative;
|
|
137
|
+
z-index: 10;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Main heading with flexbox for logo alignment */
|
|
141
|
+
.header h1 {
|
|
142
|
+
font-size: 2.5em;
|
|
143
|
+
margin-bottom: 10px;
|
|
144
|
+
font-weight: 300;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 15px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* K6 logo container */
|
|
151
|
+
.k6-logo {
|
|
152
|
+
width: 60px;
|
|
153
|
+
height: 60px;
|
|
154
|
+
background: rgba(255, 255, 255, 0.2);
|
|
155
|
+
border-radius: 12px;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
font-size: 2em;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Test status badge (PASSED/FAILED) */
|
|
163
|
+
.test-status {
|
|
164
|
+
display: inline-block;
|
|
165
|
+
padding: 10px 25px;
|
|
166
|
+
border-radius: 50px;
|
|
167
|
+
font-size: 0.9em;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
margin-top: 15px;
|
|
170
|
+
background: rgba(255, 255, 255, 0.3);
|
|
171
|
+
backdrop-filter: blur(10px);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ========================================
|
|
175
|
+
STATS GRID SECTION
|
|
176
|
+
======================================== */
|
|
177
|
+
|
|
178
|
+
/* Responsive grid for statistic cards */
|
|
179
|
+
.stats-grid {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
182
|
+
gap: 25px;
|
|
183
|
+
padding: 40px;
|
|
184
|
+
background: #f8f9fa;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Individual stat card with shadow and hover effects */
|
|
188
|
+
.stat-card {
|
|
189
|
+
background: white;
|
|
190
|
+
border-radius: 15px;
|
|
191
|
+
padding: 30px;
|
|
192
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
193
|
+
transition: all 0.3s ease;
|
|
194
|
+
position: relative;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Colored top border for each stat card */
|
|
199
|
+
.stat-card::before {
|
|
200
|
+
content: '';
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: 0;
|
|
203
|
+
left: 0;
|
|
204
|
+
width: 100%;
|
|
205
|
+
height: 4px;
|
|
206
|
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Hover effect: lift the card */
|
|
210
|
+
.stat-card:hover {
|
|
211
|
+
transform: translateY(-5px);
|
|
212
|
+
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Success state: green gradient */
|
|
216
|
+
.stat-card.success::before {
|
|
217
|
+
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Error state: red gradient */
|
|
221
|
+
.stat-card.error::before {
|
|
222
|
+
background: linear-gradient(90deg, #eb3349 0%, #f45c43 100%);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Warning state: yellow/orange gradient */
|
|
226
|
+
.stat-card.warning::before {
|
|
227
|
+
background: linear-gradient(90deg, #f2994a 0%, #f2c94c 100%);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Background icon for stat cards (low opacity) */
|
|
231
|
+
.stat-icon {
|
|
232
|
+
font-size: 3em;
|
|
233
|
+
opacity: 0.1;
|
|
234
|
+
position: absolute;
|
|
235
|
+
right: 20px;
|
|
236
|
+
top: 50%;
|
|
237
|
+
transform: translateY(-50%);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Label text for each statistic */
|
|
241
|
+
.stat-label {
|
|
242
|
+
font-size: 0.85em;
|
|
243
|
+
color: #6c757d;
|
|
244
|
+
text-transform: uppercase;
|
|
245
|
+
letter-spacing: 1px;
|
|
246
|
+
margin-bottom: 10px;
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Main value display for each statistic */
|
|
251
|
+
.stat-value {
|
|
252
|
+
font-size: 2.5em;
|
|
253
|
+
font-weight: 700;
|
|
254
|
+
color: #2c3e50;
|
|
255
|
+
position: relative;
|
|
256
|
+
z-index: 10;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Subtext below the main stat value */
|
|
260
|
+
.stat-subtext {
|
|
261
|
+
font-size: 0.9em;
|
|
262
|
+
color: #95a5a6;
|
|
263
|
+
margin-top: 8px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ========================================
|
|
267
|
+
TABS SECTION
|
|
268
|
+
======================================== */
|
|
269
|
+
|
|
270
|
+
/* Container for all tab content */
|
|
271
|
+
.tabs-container {
|
|
272
|
+
padding: 40px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Tab button container with bottom border */
|
|
276
|
+
.tabs {
|
|
277
|
+
display: flex;
|
|
278
|
+
gap: 10px;
|
|
279
|
+
border-bottom: 2px solid #e9ecef;
|
|
280
|
+
margin-bottom: 30px;
|
|
281
|
+
flex-wrap: wrap;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* Individual tab button styling */
|
|
285
|
+
.tab-button {
|
|
286
|
+
padding: 15px 30px;
|
|
287
|
+
background: none;
|
|
288
|
+
border: none;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
font-size: 1em;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
color: #6c757d;
|
|
293
|
+
border-bottom: 3px solid transparent;
|
|
294
|
+
transition: all 0.3s ease;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 10px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Tab button hover state */
|
|
301
|
+
.tab-button:hover {
|
|
302
|
+
color: #667eea;
|
|
303
|
+
background: rgba(102, 126, 234, 0.1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Active tab styling */
|
|
307
|
+
.tab-button.active {
|
|
308
|
+
color: #667eea;
|
|
309
|
+
border-bottom-color: #667eea;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Tab content panel (hidden by default) */
|
|
313
|
+
.tab-content {
|
|
314
|
+
display: none;
|
|
315
|
+
animation: fadeIn 0.5s ease;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Show active tab content */
|
|
319
|
+
.tab-content.active {
|
|
320
|
+
display: block;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Fade-in animation for tab content */
|
|
324
|
+
@keyframes fadeIn {
|
|
325
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
326
|
+
to { opacity: 1; transform: translateY(0); }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ========================================
|
|
330
|
+
METRICS TABLE
|
|
331
|
+
======================================== */
|
|
332
|
+
|
|
333
|
+
/* Main metrics table with rounded corners */
|
|
334
|
+
.metrics-table {
|
|
335
|
+
width: 100%;
|
|
336
|
+
border-collapse: separate;
|
|
337
|
+
border-spacing: 0;
|
|
338
|
+
border-radius: 10px;
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Table header with gradient background */
|
|
344
|
+
.metrics-table thead {
|
|
345
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
346
|
+
color: white;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* Table header cells */
|
|
350
|
+
.metrics-table th {
|
|
351
|
+
padding: 15px;
|
|
352
|
+
text-align: left;
|
|
353
|
+
font-weight: 600;
|
|
354
|
+
font-size: 0.9em;
|
|
355
|
+
text-transform: uppercase;
|
|
356
|
+
letter-spacing: 0.5px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Table data cells */
|
|
360
|
+
.metrics-table td {
|
|
361
|
+
padding: 15px;
|
|
362
|
+
border-bottom: 1px solid #e9ecef;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Table row hover effect */
|
|
366
|
+
.metrics-table tbody tr {
|
|
367
|
+
transition: background 0.2s ease;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.metrics-table tbody tr:hover {
|
|
371
|
+
background: #f8f9fa;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Remove border from last row */
|
|
375
|
+
.metrics-table tbody tr:last-child td {
|
|
376
|
+
border-bottom: none;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* ========================================
|
|
380
|
+
BADGES AND INDICATORS
|
|
381
|
+
======================================== */
|
|
382
|
+
|
|
383
|
+
/* Generic badge styling */
|
|
384
|
+
.badge {
|
|
385
|
+
display: inline-block;
|
|
386
|
+
padding: 5px 12px;
|
|
387
|
+
border-radius: 20px;
|
|
388
|
+
font-size: 0.85em;
|
|
389
|
+
font-weight: 600;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* Success badge (green) */
|
|
393
|
+
.badge-success {
|
|
394
|
+
background: #d4edda;
|
|
395
|
+
color: #155724;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Error badge (red) */
|
|
399
|
+
.badge-error {
|
|
400
|
+
background: #f8d7da;
|
|
401
|
+
color: #721c24;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Warning badge (yellow) */
|
|
405
|
+
.badge-warning {
|
|
406
|
+
background: #fff3cd;
|
|
407
|
+
color: #856404;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Good metric value (green text) */
|
|
411
|
+
.metric-value-good {
|
|
412
|
+
color: #28a745;
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* Bad metric value (red text) */
|
|
417
|
+
.metric-value-bad {
|
|
418
|
+
color: #dc3545;
|
|
419
|
+
font-weight: 600;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ========================================
|
|
423
|
+
CHART AND CONTAINER STYLES
|
|
424
|
+
======================================== */
|
|
425
|
+
|
|
426
|
+
/* Container for charts and content sections */
|
|
427
|
+
.chart-container {
|
|
428
|
+
background: white;
|
|
429
|
+
border-radius: 15px;
|
|
430
|
+
padding: 30px;
|
|
431
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
|
432
|
+
margin-bottom: 30px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* Chart section title */
|
|
436
|
+
.chart-title {
|
|
437
|
+
font-size: 1.3em;
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
margin-bottom: 20px;
|
|
440
|
+
color: #2c3e50;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Progress bar container */
|
|
444
|
+
.progress-bar {
|
|
445
|
+
height: 30px;
|
|
446
|
+
background: #e9ecef;
|
|
447
|
+
border-radius: 15px;
|
|
448
|
+
overflow: hidden;
|
|
449
|
+
margin: 10px 0;
|
|
450
|
+
position: relative;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* Progress bar fill with animation */
|
|
454
|
+
.progress-fill {
|
|
455
|
+
height: 100%;
|
|
456
|
+
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
|
457
|
+
border-radius: 15px;
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
justify-content: flex-end;
|
|
461
|
+
padding-right: 15px;
|
|
462
|
+
color: white;
|
|
463
|
+
font-weight: 600;
|
|
464
|
+
transition: width 1s ease;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Error state for progress bar (red gradient) */
|
|
468
|
+
.progress-fill.error {
|
|
469
|
+
background: linear-gradient(90deg, #eb3349 0%, #f45c43 100%);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ========================================
|
|
473
|
+
FOOTER
|
|
474
|
+
======================================== */
|
|
475
|
+
|
|
476
|
+
/* Footer section */
|
|
477
|
+
.footer {
|
|
478
|
+
text-align: center;
|
|
479
|
+
padding: 30px;
|
|
480
|
+
background: #f8f9fa;
|
|
481
|
+
color: #6c757d;
|
|
482
|
+
font-size: 0.9em;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Footer links */
|
|
486
|
+
.footer a {
|
|
487
|
+
color: #667eea;
|
|
488
|
+
text-decoration: none;
|
|
489
|
+
font-weight: 600;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.footer a:hover {
|
|
493
|
+
text-decoration: underline;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ========================================
|
|
497
|
+
RESPONSIVE DESIGN
|
|
498
|
+
======================================== */
|
|
499
|
+
|
|
500
|
+
/* Mobile/tablet optimizations */
|
|
501
|
+
@media (max-width: 768px) {
|
|
502
|
+
.header h1 {
|
|
503
|
+
font-size: 1.8em;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.stats-grid {
|
|
507
|
+
grid-template-columns: 1fr;
|
|
508
|
+
padding: 20px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.tabs-container {
|
|
512
|
+
padding: 20px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.tab-button {
|
|
516
|
+
font-size: 0.9em;
|
|
517
|
+
padding: 12px 20px;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* ========================================
|
|
522
|
+
CHECK ITEMS
|
|
523
|
+
======================================== */
|
|
524
|
+
|
|
525
|
+
/* Individual check result item */
|
|
526
|
+
.check-item {
|
|
527
|
+
display: flex;
|
|
528
|
+
justify-content: space-between;
|
|
529
|
+
align-items: center;
|
|
530
|
+
padding: 15px;
|
|
531
|
+
background: white;
|
|
532
|
+
border-radius: 10px;
|
|
533
|
+
margin-bottom: 10px;
|
|
534
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.check-item:hover {
|
|
538
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* Check name/description */
|
|
542
|
+
.check-name {
|
|
543
|
+
flex: 1;
|
|
544
|
+
font-weight: 500;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* Container for check statistics */
|
|
548
|
+
.check-stats {
|
|
549
|
+
display: flex;
|
|
550
|
+
gap: 20px;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ========================================
|
|
554
|
+
HEADER SUBTITLE AND METHOD BADGES
|
|
555
|
+
======================================== */
|
|
556
|
+
|
|
557
|
+
/* Subtitle display in header */
|
|
558
|
+
.header-subtitle {
|
|
559
|
+
font-size: 1.1em;
|
|
560
|
+
opacity: 0.85;
|
|
561
|
+
margin: 15px 0 10px 0;
|
|
562
|
+
font-family: 'Courier New', monospace;
|
|
563
|
+
background: rgba(255, 255, 255, 0.2);
|
|
564
|
+
padding: 10px 20px;
|
|
565
|
+
border-radius: 8px;
|
|
566
|
+
display: inline-block;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* HTTP method badge (GET, POST, etc.) */
|
|
570
|
+
.http-method-badge {
|
|
571
|
+
display: inline-block;
|
|
572
|
+
padding: 5px 15px;
|
|
573
|
+
border-radius: 6px;
|
|
574
|
+
font-weight: 700;
|
|
575
|
+
font-size: 0.9em;
|
|
576
|
+
margin-right: 10px;
|
|
577
|
+
font-family: 'Courier New', monospace;
|
|
578
|
+
letter-spacing: 1px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* Color coding for different HTTP methods */
|
|
582
|
+
.method-GET {
|
|
583
|
+
background: #28a745;
|
|
584
|
+
color: white;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.method-POST {
|
|
588
|
+
background: #007bff;
|
|
589
|
+
color: white;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.method-PUT {
|
|
593
|
+
background: #ffc107;
|
|
594
|
+
color: #000;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.method-DELETE {
|
|
598
|
+
background: #dc3545;
|
|
599
|
+
color: white;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.method-PATCH {
|
|
603
|
+
background: #17a2b8;
|
|
604
|
+
color: white;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ========================================
|
|
608
|
+
INFO TABLE
|
|
609
|
+
======================================== */
|
|
610
|
+
|
|
611
|
+
/* Table for displaying test information */
|
|
612
|
+
.info-table {
|
|
613
|
+
width: 100%;
|
|
614
|
+
border-collapse: collapse;
|
|
615
|
+
margin-top: 15px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/* Info table cells */
|
|
619
|
+
.info-table td {
|
|
620
|
+
padding: 10px 15px;
|
|
621
|
+
border-bottom: 1px solid #e9ecef;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/* Info table labels (left column) */
|
|
625
|
+
.info-table td:first-child {
|
|
626
|
+
font-weight: 600;
|
|
627
|
+
color: #667eea;
|
|
628
|
+
width: 40%;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* Info table values (right column) */
|
|
632
|
+
.info-table td:last-child {
|
|
633
|
+
color: #2c3e50;
|
|
634
|
+
font-family: 'Courier New', monospace;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Remove border from last row */
|
|
638
|
+
.info-table tr:last-child td {
|
|
639
|
+
border-bottom: none;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Row hover effect */
|
|
643
|
+
.info-table tr:hover {
|
|
644
|
+
background: #f8f9fa;
|
|
645
|
+
}
|
|
646
|
+
</style>
|
|
647
|
+
</head>
|
|
648
|
+
<body>
|
|
649
|
+
<div class="container">
|
|
650
|
+
<!-- ========================================
|
|
651
|
+
HEADER SECTION
|
|
652
|
+
======================================== -->
|
|
653
|
+
<div class="header">
|
|
654
|
+
<div class="header-content">
|
|
655
|
+
<h1>
|
|
656
|
+
<!-- K6 Logo SVG -->
|
|
657
|
+
<div class="k6-logo">
|
|
658
|
+
<svg width="40" height="36" viewBox="0 0 50 45" fill="white">
|
|
659
|
+
<path d="M31.968 34.681a2.007 2.007 0 002.011-2.003c0-1.106-.9-2.003-2.011-2.003a2.007 2.007 0 00-2.012 2.003c0 1.106.9 2.003 2.012 2.003z"/>
|
|
660
|
+
<path d="M39.575 0L27.154 16.883 16.729 9.31 0 45h50L39.575 0zM23.663 37.17l-2.97-4.072v4.072h-2.751V22.038l2.75 1.989v7.66l3.659-5.014 2.086 1.51-3.071 4.21 3.486 4.776h-3.189v.001zm8.305.17c-2.586 0-4.681-2.088-4.681-4.662 0-1.025.332-1.972.896-2.743l4.695-6.435 2.086 1.51-2.239 3.07a4.667 4.667 0 013.924 4.6c0 2.572-2.095 4.66-4.681 4.66z"/>
|
|
661
|
+
</svg>
|
|
662
|
+
</div>
|
|
663
|
+
K6 Performance Test Report
|
|
664
|
+
</h1>
|
|
665
|
+
<!-- Report title -->
|
|
666
|
+
<p style="font-size: 1.2em; opacity: 0.9; margin: 10px 0;">${escapeHtml(title)}</p>
|
|
667
|
+
<!-- Subtitle with HTTP method badge if provided -->
|
|
668
|
+
${subtitle ? `
|
|
669
|
+
<div class="header-subtitle">
|
|
670
|
+
${httpMethod ? `<span class="http-method-badge method-${httpMethod}">${httpMethod}</span>` : '<i class="fas fa-link"></i>'}
|
|
671
|
+
${escapeHtml(subtitle)}
|
|
672
|
+
</div>
|
|
673
|
+
<span class="test-status" style="margin-top: 15px;">
|
|
674
|
+
${testStatus ? '✅ ALL TESTS PASSED' : '❌ TESTS FAILED'}
|
|
675
|
+
</span>
|
|
676
|
+
` : `
|
|
677
|
+
<span class="test-status">
|
|
678
|
+
${testStatus ? '✅ ALL TESTS PASSED' : '❌ TESTS FAILED'}
|
|
679
|
+
</span>
|
|
680
|
+
`}
|
|
681
|
+
<!-- Timestamp when report was generated -->
|
|
682
|
+
<p style="margin-top: 15px; opacity: 0.9;">
|
|
683
|
+
<i class="far fa-clock"></i> Generated: ${new Date().toLocaleString()}
|
|
684
|
+
</p>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
|
|
688
|
+
<!-- ========================================
|
|
689
|
+
STATS GRID - Overview Cards
|
|
690
|
+
======================================== -->
|
|
691
|
+
<div class="stats-grid">
|
|
692
|
+
<!-- Total Requests Card -->
|
|
693
|
+
<div class="stat-card ${stats.failedRequests === 0 ? 'success' : 'error'}">
|
|
694
|
+
<i class="fas fa-globe stat-icon"></i>
|
|
695
|
+
<div class="stat-label">Total Requests</div>
|
|
696
|
+
<div class="stat-value">${stats.totalRequests.toLocaleString()}</div>
|
|
697
|
+
<div class="stat-subtext">${stats.successfulRequests.toLocaleString()} successful</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<!-- Success Rate Card -->
|
|
701
|
+
<div class="stat-card ${stats.errorRate > 0 ? 'error' : 'success'}">
|
|
702
|
+
<i class="fas fa-chart-line stat-icon"></i>
|
|
703
|
+
<div class="stat-label">Success Rate</div>
|
|
704
|
+
<div class="stat-value">${stats.successRate}%</div>
|
|
705
|
+
<div class="stat-subtext">${stats.failedRequests} failed requests</div>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
<!-- Average Response Time Card -->
|
|
709
|
+
<div class="stat-card ${parseFloat(stats.avgResponseTime) > 1000 ? 'warning' : 'success'}">
|
|
710
|
+
<i class="fas fa-tachometer-alt stat-icon"></i>
|
|
711
|
+
<div class="stat-label">Avg Response Time</div>
|
|
712
|
+
<div class="stat-value">${stats.avgResponseTime}<span style="font-size: 0.5em;">ms</span></div>
|
|
713
|
+
<div class="stat-subtext">P95: ${stats.p95ResponseTime}ms</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<!-- Virtual Users Card -->
|
|
717
|
+
<div class="stat-card">
|
|
718
|
+
<i class="fas fa-users stat-icon"></i>
|
|
719
|
+
<div class="stat-label">Virtual Users</div>
|
|
720
|
+
<div class="stat-value">${stats.maxVUs}</div>
|
|
721
|
+
<div class="stat-subtext">Average: ${stats.avgVUs} VUs</div>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<!-- Checks Card -->
|
|
725
|
+
<div class="stat-card ${stats.checkFailures > 0 ? 'error' : 'success'}">
|
|
726
|
+
<i class="fas fa-check-circle stat-icon"></i>
|
|
727
|
+
<div class="stat-label">Checks</div>
|
|
728
|
+
<div class="stat-value">${stats.totalChecks}</div>
|
|
729
|
+
<div class="stat-subtext">${stats.checkPasses} passed / ${stats.checkFailures} failed</div>
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
<!-- Thresholds Card -->
|
|
733
|
+
<div class="stat-card ${stats.thresholdFailures > 0 ? 'error' : 'success'}">
|
|
734
|
+
<i class="fas fa-exclamation-triangle stat-icon"></i>
|
|
735
|
+
<div class="stat-label">Thresholds</div>
|
|
736
|
+
<div class="stat-value">${stats.thresholdFailures}</div>
|
|
737
|
+
<div class="stat-subtext">of ${stats.thresholdCount} breached</div>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<!-- ========================================
|
|
742
|
+
TABS SECTION
|
|
743
|
+
======================================== -->
|
|
744
|
+
<div class="tabs-container">
|
|
745
|
+
<!-- Tab Navigation Buttons -->
|
|
746
|
+
<div class="tabs">
|
|
747
|
+
<button class="tab-button active" onclick="switchTab(event, 'overview')">
|
|
748
|
+
<i class="fas fa-chart-pie"></i> Overview
|
|
749
|
+
</button>
|
|
750
|
+
<button class="tab-button" onclick="switchTab(event, 'metrics')">
|
|
751
|
+
<i class="fas fa-table"></i> Detailed Metrics
|
|
752
|
+
</button>
|
|
753
|
+
<button class="tab-button" onclick="switchTab(event, 'checks')">
|
|
754
|
+
<i class="fas fa-tasks"></i> Checks & Groups
|
|
755
|
+
</button>
|
|
756
|
+
<button class="tab-button" onclick="switchTab(event, 'thresholds')">
|
|
757
|
+
<i class="fas fa-gauge-high"></i> Thresholds
|
|
758
|
+
</button>
|
|
759
|
+
<!-- Conditional Test Info tab (only if additional info provided) -->
|
|
760
|
+
${Object.keys(additionalInfo).length > 0 ? `
|
|
761
|
+
<button class="tab-button" onclick="switchTab(event, 'testinfo')">
|
|
762
|
+
<i class="fas fa-info-circle"></i> Test Info
|
|
763
|
+
</button>
|
|
764
|
+
` : ''}
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<!-- Tab Content Panels -->
|
|
768
|
+
|
|
769
|
+
<!-- Overview Tab - Charts and graphs -->
|
|
770
|
+
<div id="overview" class="tab-content active">
|
|
771
|
+
${generateOverviewSection(stats)}
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<!-- Metrics Tab - Detailed metrics table -->
|
|
775
|
+
<div id="metrics" class="tab-content">
|
|
776
|
+
${generateMetricsTable(data)}
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
<!-- Checks Tab - Test checks and validations -->
|
|
780
|
+
<div id="checks" class="tab-content">
|
|
781
|
+
${generateChecksSection(data)}
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<!-- Thresholds Tab - Threshold pass/fail status -->
|
|
785
|
+
<div id="thresholds" class="tab-content">
|
|
786
|
+
${generateThresholdsSection(data)}
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<!-- Test Info Tab - Additional configuration details -->
|
|
790
|
+
${Object.keys(additionalInfo).length > 0 ? `
|
|
791
|
+
<div id="testinfo" class="tab-content">
|
|
792
|
+
${generateTestInfoSection(additionalInfo)}
|
|
793
|
+
</div>
|
|
794
|
+
` : ''}
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<!-- ========================================
|
|
798
|
+
FOOTER
|
|
799
|
+
======================================== -->
|
|
800
|
+
<div class="footer">
|
|
801
|
+
<p><strong>K6 Modern Reporter by Samin Azhan</strong></p>
|
|
802
|
+
<p>Generated with ❤️ by K6 Performance Testing Suite</p>
|
|
803
|
+
<p style="margin-top: 10px;">
|
|
804
|
+
<a href="https://github.com/Samin005/k6-modern-reporter" target="_blank">Documentation</a> |
|
|
805
|
+
<a href="https://github.com/Samin005" target="_blank">GitHub</a>
|
|
806
|
+
</p>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<!-- ========================================
|
|
811
|
+
JAVASCRIPT - Tab Switching & Animations
|
|
812
|
+
======================================== -->
|
|
813
|
+
<script>
|
|
814
|
+
/**
|
|
815
|
+
* Switch between tabs when clicking tab buttons
|
|
816
|
+
* @param {Event} event - The click event
|
|
817
|
+
* @param {string} tabId - The ID of the tab to show
|
|
818
|
+
*/
|
|
819
|
+
function switchTab(event, tabId) {
|
|
820
|
+
// Remove active class from all tab buttons
|
|
821
|
+
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
822
|
+
// Hide all tab content panels
|
|
823
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
824
|
+
|
|
825
|
+
// Add active class to clicked button
|
|
826
|
+
event.currentTarget.classList.add('active');
|
|
827
|
+
// Show selected tab content
|
|
828
|
+
document.getElementById(tabId).classList.add('active');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Animate progress bars on page load
|
|
833
|
+
* Progress bars start at 0 width and animate to their final width
|
|
834
|
+
*/
|
|
835
|
+
window.addEventListener('load', () => {
|
|
836
|
+
document.querySelectorAll('.progress-fill').forEach(bar => {
|
|
837
|
+
const width = bar.style.width; // Store final width
|
|
838
|
+
bar.style.width = '0'; // Start at 0
|
|
839
|
+
setTimeout(() => { bar.style.width = width; }, 100); // Animate to final width
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
</script>
|
|
843
|
+
</body>
|
|
844
|
+
</html>`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Calculate summary statistics from k6 test data
|
|
849
|
+
* Processes metrics, checks, and thresholds to generate summary numbers
|
|
850
|
+
*
|
|
851
|
+
* @param {Object} data - The k6 test results data object
|
|
852
|
+
* @returns {Object} Object containing calculated statistics
|
|
853
|
+
*/
|
|
854
|
+
function calculateStats(data) {
|
|
855
|
+
// Initialize stats object with default values
|
|
856
|
+
const stats = {
|
|
857
|
+
totalRequests: 0,
|
|
858
|
+
successfulRequests: 0,
|
|
859
|
+
failedRequests: 0,
|
|
860
|
+
successRate: 0,
|
|
861
|
+
errorRate: 0,
|
|
862
|
+
avgResponseTime: 0,
|
|
863
|
+
p95ResponseTime: 0,
|
|
864
|
+
maxResponseTime: 0,
|
|
865
|
+
minResponseTime: 0,
|
|
866
|
+
thresholdFailures: 0,
|
|
867
|
+
thresholdCount: 0,
|
|
868
|
+
checkFailures: 0,
|
|
869
|
+
checkPasses: 0,
|
|
870
|
+
totalChecks: 0,
|
|
871
|
+
maxVUs: 0,
|
|
872
|
+
avgVUs: 0,
|
|
873
|
+
iterations: 0,
|
|
874
|
+
dataReceived: 0,
|
|
875
|
+
dataSent: 0,
|
|
876
|
+
testDuration: 0
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// Extract HTTP request count
|
|
880
|
+
if (data.metrics.http_reqs) {
|
|
881
|
+
stats.totalRequests = data.metrics.http_reqs.values.count || 0;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Calculate success/failure rates
|
|
885
|
+
if (data.metrics.http_req_failed) {
|
|
886
|
+
stats.failedRequests = Math.round((data.metrics.http_req_failed.values.rate || 0) * stats.totalRequests);
|
|
887
|
+
stats.successfulRequests = stats.totalRequests - stats.failedRequests;
|
|
888
|
+
stats.errorRate = ((data.metrics.http_req_failed.values.rate || 0) * 100).toFixed(2);
|
|
889
|
+
stats.successRate = (100 - stats.errorRate).toFixed(2);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Extract response time metrics (avg, p95, min, max)
|
|
893
|
+
if (data.metrics.http_req_duration) {
|
|
894
|
+
const duration = data.metrics.http_req_duration.values;
|
|
895
|
+
stats.avgResponseTime = (duration.avg || 0).toFixed(2);
|
|
896
|
+
stats.p95ResponseTime = (duration['p(95)'] || 0).toFixed(2);
|
|
897
|
+
stats.maxResponseTime = (duration.max || 0).toFixed(2);
|
|
898
|
+
stats.minResponseTime = (duration.min || 0).toFixed(2);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Extract Virtual User (VU) metrics
|
|
902
|
+
if (data.metrics.vus) {
|
|
903
|
+
stats.maxVUs = data.metrics.vus.values.max || 0;
|
|
904
|
+
stats.avgVUs = (data.metrics.vus.values.avg || 0).toFixed(0);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Extract iteration count
|
|
908
|
+
if (data.metrics.iterations) {
|
|
909
|
+
stats.iterations = data.metrics.iterations.values.count || 0;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Extract data transfer metrics (convert bytes to megabytes)
|
|
913
|
+
if (data.metrics.data_received) {
|
|
914
|
+
stats.dataReceived = (data.metrics.data_received.values.count / 1000000).toFixed(2);
|
|
915
|
+
}
|
|
916
|
+
if (data.metrics.data_sent) {
|
|
917
|
+
stats.dataSent = (data.metrics.data_sent.values.count / 1000000).toFixed(2);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Count threshold failures across all metrics
|
|
921
|
+
for (let metricName in data.metrics) {
|
|
922
|
+
if (data.metrics[metricName].thresholds) {
|
|
923
|
+
stats.thresholdCount++;
|
|
924
|
+
const thresholds = data.metrics[metricName].thresholds;
|
|
925
|
+
for (let thres in thresholds) {
|
|
926
|
+
if (!thresholds[thres].ok) {
|
|
927
|
+
stats.thresholdFailures++;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Count check passes and failures from root group
|
|
934
|
+
if (data.root_group.checks) {
|
|
935
|
+
const { passes, fails } = countChecks(data.root_group.checks);
|
|
936
|
+
stats.checkPasses += passes;
|
|
937
|
+
stats.checkFailures += fails;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Count check passes and failures from all sub-groups
|
|
941
|
+
for (let group of data.root_group.groups || []) {
|
|
942
|
+
if (group.checks) {
|
|
943
|
+
const { passes, fails } = countChecks(group.checks);
|
|
944
|
+
stats.checkPasses += passes;
|
|
945
|
+
stats.checkFailures += fails;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
stats.totalChecks = stats.checkPasses + stats.checkFailures;
|
|
950
|
+
|
|
951
|
+
return stats;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Count total passes and fails from an array of checks
|
|
956
|
+
*
|
|
957
|
+
* @param {Array} checks - Array of check objects
|
|
958
|
+
* @returns {Object} Object with passes and fails counts
|
|
959
|
+
*/
|
|
960
|
+
function countChecks(checks) {
|
|
961
|
+
let passes = 0;
|
|
962
|
+
let fails = 0;
|
|
963
|
+
for (let check of checks) {
|
|
964
|
+
passes += parseInt(check.passes || 0);
|
|
965
|
+
fails += parseInt(check.fails || 0);
|
|
966
|
+
}
|
|
967
|
+
return { passes, fails };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Generate the metrics table HTML for detailed HTTP metrics
|
|
972
|
+
* Shows timing breakdowns like duration, waiting, connecting, etc.
|
|
973
|
+
*
|
|
974
|
+
* @param {Object} data - The k6 test results data
|
|
975
|
+
* @returns {string} HTML string for the metrics table
|
|
976
|
+
*/
|
|
977
|
+
function generateMetricsTable(data) {
|
|
978
|
+
// List of standard k6 HTTP timing metrics to display
|
|
979
|
+
const standardMetrics = [
|
|
980
|
+
'http_req_duration',
|
|
981
|
+
'http_req_waiting',
|
|
982
|
+
'http_req_connecting',
|
|
983
|
+
'http_req_tls_handshaking',
|
|
984
|
+
'http_req_sending',
|
|
985
|
+
'http_req_receiving',
|
|
986
|
+
'http_req_blocked',
|
|
987
|
+
'iteration_duration',
|
|
988
|
+
];
|
|
989
|
+
|
|
990
|
+
let html = '<div class="chart-container"><h3 class="chart-title">HTTP Request Metrics</h3>';
|
|
991
|
+
html += '<table class="metrics-table"><thead><tr>';
|
|
992
|
+
html += '<th>Metric</th><th>Avg</th><th>Min</th><th>Med</th><th>Max</th><th>P90</th><th>P95</th>';
|
|
993
|
+
html += '</tr></thead><tbody>';
|
|
994
|
+
|
|
995
|
+
// Generate table row for each metric
|
|
996
|
+
for (let metricName of standardMetrics) {
|
|
997
|
+
if (data.metrics[metricName]) {
|
|
998
|
+
const metric = data.metrics[metricName];
|
|
999
|
+
const values = metric.values;
|
|
1000
|
+
|
|
1001
|
+
html += '<tr>';
|
|
1002
|
+
html += `<td><strong>${metricName}</strong></td>`;
|
|
1003
|
+
html += `<td>${(values.avg || 0).toFixed(2)}</td>`;
|
|
1004
|
+
html += `<td class="metric-value-good">${(values.min || 0).toFixed(2)}</td>`;
|
|
1005
|
+
html += `<td>${(values.med || 0).toFixed(2)}</td>`;
|
|
1006
|
+
html += `<td class="metric-value-bad">${(values.max || 0).toFixed(2)}</td>`;
|
|
1007
|
+
html += `<td>${(values['p(90)'] || 0).toFixed(2)}</td>`;
|
|
1008
|
+
html += `<td>${(values['p(95)'] || 0).toFixed(2)}</td>`;
|
|
1009
|
+
html += '</tr>';
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
html += '</tbody></table></div>';
|
|
1014
|
+
html += '<p style="margin-top: 10px; color: #6c757d; font-size: 0.9em;"><i class="fas fa-info-circle"></i> All times are in milliseconds</p>';
|
|
1015
|
+
|
|
1016
|
+
return html;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Generate the checks section HTML
|
|
1021
|
+
* Displays all test checks organized by groups
|
|
1022
|
+
*
|
|
1023
|
+
* @param {Object} data - The k6 test results data
|
|
1024
|
+
* @returns {string} HTML string for the checks section
|
|
1025
|
+
*/
|
|
1026
|
+
function generateChecksSection(data) {
|
|
1027
|
+
let html = '';
|
|
1028
|
+
|
|
1029
|
+
// Process checks from all groups
|
|
1030
|
+
if (data.root_group.groups && data.root_group.groups.length > 0) {
|
|
1031
|
+
for (let group of data.root_group.groups) {
|
|
1032
|
+
html += `<div class="chart-container">`;
|
|
1033
|
+
html += `<h3 class="chart-title"><i class="fas fa-layer-group"></i> Group: ${escapeHtml(group.name)}</h3>`;
|
|
1034
|
+
|
|
1035
|
+
// Display each check in the group
|
|
1036
|
+
if (group.checks && group.checks.length > 0) {
|
|
1037
|
+
for (let check of group.checks) {
|
|
1038
|
+
const isPassed = check.fails === 0;
|
|
1039
|
+
html += `<div class="check-item">`;
|
|
1040
|
+
html += `<div class="check-name">${escapeHtml(check.name)}</div>`;
|
|
1041
|
+
html += `<div class="check-stats">`;
|
|
1042
|
+
html += `<span class="badge badge-success"><i class="fas fa-check"></i> ${check.passes} passed</span>`;
|
|
1043
|
+
if (check.fails > 0) {
|
|
1044
|
+
html += `<span class="badge badge-error"><i class="fas fa-times"></i> ${check.fails} failed</span>`;
|
|
1045
|
+
}
|
|
1046
|
+
html += `</div></div>`;
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
html += `<p style="color: #6c757d;">No checks in this group</p>`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
html += `</div>`;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Process checks from root group (ungrouped checks)
|
|
1057
|
+
if (data.root_group.checks && data.root_group.checks.length > 0) {
|
|
1058
|
+
html += `<div class="chart-container">`;
|
|
1059
|
+
html += `<h3 class="chart-title"><i class="fas fa-list-check"></i> Other Checks</h3>`;
|
|
1060
|
+
|
|
1061
|
+
for (let check of data.root_group.checks) {
|
|
1062
|
+
html += `<div class="check-item">`;
|
|
1063
|
+
html += `<div class="check-name">${escapeHtml(check.name)}</div>`;
|
|
1064
|
+
html += `<div class="check-stats">`;
|
|
1065
|
+
html += `<span class="badge badge-success"><i class="fas fa-check"></i> ${check.passes} passed</span>`;
|
|
1066
|
+
if (check.fails > 0) {
|
|
1067
|
+
html += `<span class="badge badge-error"><i class="fas fa-times"></i> ${check.fails} failed</span>`;
|
|
1068
|
+
}
|
|
1069
|
+
html += `</div></div>`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
html += `</div>`;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Show message if no checks were configured
|
|
1076
|
+
if (!html) {
|
|
1077
|
+
html = '<div class="chart-container"><p style="color: #6c757d;">No checks configured for this test</p></div>';
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return html;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Generate the thresholds section HTML
|
|
1085
|
+
* Displays pass/fail status for all configured thresholds
|
|
1086
|
+
*
|
|
1087
|
+
* @param {Object} data - The k6 test results data
|
|
1088
|
+
* @returns {string} HTML string for the thresholds section
|
|
1089
|
+
*/
|
|
1090
|
+
function generateThresholdsSection(data) {
|
|
1091
|
+
let html = '<div class="chart-container">';
|
|
1092
|
+
html += '<h3 class="chart-title"><i class="fas fa-gauge-high"></i> Threshold Results</h3>';
|
|
1093
|
+
|
|
1094
|
+
let hasThresholds = false;
|
|
1095
|
+
let passedCount = 0;
|
|
1096
|
+
let failedCount = 0;
|
|
1097
|
+
|
|
1098
|
+
// Iterate through all metrics to find thresholds
|
|
1099
|
+
for (let metricName in data.metrics) {
|
|
1100
|
+
const metric = data.metrics[metricName];
|
|
1101
|
+
|
|
1102
|
+
if (metric.thresholds) {
|
|
1103
|
+
hasThresholds = true;
|
|
1104
|
+
|
|
1105
|
+
// Check each threshold for this metric
|
|
1106
|
+
for (let thresholdName in metric.thresholds) {
|
|
1107
|
+
const threshold = metric.thresholds[thresholdName];
|
|
1108
|
+
const isPassed = threshold.ok;
|
|
1109
|
+
|
|
1110
|
+
if (isPassed) {
|
|
1111
|
+
passedCount++;
|
|
1112
|
+
} else {
|
|
1113
|
+
failedCount++;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Create threshold result card
|
|
1117
|
+
html += `<div class="check-item" style="border-left: 4px solid ${isPassed ? '#28a745' : '#dc3545'};">`;
|
|
1118
|
+
html += `<div style="flex: 1;">`;
|
|
1119
|
+
html += `<div style="font-weight: 600; margin-bottom: 5px;">`;
|
|
1120
|
+
html += `<i class="fas fa-${isPassed ? 'check-circle' : 'times-circle'}" style="color: ${isPassed ? '#28a745' : '#dc3545'};"></i> `;
|
|
1121
|
+
html += `${escapeHtml(metricName)}`;
|
|
1122
|
+
html += `</div>`;
|
|
1123
|
+
html += `<div style="color: #6c757d; font-size: 0.9em; font-family: monospace;">`;
|
|
1124
|
+
html += `${escapeHtml(thresholdName)}`;
|
|
1125
|
+
html += `</div>`;
|
|
1126
|
+
html += `</div>`;
|
|
1127
|
+
html += `<div class="check-stats">`;
|
|
1128
|
+
|
|
1129
|
+
if (isPassed) {
|
|
1130
|
+
html += `<span class="badge badge-success">`;
|
|
1131
|
+
html += `<i class="fas fa-check"></i> PASSED`;
|
|
1132
|
+
html += `</span>`;
|
|
1133
|
+
} else {
|
|
1134
|
+
html += `<span class="badge badge-error">`;
|
|
1135
|
+
html += `<i class="fas fa-times"></i> FAILED`;
|
|
1136
|
+
html += `</span>`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
html += `</div>`;
|
|
1140
|
+
html += `</div>`;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Show message if no thresholds configured
|
|
1146
|
+
if (!hasThresholds) {
|
|
1147
|
+
html += '<p style="color: #6c757d; text-align: center; padding: 40px;">No thresholds configured for this test</p>';
|
|
1148
|
+
} else {
|
|
1149
|
+
// Add summary cards at the top showing passed/failed counts
|
|
1150
|
+
html = `<div class="chart-container">
|
|
1151
|
+
<h3 class="chart-title"><i class="fas fa-gauge-high"></i> Threshold Results</h3>
|
|
1152
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px;">
|
|
1153
|
+
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 10px; text-align: center;">
|
|
1154
|
+
<div style="font-size: 2.5em; font-weight: 700; color: #155724;">${passedCount}</div>
|
|
1155
|
+
<div style="color: #155724; font-weight: 600;">Passed</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div style="background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); padding: 20px; border-radius: 10px; text-align: center;">
|
|
1158
|
+
<div style="font-size: 2.5em; font-weight: 700; color: #721c24;">${failedCount}</div>
|
|
1159
|
+
<div style="color: #721c24; font-weight: 600;">Failed</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>` + html.substring(html.indexOf('</h3>') + 5);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
html += '</div>';
|
|
1165
|
+
return html;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Generate the test info section HTML
|
|
1170
|
+
* Displays additional test configuration and metadata
|
|
1171
|
+
*
|
|
1172
|
+
* @param {Object} additionalInfo - Key-value pairs of test information
|
|
1173
|
+
* @returns {string} HTML string for the test info section
|
|
1174
|
+
*/
|
|
1175
|
+
function generateTestInfoSection(additionalInfo) {
|
|
1176
|
+
let html = '<div class="chart-container">';
|
|
1177
|
+
html += '<h3 class="chart-title"><i class="fas fa-info-circle"></i> Test Configuration & Additional Information</h3>';
|
|
1178
|
+
|
|
1179
|
+
html += '<table class="info-table">';
|
|
1180
|
+
|
|
1181
|
+
// Generate table row for each info item
|
|
1182
|
+
for (let key in additionalInfo) {
|
|
1183
|
+
html += '<tr>';
|
|
1184
|
+
html += `<td><i class="fas fa-caret-right" style="color: #667eea; margin-right: 8px;"></i>${escapeHtml(key)}</td>`;
|
|
1185
|
+
html += `<td>${escapeHtml(String(additionalInfo[key]))}</td>`;
|
|
1186
|
+
html += '</tr>';
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
html += '</table>';
|
|
1190
|
+
html += '</div>';
|
|
1191
|
+
|
|
1192
|
+
return html;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Generate the overview section HTML
|
|
1197
|
+
* Contains performance charts and data transfer statistics
|
|
1198
|
+
*
|
|
1199
|
+
* @param {Object} stats - Calculated statistics object
|
|
1200
|
+
* @returns {string} HTML string for the overview section
|
|
1201
|
+
*/
|
|
1202
|
+
function generateOverviewSection(stats) {
|
|
1203
|
+
return `
|
|
1204
|
+
<div class="chart-container">
|
|
1205
|
+
<h3 class="chart-title">Performance Overview</h3>
|
|
1206
|
+
|
|
1207
|
+
<!-- Success Rate Progress Bar -->
|
|
1208
|
+
<div style="margin: 30px 0;">
|
|
1209
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
|
1210
|
+
<span>Success Rate</span>
|
|
1211
|
+
<span class="metric-value-good">${stats.successRate}%</span>
|
|
1212
|
+
</div>
|
|
1213
|
+
<div class="progress-bar">
|
|
1214
|
+
<div class="progress-fill" style="width: ${stats.successRate}%">${stats.successRate}%</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
<!-- Error Rate Progress Bar -->
|
|
1219
|
+
<div style="margin: 30px 0;">
|
|
1220
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
|
1221
|
+
<span>Error Rate</span>
|
|
1222
|
+
<span class="metric-value-bad">${stats.errorRate}%</span>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div class="progress-bar">
|
|
1225
|
+
<div class="progress-fill error" style="width: ${stats.errorRate}%">${stats.errorRate}%</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
|
|
1229
|
+
<!-- Response Time Statistics Grid -->
|
|
1230
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 30px;">
|
|
1231
|
+
<!-- Minimum Response Time -->
|
|
1232
|
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
1233
|
+
<div style="font-size: 0.9em; color: #6c757d; margin-bottom: 5px;">MIN</div>
|
|
1234
|
+
<div style="font-size: 1.8em; font-weight: 700; color: #28a745;">${stats.minResponseTime}ms</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
<!-- Average Response Time -->
|
|
1237
|
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
1238
|
+
<div style="font-size: 0.9em; color: #6c757d; margin-bottom: 5px;">AVG</div>
|
|
1239
|
+
<div style="font-size: 1.8em; font-weight: 700; color: #667eea;">${stats.avgResponseTime}ms</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
<!-- 95th Percentile Response Time -->
|
|
1242
|
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
1243
|
+
<div style="font-size: 0.9em; color: #6c757d; margin-bottom: 5px;">P95</div>
|
|
1244
|
+
<div style="font-size: 1.8em; font-weight: 700; color: #f2994a;">${stats.p95ResponseTime}ms</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
<!-- Maximum Response Time -->
|
|
1247
|
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
1248
|
+
<div style="font-size: 0.9em; color: #6c757d; margin-bottom: 5px;">MAX</div>
|
|
1249
|
+
<div style="font-size: 1.8em; font-weight: 700; color: #dc3545;">${stats.maxResponseTime}ms</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
|
|
1254
|
+
<!-- Data Transfer Statistics -->
|
|
1255
|
+
<div class="chart-container">
|
|
1256
|
+
<h3 class="chart-title">Data Transfer</h3>
|
|
1257
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 20px;">
|
|
1258
|
+
<!-- Data Received -->
|
|
1259
|
+
<div style="text-align: center;">
|
|
1260
|
+
<i class="fas fa-download" style="font-size: 3em; color: #667eea; opacity: 0.3;"></i>
|
|
1261
|
+
<div style="font-size: 2em; font-weight: 700; margin: 10px 0;">${stats.dataReceived} MB</div>
|
|
1262
|
+
<div style="color: #6c757d;">Data Received</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
<!-- Data Sent -->
|
|
1265
|
+
<div style="text-align: center;">
|
|
1266
|
+
<i class="fas fa-upload" style="font-size: 3em; color: #764ba2; opacity: 0.3;"></i>
|
|
1267
|
+
<div style="font-size: 2em; font-weight: 700; margin: 10px 0;">${stats.dataSent} MB</div>
|
|
1268
|
+
<div style="color: #6c757d;">Data Sent</div>
|
|
1269
|
+
</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
`;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Escape HTML special characters to prevent XSS attacks
|
|
1277
|
+
* Converts characters like <, >, &, etc. to HTML entities
|
|
1278
|
+
*
|
|
1279
|
+
* @param {string} text - Text to escape
|
|
1280
|
+
* @returns {string} Escaped HTML-safe text
|
|
1281
|
+
*/
|
|
1282
|
+
function escapeHtml(text) {
|
|
1283
|
+
const map = {
|
|
1284
|
+
'&': '&',
|
|
1285
|
+
'<': '<',
|
|
1286
|
+
'>': '>',
|
|
1287
|
+
'"': '"',
|
|
1288
|
+
"'": '''
|
|
1289
|
+
};
|
|
1290
|
+
return String(text).replace(/[&<>"']/g, m => map[m]);
|
|
1291
|
+
}
|