@testomatio/reporter 2.6.0 → 2.6.2

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.
Files changed (41) hide show
  1. package/README.md +10 -10
  2. package/lib/adapter/playwright.d.ts +2 -12
  3. package/lib/adapter/playwright.js +15 -72
  4. package/lib/adapter/utils/playwright.d.ts +25 -0
  5. package/lib/adapter/utils/playwright.js +123 -0
  6. package/lib/adapter/vitest.js +2 -1
  7. package/lib/bin/cli.js +1 -1
  8. package/lib/data-storage.d.ts +1 -1
  9. package/lib/data-storage.js +1 -0
  10. package/lib/pipe/debug.js +1 -1
  11. package/lib/pipe/html.d.ts +2 -3
  12. package/lib/pipe/html.js +745 -37
  13. package/lib/pipe/testomatio.js +13 -2
  14. package/lib/reporter-functions.d.ts +36 -11
  15. package/lib/reporter-functions.js +67 -25
  16. package/lib/reporter.d.ts +42 -86
  17. package/lib/services/artifacts.d.ts +1 -1
  18. package/lib/services/key-values.d.ts +1 -1
  19. package/lib/services/links.d.ts +1 -1
  20. package/lib/services/logger.d.ts +1 -1
  21. package/lib/template/testomatio-old.hbs +1421 -0
  22. package/lib/template/testomatio.hbs +3200 -1157
  23. package/lib/utils/log-formatter.d.ts +2 -1
  24. package/lib/utils/log-formatter.js +8 -4
  25. package/package.json +5 -2
  26. package/src/adapter/playwright.js +15 -79
  27. package/src/adapter/utils/playwright.js +121 -0
  28. package/src/adapter/vitest.js +2 -1
  29. package/src/bin/cli.js +1 -1
  30. package/src/data-storage.js +1 -0
  31. package/src/pipe/debug.js +1 -1
  32. package/src/pipe/html.js +844 -38
  33. package/src/pipe/testomatio.js +13 -2
  34. package/src/reporter-functions.js +68 -28
  35. package/src/template/testomatio-old.hbs +1421 -0
  36. package/src/template/testomatio.hbs +3200 -1157
  37. package/src/utils/log-formatter.js +9 -4
  38. package/types/types.d.ts +29 -5
  39. package/lib/services/labels.d.ts +0 -0
  40. package/lib/services/labels.js +0 -0
  41. package/src/services/labels.js +0 -1
@@ -4,22 +4,22 @@
4
4
  <meta charset='UTF-8' />
5
5
  <meta
6
6
  name='viewport'
7
- content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'
7
+ content='width=device-width, initial-scale=1.0'
8
8
  />
9
9
  <meta http-equiv='X-UA-Compatible' content='ie=edge' />
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
13
13
  <link
14
- href='https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css'
14
+ href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css'
15
15
  rel='stylesheet'
16
- integrity='sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC'
16
+ integrity='sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM'
17
17
  crossorigin='anonymous'
18
18
  />
19
19
  <link
20
20
  rel='stylesheet'
21
- href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css'
22
- integrity='sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=='
21
+ href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'
22
+ integrity='sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=='
23
23
  crossorigin='anonymous'
24
24
  referrerpolicy='no-referrer'
25
25
  />
@@ -29,1393 +29,3436 @@
29
29
  <title>Report Testomat.io</title>
30
30
  {{/if}}
31
31
  <style>
32
+ :root {
33
+ --primary-color: #6366f1;
34
+ --primary-hover: #4f46e5;
35
+ --success-color: #10b981;
36
+ --danger-color: #ef4444;
37
+ --warning-color: #f59e0b;
38
+ --info-color: #3b82f6;
39
+ --gray-50: #f9fafb;
40
+ --gray-100: #f3f4f6;
41
+ --gray-200: #e5e7eb;
42
+ --gray-300: #d1d5db;
43
+ --gray-400: #9ca3af;
44
+ --gray-500: #6b7280;
45
+ --gray-600: #4b5563;
46
+ --gray-700: #374151;
47
+ --gray-800: #1f2937;
48
+ --gray-900: #111827;
49
+ --border-radius: 12px;
50
+ --transition: all 0.2s ease;
51
+ }
52
+
53
+ * {
54
+ box-sizing: border-box;
55
+ }
56
+
32
57
  body {
33
- padding: 0;
34
58
  margin: 0;
35
- font-family: "Inter", sans-serif;
59
+ padding: 0;
60
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
61
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
62
+ min-height: 100vh;
63
+ color: var(--gray-800);
36
64
  }
37
65
 
38
- header,
39
- div,
40
- p,
41
- form,
42
- input,
43
- a,
44
- span,
45
- button {
46
- box-sizing: border-box;
66
+ .main-container {
67
+ background: rgba(255, 255, 255, 0.98);
68
+ backdrop-filter: blur(20px);
69
+ margin: 20px auto;
70
+ max-width: 1400px;
71
+ border-radius: 20px;
72
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
73
+ overflow: hidden;
74
+ overflow-y: auto;
47
75
  }
48
- img {
49
- width: 100%;
50
- display: block;
51
- max-width: 100%;
52
- height: auto;
76
+
77
+ .header {
78
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
79
+ color: white;
80
+ padding: 30px;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ flex-wrap: wrap;
85
+ gap: 20px;
53
86
  }
54
- p, span {
55
- font-weight: 400;
56
- font-size: 14px;
57
- color: grey;
87
+
88
+ .header-title {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 15px;
92
+ }
93
+
94
+ .header-title h1 {
58
95
  margin: 0;
96
+ font-size: 28px;
97
+ font-weight: 700;
98
+ transition: color 0.3s ease;
59
99
  }
60
- .report {
61
- padding-top: 15px;
100
+
101
+ .header-title h1.status-passed {
102
+ color: var(--success-color);
62
103
  }
63
- .level_1 {
64
- margin-bottom: 20px;
65
- border-bottom: 2px solid #ABABAB;
66
- padding-bottom: 20px;
104
+
105
+ .header-title h1.status-failed {
106
+ color: var(--danger-color);
67
107
  }
68
- .header {
108
+
109
+ .header-title h1.status-skipped {
110
+ color: var(--warning-color);
111
+ }
112
+
113
+ .header-title h1.status-todo {
114
+ color: #8b5cf6;
115
+ }
116
+
117
+ .header-badge {
118
+ background: rgba(255, 255, 255, 0.2);
119
+ padding: 6px 16px;
120
+ border-radius: 20px;
121
+ font-size: 14px;
122
+ font-weight: 500;
123
+ backdrop-filter: blur(10px);
124
+ }
125
+
126
+ .header-actions {
69
127
  display: flex;
70
- justify-content: space-between;
128
+ gap: 15px;
71
129
  align-items: center;
72
130
  }
73
- .header__point {
74
- width: 8px;
75
- height: 8px;
76
- border-radius: 50%;
77
- margin-right: 15px;
131
+
132
+ .run-info {
133
+ background: rgba(255, 255, 255, 0.15);
134
+ padding: 8px 16px;
135
+ border-radius: 8px;
136
+ font-size: 14px;
137
+ }
138
+
139
+ .btn-primary-custom {
140
+ background: rgba(255, 255, 255, 0.9);
141
+ color: var(--primary-color);
142
+ border: none;
143
+ padding: 10px 24px;
144
+ border-radius: 8px;
145
+ font-weight: 600;
146
+ text-decoration: none;
147
+ transition: var(--transition);
148
+ display: inline-flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ }
152
+
153
+ .btn-primary-custom:hover {
154
+ background: white;
155
+ transform: translateY(-1px);
156
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
157
+ color: var(--primary-hover);
78
158
  }
79
- .header__point_red {
80
- background: red;
159
+
160
+ .btn-primary-custom:disabled {
161
+ opacity: 0.6;
162
+ cursor: not-allowed;
163
+ transform: none;
164
+ }
165
+
166
+ .stats-section {
167
+ padding: 30px;
168
+ background: var(--gray-50);
169
+ border-bottom: 1px solid var(--gray-200);
170
+ }
171
+
172
+ .stats-grid {
173
+ display: grid;
174
+ grid-template-columns: 2fr 1fr;
175
+ gap: 30px;
176
+ margin-bottom: 30px;
177
+ }
178
+
179
+ .chart-container {
180
+ background: white;
181
+ padding: 25px;
182
+ border-radius: var(--border-radius);
183
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
184
+ min-height: 400px;
185
+ overflow: visible !important;
186
+ }
187
+
188
+ .chart-section {
189
+ width: 100%;
190
+ overflow: hidden;
191
+ }
192
+
193
+ #testChart {
194
+ width: 100% !important;
195
+ max-width: 100% !important;
196
+ }
197
+
198
+ /* Google Charts tooltip stabilization */
199
+ #testChart > div > div[style*="position: absolute"] {
200
+ pointer-events: none !important;
201
+ will-change: transform, opacity !important;
202
+ backface-visibility: hidden !important;
203
+ transform: translateZ(0) !important;
204
+ }
205
+
206
+ .google-visualization-tooltip {
207
+ pointer-events: none !important;
208
+ opacity: 1 !important;
209
+ transition: none !important;
81
210
  }
82
- .header__block {
211
+
212
+ .stats-info {
213
+ background: white;
214
+ padding: 25px;
215
+ border-radius: var(--border-radius);
216
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
83
217
  display: flex;
84
- align-items: center;
218
+ flex-direction: column;
219
+ justify-content: center;
220
+ gap: 20px;
85
221
  }
86
- .header__case {
222
+
223
+ .stat-card {
87
224
  display: flex;
225
+ justify-content: space-between;
88
226
  align-items: center;
89
- margin-right: 15px;
227
+ padding: 12px 0;
228
+ border-bottom: 1px solid var(--gray-100);
90
229
  }
91
- .header__case p {
92
- margin-right: 7px;
230
+
231
+ .stat-card:last-child {
232
+ border-bottom: none;
93
233
  }
94
- .header__case span {
95
- color: black;
234
+
235
+ .stat-label {
236
+ font-size: 13px;
237
+ color: var(--gray-600);
238
+ font-weight: 500;
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 8px;
96
242
  }
97
- .header__block button {
98
- margin-left: 15px;
243
+
244
+ .stat-value {
245
+ font-size: 16px;
246
+ font-weight: 700;
247
+ color: var(--gray-900);
99
248
  }
100
- .header__type {
101
- font-size: 15px;
249
+
250
+ .status-badge {
251
+ padding: 4px 12px;
252
+ border-radius: 20px;
253
+ font-size: 12px;
102
254
  font-weight: 600;
103
- background: #CA95FF;
104
- color: #FFF;
105
- border-radius: 5px;
106
- padding: 5px 35px;
107
- margin-right: 15px;
108
- }
109
- .btn {
110
- box-shadow:none!important;
255
+ text-transform: uppercase;
256
+ letter-spacing: 0.5px;
111
257
  }
112
- .btn-report {
113
- text-decoration: none;
114
- font-weight: 600;
115
- background: #CA95FF;
116
- color: #FFF!important;
117
- border-radius: 5px;
118
- padding: 9px 37px;
119
- font-size: 15px;
258
+
259
+ .status-passed {
260
+ color: var(--success-color);
120
261
  }
121
- .run-style {
122
- color: #5D5C5D;
262
+
263
+ .status-failed {
264
+ color: var(--danger-color);
123
265
  }
124
- .run-span {
125
- font-weight: 800!important;
126
- color: #5D5C5D!important;
266
+
267
+ .status-skipped {
268
+ color: var(--warning-color);
127
269
  }
128
- .btn-report:hover {
129
- background: #B468FF;
270
+
271
+ .status-running {
272
+ color: var(--info-color);
130
273
  }
131
- .border-none {
132
- border: none;
274
+
275
+ .controls-section {
276
+ padding: 30px;
277
+ background: white;
278
+ border-bottom: 1px solid var(--gray-200);
133
279
  }
134
- .level_4 {
135
- height: 53px!important;
136
- }
137
- .inputSearch:focus {
138
- box-shadow: none;
139
- border: 1px solid black!important;
140
- background: #FFF!important;
141
- border-radius: 3px;
142
- }
143
- .btn-all {
144
- border: 1px solid #A0CAFF!important;
145
- }
146
- .btn-passed {
147
- border: 1px solid #75B583!important;
148
- }
149
- .btn-failed {
150
- border: 1px solid #FF6363!important;
151
- }
152
- .btn-skipped {
153
- border: 1px solid #FFC350!important;
154
- }
155
- .btn-all:hover,
156
- .btn-all:focus {
157
- color: #A0CAFF!important;
158
- background: #FFF!important;
159
- border: 1px solid #A0CAFF!important;
160
- font-weight: 700!important;
161
- }
162
- .btn-all:hover span,
163
- .btn-all:focus span {
164
- color: #A0CAFF!important;
165
- font-weight: 700!important;
166
- }
167
- .allTest:checked + .btn-all span {
168
- color: #A0CAFF!important;
169
- }
170
- .allTest:checked + .btn-all {
171
- color:#A0CAFF!important;
172
- background: #FFF!important;
173
- border: 1px solid #A0CAFF!important;
174
- font-weight: 700!important;
175
- }
176
- .btn-passed:hover,
177
- .btn-passed:focus {
178
- color: #75B583!important;
179
- border: 1px solid #75B583!important;
180
- background: #FFF!important;
181
- font-weight: 700!important;
182
- }
183
- .btn-passed:hover span,
184
- .btn-passed:focus span {
185
- color: #75B583!important;
186
- font-weight: 700!important;
187
- }
188
- .passedTest:checked + .btn-passed span {
189
- color: #75B583!important;
190
- font-weight: 700!important;
191
- }
192
- .passedTest:checked + .btn-passed {
193
- color: #75B583!important;
194
- border: 1px solid #75B583!important;
195
- background: #FFF!important;
196
- font-weight: 700!important;
197
- }
198
- .btn-failed:hover,
199
- .btn-failed:focus {
200
- color: #FF6363!important;
201
- border: 1px solid #FF6363!important;
202
- background: #FFF!important;
203
- font-weight: 700!important;
204
- }
205
- .btn-failed:hover span,
206
- .btn-failed:focus span {
207
- color: #FF6363!important;
208
- font-weight: 700!important;
209
- }
210
- .failedTest:checked + .btn-failed span {
211
- color: #FF6363!important;
212
- font-weight: 700!important;
213
- }
214
- .failedTest:checked + .btn-failed {
215
- color: #FF6363!important;
216
- border: 1px solid #FF6363!important;
217
- background: #FFF!important;
218
- font-weight: 700!important;
219
- }
220
- .btn-skipped:hover,
221
- .btn-skipped:focus {
222
- color: #FFC350!important;
223
- border: 1px solid #FFC350!important;
224
- background: #FFF!important;
225
- font-weight: 700!important;
226
- }
227
- .btn-skipped:hover span,
228
- .btn-skipped:focus span {
229
- color: #FFC350!important;
230
- font-weight: 700!important;
231
- }
232
- .skippedTest:checked + .btn-skipped span {
233
- color: #FFC350!important;
234
- font-weight: 700!important;
235
- }
236
- .skippedTest:checked + .btn-skipped {
237
- color: #FFC350!important;
238
- border: 1px solid #FFC350!important;
239
- background: #FFF!important;
240
- font-weight: 700!important;
241
- }
242
- .passed {
243
- color: #75B583;
244
- }
245
- .failed {
246
- color: #FF6363;
247
- }
248
- .skipped {
249
- color: #FFC350;
250
- }
251
- .testWrapp {
252
- margin-top: 45px;
253
- }
254
- .fa-arrow-right {
255
- display: none!important;
256
- }
257
- .header__type:hover {
258
- background: #B468FF;
259
- }
260
- .header__case i {
261
- color: grey;
262
- margin-right: 5px;
263
- }
264
- .title {
265
- display: flex;
266
- margin-bottom: 20px;
280
+
281
+ .search-container {
282
+ position: relative;
283
+ margin-bottom: 25px;
267
284
  }
268
- .title p {
269
- color: #262523;
285
+
286
+ .search-input {
287
+ width: 100%;
288
+ padding: 15px 20px 15px 50px;
289
+ border: 2px solid var(--gray-200);
290
+ border-radius: var(--border-radius);
270
291
  font-size: 15px;
271
- font-weight: 500;
272
- margin-right: 5px;
292
+ transition: var(--transition);
293
+ background: var(--gray-50);
294
+ }
295
+
296
+ .search-input:focus {
297
+ outline: none;
298
+ border-color: var(--primary-color);
299
+ background: white;
300
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
301
+ }
302
+
303
+ .search-icon {
304
+ position: absolute;
305
+ left: 20px;
306
+ top: 50%;
307
+ transform: translateY(-50%);
308
+ color: var(--gray-400);
309
+ font-size: 18px;
273
310
  }
274
- .statright {
311
+
312
+ .filters-container {
275
313
  display: flex;
314
+ gap: 15px;
315
+ flex-wrap: wrap;
316
+ align-items: center;
317
+ justify-content: space-between;
276
318
  }
277
- .statdesc {
319
+
320
+ .filter-tabs {
278
321
  display: flex;
279
- flex-direction: column;
322
+ gap: 8px;
323
+ background: var(--gray-100);
324
+ padding: 4px;
325
+ border-radius: var(--border-radius);
280
326
  }
281
- .statdesc__row {
282
- padding: 15px 25px;
327
+
328
+ .filter-tab {
329
+ padding: 10px 20px;
330
+ border: none;
331
+ background: transparent;
332
+ border-radius: 8px;
333
+ font-weight: 500;
334
+ cursor: pointer;
335
+ transition: var(--transition);
283
336
  display: flex;
284
- border-radius: 5px;
337
+ align-items: center;
338
+ gap: 8px;
339
+ color: var(--gray-600);
340
+ font-size: 14px;
285
341
  }
286
- .statdesc__row_first {
287
- width: 150px;
342
+
343
+ .filter-tab:hover {
344
+ background: rgba(255, 255, 255, 0.5);
288
345
  }
289
- .statdesc__row:nth-child(odd) {
290
- background: #F6FAFF;
346
+
347
+ .filter-tab.active {
348
+ background: white;
349
+ color: var(--primary-color);
350
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
291
351
  }
292
- .statdesc__row p,
293
- .statdesc__row span {
294
- font-weight: semi-bold;
295
- font-size: 11px;
352
+
353
+ .filter-tab .count {
354
+ background: var(--gray-200);
355
+ padding: 2px 8px;
356
+ border-radius: 12px;
357
+ font-size: 12px;
358
+ font-weight: 600;
296
359
  }
297
- .statdesc {
298
- width: 100%;
360
+
361
+ .filter-tab.active .count {
362
+ background: var(--primary-color);
363
+ color: white;
299
364
  }
300
- .statstatus {
365
+
366
+ .pagination-control {
301
367
  display: flex;
302
368
  align-items: center;
369
+ gap: 15px;
303
370
  }
304
- .statstatus p {
305
- font-weight: bold;
371
+
372
+ .pagination-select {
373
+ padding: 10px 15px;
374
+ border: 2px solid var(--gray-200);
375
+ border-radius: 8px;
376
+ background: white;
377
+ font-size: 14px;
378
+ cursor: pointer;
379
+ transition: var(--transition);
306
380
  }
307
- .statstatus div {
308
- font-weight: bold;
381
+
382
+ .pagination-select:focus {
383
+ outline: none;
384
+ border-color: var(--primary-color);
385
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
309
386
  }
310
- .statstatus_failed div {
311
- width: 10px;
312
- height: 10px;
313
- border-radius: 50%;
314
- margin-right: 5px;
387
+
388
+ .tests-section {
389
+ padding: 30px;
390
+ background: white;
391
+ min-height: 400px;
315
392
  }
316
- .statstatus_failed p {
317
- color: red;
318
- text-transform: uppercase;
393
+
394
+ .test-item {
395
+ background: white;
396
+ border: 2px solid var(--gray-200);
397
+ border-radius: var(--border-radius);
398
+ margin-bottom: 20px;
399
+ overflow: hidden;
400
+ transition: var(--transition);
319
401
  }
320
- .statstatus_failed div {
321
- background: red;
402
+
403
+ .test-item:hover {
404
+ border-color: var(--primary-color);
405
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
322
406
  }
323
- .statstatus_passed div {
324
- width: 10px;
325
- height: 10px;
326
- border-radius: 50%;
327
- margin-right: 5px;
328
- text-transform: uppercase;
407
+
408
+ .test-header {
409
+ padding: 20px;
410
+ background: var(--gray-50);
411
+ cursor: pointer;
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: center;
415
+ transition: var(--transition);
329
416
  }
330
- .statstatus_passed div {
331
- background: green;
417
+
418
+ .test-header:hover {
419
+ background: var(--gray-100);
332
420
  }
333
- .statstatus_passed p {
334
- color: green;
335
- text-transform: uppercase;
421
+
422
+ .test-info {
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 15px;
426
+ flex: 1;
336
427
  }
337
- .statstatus_skipped div{
338
- width: 10px;
339
- height: 10px;
428
+
429
+ .test-status-icon {
430
+ width: 40px;
431
+ height: 40px;
340
432
  border-radius: 50%;
341
- margin-right: 5px;
342
- text-transform: uppercase;
343
- }
344
- .statstatus_skipped div {
345
- background: yellow;
433
+ display: flex;
434
+ align-items: center;
435
+ justify-content: center;
436
+ font-size: 16px;
437
+ font-weight: 600;
346
438
  }
347
- .statstatus_skipped p {
348
- color: yellow;
349
- text-transform: uppercase;
439
+
440
+ .test-status-icon.passed {
441
+ background: rgba(16, 185, 129, 0.1);
442
+ color: var(--success-color);
350
443
  }
351
- .fa-magnifying-glass {
352
- font-size: 24px;
353
- color: gray;
444
+
445
+ .test-status-icon.failed {
446
+ background: rgba(239, 68, 68, 0.1);
447
+ color: var(--danger-color);
354
448
  }
355
- .numTest {
356
- font-weight: 500;
357
- font-size: 16px;
358
- color: #0D6EFD;
449
+
450
+ .test-status-icon.skipped {
451
+ background: rgba(245, 158, 11, 0.1);
452
+ color: var(--warning-color);
359
453
  }
360
- .testitem {
361
- border: 1px solid grey;
362
- border-radius: 3px;
363
- margin-bottom: 15px;
454
+
455
+ .test-details {
456
+ flex: 1;
364
457
  }
365
- .testitem__top {
366
- width: 100%;
458
+
459
+ .test-title {
460
+ font-size: 16px;
461
+ font-weight: 600;
462
+ color: var(--gray-900);
463
+ margin-bottom: 5px;
367
464
  display: flex;
368
- background: #F6FAFF;
369
- padding: 15px 13px 15px 30px;
370
- border-radius: 10px;
371
- cursor: pointer;
372
- justify-content: space-between;
465
+ align-items: center;
466
+ gap: 8px;
467
+ flex-wrap: wrap;
373
468
  }
374
- .testitem__icon {
375
- display: flex;
469
+
470
+ .artifact-upload-indicator {
471
+ display: inline-flex;
376
472
  align-items: center;
377
473
  justify-content: center;
378
- width: 30px;
379
- margin-right: 5px;
474
+ width: 20px;
475
+ height: 20px;
476
+ color: var(--primary-color);
477
+ font-size: 14px;
478
+ flex-shrink: 0;
380
479
  }
381
- .testitem__ico {
382
- font-size: 18px;
480
+
481
+ .artifact-upload-indicator:hover {
482
+ transform: scale(1.1);
483
+ transition: transform 0.2s ease;
383
484
  }
384
- .testitem__name {
385
- font-weight: 700;
386
- color: black;
387
- font-size: 16px;
485
+
486
+ .test-suite {
487
+ font-size: 14px;
488
+ color: var(--gray-600);
388
489
  }
389
- .testitem__body {
490
+
491
+ .test-meta {
390
492
  display: flex;
391
- background-color: #FFF;
392
- margin-top: 10px;
493
+ align-items: center;
494
+ gap: 20px;
393
495
  }
394
- .testitem__menu {
496
+
497
+ .test-duration {
395
498
  display: flex;
396
- flex-direction: column;
397
- padding: 21px 51px 21px 51px;
499
+ align-items: center;
500
+ gap: 5px;
501
+ color: var(--gray-500);
502
+ font-size: 14px;
398
503
  }
399
- .testitem__mitem {
400
- font-size: 15px;
504
+
505
+ .test-badges {
506
+ display: flex;
507
+ gap: 8px;
508
+ }
509
+
510
+ .badge {
511
+ padding: 4px 10px;
512
+ border-radius: 12px;
513
+ font-size: 11px;
401
514
  font-weight: 600;
515
+ text-transform: uppercase;
516
+ letter-spacing: 0.3px;
517
+ }
518
+
519
+ .badge-flaky {
520
+ background: rgba(245, 158, 11, 0.1);
521
+ color: var(--warning-color);
522
+ }
523
+
524
+ .badge-retry {
525
+ background: rgba(59, 130, 246, 0.1);
526
+ color: var(--info-color);
527
+ }
528
+
529
+ .expand-icon {
530
+ color: var(--gray-400);
531
+ transition: var(--transition);
532
+ font-size: 18px;
533
+ }
534
+
535
+ .test-item.expanded .expand-icon {
536
+ transform: rotate(180deg);
537
+ }
538
+
539
+ .test-content {
540
+ display: none;
541
+ border-top: 1px solid var(--gray-200);
542
+ }
543
+
544
+ .test-item.expanded .test-content {
545
+ display: block;
546
+ }
547
+
548
+ .test-group {
549
+ background: white;
550
+ border: 2px solid var(--gray-200);
551
+ border-radius: var(--border-radius);
552
+ margin-bottom: 20px;
553
+ overflow: hidden;
554
+ transition: var(--transition);
555
+ }
556
+
557
+ .test-group:hover {
558
+ border-color: var(--primary-color);
559
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
560
+ }
561
+
562
+ .test-group-header {
563
+ padding: 20px;
564
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
565
+ color: white;
402
566
  cursor: pointer;
403
- color: #808080;
404
- width: 170px;
405
- height: 65px;
406
- text-align: center;
407
- padding-top: 20px;
408
- padding-bottom: 20px;
567
+ display: flex;
568
+ justify-content: space-between;
569
+ align-items: center;
570
+ transition: var(--transition);
409
571
  }
410
- .testitem__mitem_active {
411
- background: #F6FAFF;
412
- color: #4D4C4C;
413
- font-weight: 700;
572
+
573
+ .test-group-header:hover {
574
+ filter: brightness(1.1);
414
575
  }
415
- .testitem__case {
576
+
577
+ .test-group-title {
416
578
  display: flex;
417
- flex-direction: column;
418
- padding: 10px;
579
+ align-items: center;
580
+ gap: 15px;
581
+ flex: 1;
419
582
  }
420
- .testitem__title {
583
+
584
+ .test-group-name {
421
585
  font-size: 18px;
422
- color: black;
423
586
  font-weight: 700;
424
- margin-bottom: 15px;
425
- text-decoration: underline;
426
587
  }
427
- .testitem__block p {
588
+
589
+ .test-group-count {
590
+ background: rgba(255, 255, 255, 0.2);
591
+ padding: 6px 16px;
592
+ border-radius: 20px;
428
593
  font-size: 14px;
429
- white-space: pre-line;
594
+ font-weight: 600;
430
595
  }
431
- .test__empty__list {
432
- margin-bottom: 10px;
433
- color: grey;
596
+
597
+ .test-group-stats {
598
+ display: flex;
599
+ gap: 20px;
600
+ font-size: 14px;
601
+ margin-right: 10px;
434
602
  }
435
- .testitem__content {
436
- width: 92%;
603
+
604
+ .test-group-stat {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: 6px;
437
608
  }
438
- /* Pagination component styles*/
439
- .page-link {
440
- color: black;
441
- background-color: #F6FAFF;
442
- padding: 7px 19px;
443
- font-weight: 600;
444
- border-radius: 3px;
609
+
610
+ .test-group-expand {
611
+ color: white;
612
+ transition: var(--transition);
613
+ font-size: 18px;
445
614
  }
446
- .page-link:hover {
447
- color: black;
615
+
616
+ .test-group.expanded .test-group-expand {
617
+ transform: rotate(180deg);
448
618
  }
449
- .form-select {
450
- background-color:#F6FAFF;
619
+
620
+ .test-group-content {
621
+ display: none;
622
+ background: var(--gray-50);
451
623
  }
452
- .form-select:focus {
453
- border-color: #ADADAD;
454
- box-shadow: 0 0 0 0.25rem rgb(67 71 78 / 25%);
624
+
625
+ .test-group.expanded .test-group-content {
626
+ display: block;
455
627
  }
456
- .page-item:not(:first-child) {
457
- margin-left: 15px;
628
+
629
+ .test-group-content .test-item {
630
+ border: none;
631
+ border-bottom: 1px solid var(--gray-200);
632
+ border-radius: 0;
633
+ margin-bottom: 0;
458
634
  }
459
- .page-item.active .page-link {
460
- background: #F6FAFF;
461
- border: 2px solid #4384FE;
462
- color: black;
635
+
636
+ .test-group-content .test-item:last-child {
637
+ border-bottom: none;
463
638
  }
464
- .noData {
639
+
640
+ .test-tabs {
465
641
  display: flex;
466
- align-items: center;
467
- flex-direction: column;
468
- justify-content: center;
469
- margin-bottom: 150px;
642
+ background: var(--gray-50);
643
+ border-bottom: 1px solid var(--gray-200);
470
644
  }
471
- .noDataText {
472
- font-weight: 600;
473
- font-size: 20px;
474
- color: #A1A1A1;
645
+
646
+ .test-tab {
647
+ padding: 15px 25px;
648
+ background: none;
649
+ border: none;
650
+ cursor: pointer;
651
+ font-weight: 500;
652
+ color: var(--gray-600);
653
+ transition: var(--transition);
654
+ position: relative;
475
655
  }
476
- </style>
477
- </head>
478
656
 
479
- <body class='bg-gray-50 pt-6 px-4 lg:pt-4 lg:px-40 h-full;'>
480
- <section class='report' id='report'>
481
- <div class='container'>
482
- <!-- top -->
483
- <div class='row level_1'>
484
- <div class='col-12'>
485
- <div class='header'>
486
- <div class='header__block'>
487
- <p class='header__type'>
488
- automated job
489
- </p>
490
- </div>
491
- <div class='header__block'>
492
- {{#if runId}}
493
- <div class='header__case run-style'>
494
- <p>Run</p>
495
- <span class='run-span'><strong>#</strong>{{runId}}</span>
496
- </div>
497
- {{/if}}
498
- </div>
499
- <div class='header__block'>
500
- {{#if runUrl}}
501
- <a href='{{runUrl}}' target='_blank' class='btn-report'>
502
- Full Report
503
- </a>
504
- {{else}}
505
- <button class='btn-report border-none' disabled>
506
- Full Report
507
- </button>
508
- {{/if}}
509
- </div>
510
- </div>
511
- </div>
512
- </div>
513
- <!-- top -->
657
+ .test-tab:hover {
658
+ color: var(--primary-color);
659
+ background: rgba(255, 255, 255, 0.5);
660
+ }
661
+
662
+ .test-tab.active {
663
+ color: var(--primary-color);
664
+ background: white;
665
+ }
666
+
667
+ .test-tab.active::after {
668
+ content: '';
669
+ position: absolute;
670
+ bottom: 0;
671
+ left: 0;
672
+ right: 0;
673
+ height: 2px;
674
+ background: var(--primary-color);
675
+ }
676
+
677
+ .test-tab-content {
678
+ padding: 25px;
679
+ display: none;
680
+ }
681
+
682
+ .test-tab-content.active {
683
+ display: block;
684
+ }
685
+
686
+ .code-block {
687
+ background: var(--gray-900);
688
+ color: #e5e7eb;
689
+ padding: 20px;
690
+ border-radius: 8px;
691
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
692
+ font-size: 14px;
693
+ line-height: 1.5;
694
+ overflow-x: auto;
695
+ white-space: pre-wrap;
696
+ }
697
+
698
+ .steps-container {
699
+ max-height: 500px;
700
+ overflow-y: auto;
701
+ border: 1px solid var(--gray-200);
702
+ border-radius: 8px;
703
+ background: white;
704
+ }
705
+
706
+ .step-item {
707
+ padding: 12px 16px;
708
+ border-bottom: 1px solid var(--gray-100);
709
+ display: flex;
710
+ align-items: flex-start;
711
+ gap: 12px;
712
+ transition: var(--transition);
713
+ position: relative;
714
+ }
715
+
716
+ .step-item:hover {
717
+ background: var(--gray-50);
718
+ }
719
+
720
+ .step-item:last-child {
721
+ border-bottom: none;
722
+ }
723
+
724
+ .step-number {
725
+ flex-shrink: 0;
726
+ width: 28px;
727
+ height: 28px;
728
+ border-radius: 50%;
729
+ background: var(--primary-color);
730
+ color: white;
731
+ display: flex;
732
+ align-items: center;
733
+ justify-content: center;
734
+ font-size: 12px;
735
+ font-weight: 600;
736
+ margin-top: 2px;
737
+ }
738
+
739
+ .step-content {
740
+ flex: 1;
741
+ min-width: 0;
742
+ }
743
+
744
+ .step-text {
745
+ font-size: 14px;
746
+ color: var(--gray-700);
747
+ line-height: 1.5;
748
+ margin: 0;
749
+ word-break: break-word;
750
+ }
751
+
752
+ .step-time {
753
+ font-size: 12px;
754
+ color: var(--gray-500);
755
+ margin-top: 4px;
756
+ }
757
+
758
+ .step-status {
759
+ width: 8px;
760
+ height: 8px;
761
+ border-radius: 50%;
762
+ margin-top: 8px;
763
+ flex-shrink: 0;
764
+ }
765
+
766
+ .step-status.passed {
767
+ background: var(--success-color);
768
+ }
769
+
770
+ .step-status.failed {
771
+ background: var(--danger-color);
772
+ }
773
+
774
+ .step-status.skipped {
775
+ background: var(--warning-color);
776
+ }
777
+
778
+ .step-category {
779
+ font-size: 11px;
780
+ text-transform: uppercase;
781
+ letter-spacing: 0.5px;
782
+ color: var(--gray-500);
783
+ font-weight: 600;
784
+ margin-top: 4px;
785
+ }
786
+
787
+ .step-children {
788
+ position: relative;
789
+ margin-left: 48px;
790
+ padding-left: 0;
791
+ }
792
+
793
+ .step-children::before {
794
+ content: '';
795
+ position: absolute;
796
+ left: 0;
797
+ top: 0;
798
+ bottom: 0;
799
+ width: 2px;
800
+ background: var(--gray-300);
801
+ }
802
+
803
+ .step-children .step-item {
804
+ position: relative;
805
+ }
806
+
807
+ .step-children .step-item::before {
808
+ content: '';
809
+ position: absolute;
810
+ left: -16px;
811
+ top: 24px;
812
+ width: 16px;
813
+ height: 2px;
814
+ background: var(--gray-300);
815
+ }
816
+
817
+ .step-children .step-item:last-child {
818
+ border-bottom: none;
819
+ }
820
+
821
+ .step-level-0 {
822
+ padding-left: 16px;
823
+ }
824
+
825
+ .step-level-1 {
826
+ padding-left: 8px;
827
+ }
828
+
829
+ .step-level-2 {
830
+ padding-left: 16px;
831
+ }
832
+
833
+ .step-level-3 {
834
+ padding-left: 24px;
835
+ }
836
+
837
+ .step-level-4 {
838
+ padding-left: 32px;
839
+ }
840
+
841
+ .step-item[data-step-id] {
842
+ position: relative;
843
+ }
844
+
845
+ .step-item.step-level-0::before {
846
+ display: none;
847
+ }
848
+
849
+ .step-duration {
850
+ display: inline-flex;
851
+ align-items: center;
852
+ gap: 4px;
853
+ font-size: 11px;
854
+ color: var(--gray-500);
855
+ margin-left: 8px;
856
+ }
857
+
858
+ .message-block {
859
+ background: var(--gray-50);
860
+ padding: 20px;
861
+ border-radius: 8px;
862
+ border-left: 4px solid var(--danger-color);
863
+ margin-bottom: 15px;
864
+ }
865
+
866
+ .message-block.info {
867
+ border-left-color: var(--info-color);
868
+ }
869
+
870
+ .metadata-grid {
871
+ display: grid;
872
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
873
+ gap: 15px;
874
+ }
875
+
876
+ .metadata-item {
877
+ background: var(--gray-50);
878
+ padding: 15px;
879
+ border-radius: 8px;
880
+ }
881
+
882
+ .metadata-key {
883
+ font-weight: 600;
884
+ color: var(--gray-700);
885
+ margin-bottom: 5px;
886
+ font-size: 14px;
887
+ }
888
+
889
+ .metadata-value {
890
+ color: var(--gray-600);
891
+ font-size: 14px;
892
+ word-break: break-all;
893
+ }
894
+
895
+ .attachments-grid {
896
+ display: grid;
897
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
898
+ gap: 20px;
899
+ }
900
+
901
+ .attachment-item {
902
+ background: var(--gray-50);
903
+ border-radius: 8px;
904
+ overflow: hidden;
905
+ transition: var(--transition);
906
+ cursor: pointer;
907
+ }
908
+
909
+ .attachment-item:hover {
910
+ transform: translateY(-2px);
911
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
912
+ }
913
+
914
+ .attachment-image {
915
+ width: 100%;
916
+ height: 150px;
917
+ object-fit: cover;
918
+ cursor: pointer;
919
+ transition: var(--transition);
920
+ }
921
+
922
+ .attachment-image:hover {
923
+ transform: scale(1.02);
924
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
925
+ }
926
+
927
+ .attachment-file {
928
+ display: flex;
929
+ flex-direction: column;
930
+ align-items: center;
931
+ justify-content: center;
932
+ height: 150px;
933
+ background: white;
934
+ cursor: pointer;
935
+ transition: var(--transition);
936
+ }
937
+
938
+ .attachment-file:hover {
939
+ transform: scale(1.02);
940
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
941
+ }
942
+
943
+ .attachment-preview {
944
+ position: relative;
945
+ overflow: hidden;
946
+ }
947
+
948
+ .attachment-overlay {
949
+ position: absolute;
950
+ top: 0;
951
+ left: 0;
952
+ right: 0;
953
+ bottom: 0;
954
+ background: rgba(0, 0, 0, 0.7);
955
+ display: flex;
956
+ align-items: center;
957
+ justify-content: center;
958
+ opacity: 0;
959
+ transition: opacity 0.2s ease;
960
+ color: white;
961
+ font-size: 14px;
962
+ font-weight: 600;
963
+ }
964
+
965
+ .attachment-item:hover .attachment-overlay {
966
+ opacity: 1;
967
+ }
968
+
969
+ .attachment-actions {
970
+ display: flex;
971
+ gap: 8px;
972
+ }
973
+
974
+ .attachment-action {
975
+ padding: 6px 12px;
976
+ background: rgba(255, 255, 255, 0.2);
977
+ border: 1px solid rgba(255, 255, 255, 0.3);
978
+ border-radius: 4px;
979
+ color: white;
980
+ text-decoration: none;
981
+ font-size: 12px;
982
+ cursor: pointer;
983
+ transition: var(--transition);
984
+ }
985
+
986
+ .attachment-action:hover {
987
+ background: rgba(255, 255, 255, 0.3);
988
+ transform: translateY(-1px);
989
+ }
990
+
991
+ .attachment-icon {
992
+ font-size: 48px;
993
+ color: var(--gray-400);
994
+ margin-bottom: 10px;
995
+ }
996
+
997
+ .attachment-name {
998
+ padding: 12px;
999
+ font-size: 14px;
1000
+ font-weight: 500;
1001
+ color: var(--gray-700);
1002
+ text-align: center;
1003
+ border-top: 1px solid var(--gray-200);
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ gap: 4px;
1007
+ }
1008
+
1009
+ .attachment-size {
1010
+ font-size: 11px;
1011
+ color: var(--gray-500);
1012
+ font-weight: 400;
1013
+ }
1014
+
1015
+ .retry-info {
1016
+ background: rgba(59, 130, 246, 0.05);
1017
+ border: 1px solid rgba(59, 130, 246, 0.2);
1018
+ border-radius: 8px;
1019
+ padding: 15px;
1020
+ margin-bottom: 20px;
1021
+ }
1022
+
1023
+ .retry-title {
1024
+ font-weight: 600;
1025
+ color: var(--info-color);
1026
+ margin-bottom: 10px;
1027
+ display: flex;
1028
+ align-items: center;
1029
+ gap: 8px;
1030
+ }
1031
+
1032
+ .retry-attempts {
1033
+ display: flex;
1034
+ gap: 10px;
1035
+ flex-wrap: wrap;
1036
+ }
1037
+
1038
+ .retry-attempt {
1039
+ background: white;
1040
+ border: 1px solid var(--gray-200);
1041
+ border-radius: 6px;
1042
+ padding: 8px 12px;
1043
+ font-size: 13px;
1044
+ display: flex;
1045
+ align-items: center;
1046
+ gap: 6px;
1047
+ }
1048
+
1049
+ .retry-attempt.failed {
1050
+ border-color: var(--danger-color);
1051
+ color: var(--danger-color);
1052
+ }
1053
+
1054
+ .retry-attempt.passed {
1055
+ border-color: var(--success-color);
1056
+ color: var(--success-color);
1057
+ }
1058
+
1059
+ .traces-instruction {
1060
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1061
+ border-radius: 12px;
1062
+ padding: 20px;
1063
+ margin-bottom: 20px;
1064
+ color: white;
1065
+ }
1066
+
1067
+ .instruction-header {
1068
+ display: flex;
1069
+ align-items: center;
1070
+ gap: 10px;
1071
+ font-size: 16px;
1072
+ font-weight: 600;
1073
+ margin-bottom: 15px;
1074
+ }
1075
+
1076
+ .instruction-content ol {
1077
+ margin: 0;
1078
+ padding-left: 20px;
1079
+ }
1080
+
1081
+ .instruction-content li {
1082
+ margin-bottom: 15px;
1083
+ line-height: 1.6;
1084
+ }
1085
+
1086
+ .instruction-content strong {
1087
+ color: #fff;
1088
+ font-weight: 600;
1089
+ }
1090
+
1091
+ .instruction-content code {
1092
+ background: rgba(0, 0, 0, 0.3);
1093
+ color: #fff;
1094
+ padding: 6px 10px;
1095
+ border-radius: 6px;
1096
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1097
+ font-size: 13px;
1098
+ display: inline-block;
1099
+ margin: 5px 0;
1100
+ border: 1px solid rgba(255, 255, 255, 0.2);
1101
+ }
1102
+
1103
+ .traces-list {
1104
+ display: flex;
1105
+ flex-direction: column;
1106
+ gap: 12px;
1107
+ }
1108
+
1109
+ .trace-download-item {
1110
+ background: white;
1111
+ border: 1px solid var(--gray-200);
1112
+ border-radius: 8px;
1113
+ padding: 15px 20px;
1114
+ display: flex;
1115
+ justify-content: space-between;
1116
+ align-items: center;
1117
+ gap: 15px;
1118
+ transition: var(--transition);
1119
+ }
1120
+
1121
+ .trace-download-item:hover {
1122
+ border-color: var(--primary-color);
1123
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.1);
1124
+ }
1125
+
1126
+ .trace-info {
1127
+ display: flex;
1128
+ align-items: center;
1129
+ gap: 12px;
1130
+ flex: 1;
1131
+ }
1132
+
1133
+ .trace-info i {
1134
+ font-size: 24px;
1135
+ color: var(--warning-color);
1136
+ }
1137
+
1138
+ .trace-name {
1139
+ font-size: 14px;
1140
+ font-weight: 500;
1141
+ color: var(--gray-700);
1142
+ }
1143
+
1144
+ .trace-download-btn {
1145
+ background: var(--primary-color);
1146
+ color: white;
1147
+ border: none;
1148
+ padding: 10px 20px;
1149
+ border-radius: 6px;
1150
+ font-size: 14px;
1151
+ font-weight: 600;
1152
+ cursor: pointer;
1153
+ transition: var(--transition);
1154
+ display: inline-flex;
1155
+ align-items: center;
1156
+ gap: 8px;
1157
+ }
1158
+
1159
+ .trace-download-btn:hover {
1160
+ background: var(--primary-hover);
1161
+ transform: translateY(-1px);
1162
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
1163
+ }
1164
+
1165
+ .trace-download-btn:active {
1166
+ transform: translateY(0);
1167
+ }
1168
+
1169
+ .pagination-container {
1170
+ padding: 30px;
1171
+ background: white;
1172
+ border-top: 1px solid var(--gray-200);
1173
+ }
1174
+
1175
+ .pagination {
1176
+ display: flex;
1177
+ justify-content: center;
1178
+ align-items: center;
1179
+ gap: 10px;
1180
+ }
1181
+
1182
+ .pagination-btn {
1183
+ padding: 10px 15px;
1184
+ border: 2px solid var(--gray-200);
1185
+ background: white;
1186
+ border-radius: 8px;
1187
+ cursor: pointer;
1188
+ transition: var(--transition);
1189
+ font-weight: 500;
1190
+ color: var(--gray-700);
1191
+ }
1192
+
1193
+ .pagination-btn:hover:not(:disabled) {
1194
+ border-color: var(--primary-color);
1195
+ color: var(--primary-color);
1196
+ }
1197
+
1198
+ .pagination-btn.active {
1199
+ background: var(--primary-color);
1200
+ border-color: var(--primary-color);
1201
+ color: white;
1202
+ }
1203
+
1204
+ .pagination-btn:disabled {
1205
+ opacity: 0.5;
1206
+ cursor: not-allowed;
1207
+ }
1208
+
1209
+ .empty-state {
1210
+ text-align: center;
1211
+ padding: 60px 20px;
1212
+ color: var(--gray-500);
1213
+ }
1214
+
1215
+ .empty-icon {
1216
+ font-size: 64px;
1217
+ color: var(--gray-300);
1218
+ margin-bottom: 20px;
1219
+ }
1220
+
1221
+ .empty-title {
1222
+ font-size: 20px;
1223
+ font-weight: 600;
1224
+ margin-bottom: 10px;
1225
+ color: var(--gray-600);
1226
+ }
1227
+
1228
+ .empty-description {
1229
+ font-size: 16px;
1230
+ }
1231
+
1232
+ .loading {
1233
+ display: flex;
1234
+ justify-content: center;
1235
+ align-items: center;
1236
+ padding: 40px;
1237
+ }
1238
+
1239
+ .spinner {
1240
+ width: 40px;
1241
+ height: 40px;
1242
+ border: 4px solid var(--gray-200);
1243
+ border-top: 4px solid var(--primary-color);
1244
+ border-radius: 50%;
1245
+ animation: spin 1s linear infinite;
1246
+ }
1247
+
1248
+ .status-todo {
1249
+ background: rgba(139, 92, 246, 0.1);
1250
+ color: #8b5cf6;
1251
+ }
1252
+
1253
+ .status-pending {
1254
+ background: rgba(156, 163, 175, 0.15);
1255
+ color: var(--gray-600);
1256
+ }
1257
+
1258
+ @keyframes spin {
1259
+ 0% { transform: rotate(0deg); }
1260
+ 100% { transform: rotate(360deg); }
1261
+ }
1262
+
1263
+ @media (max-width: 768px) {
1264
+ .main-container {
1265
+ margin: 5px;
1266
+ border-radius: 15px;
1267
+ }
1268
+
1269
+ .header {
1270
+ padding: 15px;
1271
+ flex-direction: column;
1272
+ align-items: flex-start;
1273
+ gap: 15px;
1274
+ }
1275
+
1276
+ .header-actions {
1277
+ width: 100%;
1278
+ justify-content: space-between;
1279
+ }
1280
+
1281
+ .stats-section {
1282
+ padding: 15px;
1283
+ }
1284
+
1285
+ .stats-grid {
1286
+ grid-template-columns: 1fr;
1287
+ gap: 15px;
1288
+ }
1289
+
1290
+ .chart-container {
1291
+ min-height: auto;
1292
+ padding: 15px;
1293
+ }
1294
+
1295
+ .stats-info {
1296
+ padding: 15px;
1297
+ }
1298
+
1299
+ .stat-card {
1300
+ padding: 10px 0;
1301
+ }
1302
+
1303
+ .stat-value {
1304
+ font-size: 20px;
1305
+ }
1306
+
1307
+ .stat-label {
1308
+ font-size: 12px;
1309
+ }
1310
+
1311
+ .chart-section {
1312
+ padding: 10px;
1313
+ width: 100%;
1314
+ }
1315
+
1316
+ #testChart {
1317
+ width: 100% !important;
1318
+ max-width: 100% !important;
1319
+ }
1320
+
1321
+ .filters-container {
1322
+ padding: 15px;
1323
+ flex-direction: column;
1324
+ gap: 15px;
1325
+ }
1326
+
1327
+ .filter-tabs {
1328
+ width: 100%;
1329
+ overflow-x: auto;
1330
+ -webkit-overflow-scrolling: touch;
1331
+ order: 2;
1332
+ }
1333
+
1334
+ .pagination-control {
1335
+ order: 1;
1336
+ justify-content: center;
1337
+ flex-direction: column;
1338
+ gap: 10px;
1339
+ }
1340
+
1341
+ .pagination-select {
1342
+ font-size: 14px;
1343
+ padding: 6px 10px;
1344
+ }
1345
+
1346
+ .search-container {
1347
+ margin-bottom: 5px;
1348
+ }
1349
+
1350
+ .tests-section, .controls-section {
1351
+ padding: 10px;
1352
+ }
1353
+
1354
+ .test-item {
1355
+ margin-bottom: 10px;
1356
+ }
1357
+
1358
+ .test-header {
1359
+ padding: 12px;
1360
+ flex-direction: column;
1361
+ align-items: flex-start;
1362
+ gap: 10px;
1363
+ }
1364
+
1365
+ .test-meta {
1366
+ width: 100%;
1367
+ justify-content: space-between;
1368
+ padding: 0;
1369
+ }
1370
+
1371
+ .test-tabs {
1372
+ overflow-x: auto;
1373
+ white-space: nowrap;
1374
+ -webkit-overflow-scrolling: touch;
1375
+ }
1376
+
1377
+ .test-tab {
1378
+ padding: 8px 12px;
1379
+ font-size: 13px;
1380
+ }
1381
+
1382
+ .test-tab-content {
1383
+ padding: 12px;
1384
+ }
1385
+
1386
+ .metadata-grid,
1387
+ .attachments-grid {
1388
+ grid-template-columns: 1fr;
1389
+ gap: 8px;
1390
+ }
1391
+
1392
+ .metadata-item,
1393
+ .attachment-item {
1394
+ padding: 10px;
1395
+ }
1396
+
1397
+ .pagination-container {
1398
+ padding: 15px;
1399
+ }
1400
+
1401
+ .pagination-btn {
1402
+ padding: 8px 12px;
1403
+ font-size: 14px;
1404
+ }
1405
+
1406
+ .settings-modal-content {
1407
+ width: 95%;
1408
+ max-width: none;
1409
+ margin: 10px;
1410
+ }
1411
+
1412
+ .settings-meta-grid {
1413
+ grid-template-columns: 1fr;
1414
+ }
1415
+
1416
+ .env-var-item {
1417
+ grid-template-columns: 1fr;
1418
+ gap: 8px;
1419
+ }
1420
+
1421
+ .env-vars-list {
1422
+ max-height: 250px;
1423
+ }
1424
+
1425
+ .env-var-value {
1426
+ padding: 4px 8px;
1427
+ }
1428
+ }
1429
+
1430
+ @media (min-width: 769px) and (max-width: 1024px) {
1431
+ .stats-section {
1432
+ padding: 20px;
1433
+ }
1434
+
1435
+ .stats-grid {
1436
+ grid-template-columns: 1fr;
1437
+ gap: 20px;
1438
+ }
1439
+
1440
+ .chart-container {
1441
+ padding: 20px;
1442
+ }
1443
+
1444
+ .stats-info {
1445
+ padding: 20px;
1446
+ }
1447
+
1448
+ .chart-section {
1449
+ padding: 20px;
1450
+ }
1451
+
1452
+ #testChart {
1453
+ width: 100% !important;
1454
+ max-width: 100% !important;
1455
+ }
1456
+
1457
+ .filters-container {
1458
+ padding: 15px 20px;
1459
+ }
1460
+
1461
+ .tests-section {
1462
+ padding: 15px;
1463
+ }
1464
+
1465
+ .test-header {
1466
+ padding: 15px;
1467
+ }
1468
+
1469
+ .metadata-grid,
1470
+ .attachments-grid {
1471
+ grid-template-columns: repeat(2, 1fr);
1472
+ }
1473
+ }
1474
+
1475
+ @media (max-width: 480px) {
1476
+ .stats-section {
1477
+ padding: 10px;
1478
+ }
1479
+
1480
+ .stats-grid {
1481
+ grid-template-columns: 1fr;
1482
+ gap: 10px;
1483
+ }
1484
+
1485
+ .chart-container {
1486
+ padding: 10px;
1487
+ min-height: auto;
1488
+ }
1489
+
1490
+ .stats-info {
1491
+ padding: 12px;
1492
+ }
1493
+
1494
+ .stat-card {
1495
+ padding: 8px 0;
1496
+ }
1497
+
1498
+ .stat-value {
1499
+ font-size: 18px;
1500
+ }
1501
+
1502
+ .stat-label {
1503
+ font-size: 11px;
1504
+ }
1505
+
1506
+ .chart-section {
1507
+ padding: 5px;
1508
+ }
1509
+
1510
+ .filter-tabs {
1511
+ gap: 5px;
1512
+ }
1513
+
1514
+ .test-tab {
1515
+ padding: 6px 10px;
1516
+ font-size: 12px;
1517
+ }
1518
+
1519
+ .badge {
1520
+ font-size: 10px;
1521
+ padding: 2px 6px;
1522
+ }
1523
+ }
1524
+
1525
+ .text-center { text-align: center; }
1526
+ .mt-20 { margin-top: 20px; }
1527
+ .mb-20 { margin-bottom: 20px; }
1528
+ .hidden { display: none !important; }
1529
+ .fade-in {
1530
+ animation: fadeIn 0.3s ease-in;
1531
+ }
1532
+
1533
+ @keyframes fadeIn {
1534
+ from { opacity: 0; transform: translateY(10px); }
1535
+ to { opacity: 1; transform: translateY(0); }
1536
+ }
1537
+
1538
+ .todo-info {
1539
+ text-align: center;
1540
+ padding: 40px 20px;
1541
+ color: var(--warning-color);
1542
+ }
1543
+
1544
+ .todo-title {
1545
+ font-size: 18px;
1546
+ font-weight: 600;
1547
+ margin-bottom: 15px;
1548
+ color: var(--warning-color);
1549
+ }
1550
+
1551
+ .todo-title i {
1552
+ margin-right: 8px;
1553
+ }
1554
+
1555
+ .todo-description {
1556
+ font-size: 14px;
1557
+ color: var(--gray-600);
1558
+ margin-bottom: 25px;
1559
+ }
1560
+
1561
+ .todo-details {
1562
+ background: var(--gray-50);
1563
+ border-radius: 8px;
1564
+ padding: 20px;
1565
+ text-align: left;
1566
+ border: 1px solid var(--gray-200);
1567
+ }
1568
+
1569
+ .todo-details p {
1570
+ margin: 8px 0;
1571
+ font-size: 13px;
1572
+ }
1573
+
1574
+ .todo-details strong {
1575
+ color: var(--gray-700);
1576
+ }
1577
+
1578
+ .test-status-icon.todo {
1579
+ background: var(--warning-color);
1580
+ color: white;
1581
+ }
1582
+
1583
+ .test-status-icon.todo i {
1584
+ color: white;
1585
+ }
1586
+
1587
+ .settings-btn {
1588
+ background: rgba(255, 255, 255, 0.2);
1589
+ border: none;
1590
+ padding: 10px 16px;
1591
+ border-radius: 8px;
1592
+ color: white;
1593
+ cursor: pointer;
1594
+ transition: var(--transition);
1595
+ font-size: 18px;
1596
+ display: flex;
1597
+ align-items: center;
1598
+ justify-content: center;
1599
+ }
1600
+
1601
+ .settings-btn i{
1602
+ transition: var(--transition);
1603
+ }
1604
+
1605
+ .settings-btn:hover {
1606
+ background: rgba(255, 255, 255, 0.3);
1607
+ }
1608
+
1609
+ .settings-btn:hover i{
1610
+ transform: rotate(45deg);
1611
+ }
1612
+
1613
+ .settings-modal {
1614
+ display: none;
1615
+ position: fixed;
1616
+ z-index: 10000;
1617
+ left: 0;
1618
+ top: 0;
1619
+ width: 100%;
1620
+ height: 100%;
1621
+ background-color: rgba(0, 0, 0, 0.6);
1622
+ backdrop-filter: blur(4px);
1623
+ }
1624
+
1625
+ .settings-modal.active {
1626
+ display: flex;
1627
+ align-items: center;
1628
+ justify-content: center;
1629
+ }
1630
+
1631
+ .settings-modal-content {
1632
+ background: white;
1633
+ border-radius: var(--border-radius);
1634
+ width: 90%;
1635
+ max-width: 600px;
1636
+ max-height: 80vh;
1637
+ overflow: hidden;
1638
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1639
+ animation: modalSlideIn 0.3s ease;
1640
+ }
1641
+
1642
+ @keyframes modalSlideIn {
1643
+ from {
1644
+ opacity: 0;
1645
+ transform: translateY(-50px) scale(0.95);
1646
+ }
1647
+ to {
1648
+ opacity: 1;
1649
+ transform: translateY(0) scale(1);
1650
+ }
1651
+ }
1652
+
1653
+ .settings-modal-header {
1654
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
1655
+ color: white;
1656
+ padding: 20px 25px;
1657
+ display: flex;
1658
+ justify-content: space-between;
1659
+ align-items: center;
1660
+ }
1661
+
1662
+ .settings-modal-title {
1663
+ font-size: 20px;
1664
+ font-weight: 600;
1665
+ display: flex;
1666
+ align-items: center;
1667
+ gap: 10px;
1668
+ }
1669
+
1670
+ .settings-modal-close {
1671
+ background: rgba(255, 255, 255, 0.2);
1672
+ border: none;
1673
+ color: white;
1674
+ font-size: 20px;
1675
+ width: 36px;
1676
+ height: 36px;
1677
+ border-radius: 50%;
1678
+ cursor: pointer;
1679
+ transition: var(--transition);
1680
+ display: flex;
1681
+ align-items: center;
1682
+ justify-content: center;
1683
+ }
1684
+
1685
+ .settings-modal-close:hover {
1686
+ background: rgba(255, 255, 255, 0.3);
1687
+ transform: rotate(90deg);
1688
+ }
1689
+
1690
+ .settings-modal-body {
1691
+ padding: 25px;
1692
+ overflow-y: auto;
1693
+ max-height: calc(80vh - 80px);
1694
+ }
1695
+
1696
+ .settings-section {
1697
+ margin-bottom: 25px;
1698
+ }
1699
+
1700
+ .settings-section:last-child {
1701
+ margin-bottom: 0;
1702
+ }
1703
+
1704
+ .settings-section-title {
1705
+ font-size: 14px;
1706
+ font-weight: 600;
1707
+ color: var(--gray-700);
1708
+ text-transform: uppercase;
1709
+ letter-spacing: 0.5px;
1710
+ margin-bottom: 15px;
1711
+ display: flex;
1712
+ align-items: center;
1713
+ gap: 8px;
1714
+ }
1715
+
1716
+ .settings-meta-grid {
1717
+ display: grid;
1718
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1719
+ gap: 12px;
1720
+ }
1721
+
1722
+ .settings-meta-item {
1723
+ display: flex;
1724
+ align-items: center;
1725
+ gap: 10px;
1726
+ padding: 10px 12px;
1727
+ background: var(--gray-50);
1728
+ border-radius: 8px;
1729
+ border: 2px solid var(--gray-200);
1730
+ cursor: pointer;
1731
+ transition: var(--transition);
1732
+ }
1733
+
1734
+ .settings-meta-item:hover {
1735
+ border-color: var(--primary-color);
1736
+ background: white;
1737
+ }
1738
+
1739
+ .settings-meta-item input[type="checkbox"] {
1740
+ width: 18px;
1741
+ height: 18px;
1742
+ cursor: pointer;
1743
+ accent-color: var(--primary-color);
1744
+ }
1745
+
1746
+ .settings-meta-label {
1747
+ font-size: 14px;
1748
+ color: var(--gray-700);
1749
+ font-weight: 500;
1750
+ flex: 1;
1751
+ word-break: break-word;
1752
+ }
1753
+
1754
+ .settings-meta-count {
1755
+ font-size: 11px;
1756
+ color: var(--gray-500);
1757
+ background: var(--gray-200);
1758
+ padding: 2px 8px;
1759
+ border-radius: 12px;
1760
+ font-weight: 600;
1761
+ }
1762
+
1763
+ .settings-tabs-nav {
1764
+ display: flex;
1765
+ gap: 10px;
1766
+ margin-bottom: 20px;
1767
+ border-bottom: 2px solid var(--gray-200);
1768
+ padding-bottom: 0;
1769
+ }
1770
+
1771
+ .settings-tab {
1772
+ flex: 1;
1773
+ padding: 12px 16px;
1774
+ border: none;
1775
+ background: transparent;
1776
+ color: var(--gray-600);
1777
+ font-size: 14px;
1778
+ font-weight: 600;
1779
+ cursor: pointer;
1780
+ transition: var(--transition);
1781
+ display: flex;
1782
+ align-items: center;
1783
+ justify-content: center;
1784
+ gap: 8px;
1785
+ border-bottom: 2px solid transparent;
1786
+ margin-bottom: -2px;
1787
+ }
1788
+
1789
+ .settings-tab:hover {
1790
+ background: var(--gray-100);
1791
+ color: var(--gray-700);
1792
+ }
1793
+
1794
+ .settings-tab.active {
1795
+ color: var(--primary-color);
1796
+ border-bottom-color: var(--primary-color);
1797
+ }
1798
+
1799
+ .settings-tab-content {
1800
+ display: none;
1801
+ }
1802
+
1803
+ .settings-tab-content.active {
1804
+ display: block;
1805
+ }
1806
+
1807
+ .env-vars-container {
1808
+ display: flex;
1809
+ flex-direction: column;
1810
+ gap: 20px;
1811
+ }
1812
+
1813
+ .env-vars-group {
1814
+ background: var(--gray-50);
1815
+ border: 1px solid var(--gray-200);
1816
+ border-radius: 8px;
1817
+ overflow: hidden;
1818
+ }
1819
+
1820
+ .env-vars-group-title {
1821
+ background: var(--gray-100);
1822
+ padding: 12px 15px;
1823
+ font-size: 13px;
1824
+ font-weight: 600;
1825
+ color: var(--gray-700);
1826
+ border-bottom: 1px solid var(--gray-200);
1827
+ display: flex;
1828
+ align-items: center;
1829
+ justify-content: space-between;
1830
+ gap: 8px;
1831
+ }
1832
+
1833
+ .env-vars-count {
1834
+ font-size: 11px;
1835
+ background: var(--primary-color);
1836
+ color: white;
1837
+ padding: 2px 8px;
1838
+ border-radius: 10px;
1839
+ font-weight: 500;
1840
+ }
1841
+
1842
+ .env-vars-list {
1843
+ display: flex;
1844
+ flex-direction: column;
1845
+ max-height: 400px;
1846
+ overflow-y: auto;
1847
+ }
1848
+
1849
+ .env-var-item {
1850
+ display: grid;
1851
+ grid-template-columns: 1fr 1fr;
1852
+ padding: 12px 15px;
1853
+ border-bottom: 1px solid var(--gray-200);
1854
+ gap: 15px;
1855
+ transition: var(--transition);
1856
+ }
1857
+
1858
+ .env-var-item:last-child {
1859
+ border-bottom: none;
1860
+ }
1861
+
1862
+ .env-var-item:hover {
1863
+ background: white;
1864
+ }
1865
+
1866
+ .env-var-item.env-var-unset {
1867
+ background: var(--gray-100);
1868
+ opacity: 0.7;
1869
+ }
1870
+
1871
+ .env-var-item.env-var-unset:hover {
1872
+ background: var(--gray-200);
1873
+ }
1874
+
1875
+ .env-var-info {
1876
+ display: flex;
1877
+ flex-direction: column;
1878
+ gap: 4px;
1879
+ }
1880
+
1881
+ .env-var-key {
1882
+ font-size: 12px;
1883
+ font-weight: 600;
1884
+ color: var(--primary-color);
1885
+ font-family: 'Courier New', monospace;
1886
+ word-break: break-all;
1887
+ }
1888
+
1889
+ .env-var-description {
1890
+ font-size: 11px;
1891
+ color: var(--gray-600);
1892
+ line-height: 1.3;
1893
+ }
1894
+
1895
+ .env-var-value {
1896
+ font-size: 12px;
1897
+ color: var(--gray-700);
1898
+ font-family: 'Courier New', monospace;
1899
+ word-break: break-all;
1900
+ padding: 6px 10px;
1901
+ background: white;
1902
+ border: 1px solid var(--gray-300);
1903
+ border-radius: 4px;
1904
+ display: flex;
1905
+ align-items: center;
1906
+ }
1907
+
1908
+ .env-var-value.env-var-empty {
1909
+ background: var(--gray-100);
1910
+ color: var(--gray-500);
1911
+ font-style: italic;
1912
+ border-style: dashed;
1913
+ }
1914
+
1915
+ .env-var-value.env-var-confidential {
1916
+ background: var(--gray-100);
1917
+ color: var(--gray-600);
1918
+ font-style: italic;
1919
+ }
1920
+
1921
+ .settings-footer {
1922
+ padding: 15px 25px;
1923
+ background: var(--gray-50);
1924
+ border-top: 1px solid var(--gray-200);
1925
+ display: flex;
1926
+ justify-content: flex-end;
1927
+ gap: 10px;
1928
+ }
1929
+
1930
+ .settings-btn-action {
1931
+ padding: 10px 20px;
1932
+ border: none;
1933
+ border-radius: 8px;
1934
+ font-weight: 600;
1935
+ cursor: pointer;
1936
+ transition: var(--transition);
1937
+ font-size: 14px;
1938
+ }
1939
+
1940
+ .settings-btn-reset {
1941
+ background: transparent;
1942
+ color: var(--gray-500);
1943
+ border: 1px solid var(--gray-300);
1944
+ margin-right: auto;
1945
+ }
1946
+
1947
+ .settings-btn-reset:hover {
1948
+ background: var(--gray-100);
1949
+ color: var(--gray-700);
1950
+ }
1951
+
1952
+ .settings-btn-cancel {
1953
+ background: var(--gray-200);
1954
+ color: var(--gray-700);
1955
+ }
1956
+
1957
+ .settings-btn-cancel:hover {
1958
+ background: var(--gray-300);
1959
+ }
1960
+
1961
+ .settings-btn-save {
1962
+ background: var(--primary-color);
1963
+ color: white;
1964
+ }
1965
+
1966
+ .settings-btn-save:hover {
1967
+ background: var(--primary-hover);
1968
+ }
1969
+
1970
+ /* Meta item visibility control */
1971
+ .meta-item {
1972
+ transition: var(--transition);
1973
+ }
1974
+
1975
+ .meta-item.hidden {
1976
+ display: none !important;
1977
+ }
1978
+ </style>
1979
+ </head>
514
1980
 
515
- <div class='row level_2'>
516
- <div class='col-12'>
517
- <div class='title'>
518
- <p>Testomatio Run Information</p>
1981
+ <body>
1982
+ <div class='main-container'>
1983
+ <header class='header'>
1984
+ <div class='header-title'>
1985
+ <h1 id='mainTitle'>{{title}}</h1>
1986
+ <div class='header-badge'>Automated Tests</div>
1987
+ </div>
1988
+ <div class='header-actions'>
1989
+ {{#if runId}}
1990
+ <div class='run-info'>
1991
+ <i class='fas fa-hashtag'></i>
1992
+ Run #{{runId}}
519
1993
  </div>
520
- </div>
1994
+ {{/if}}
1995
+ <button class='settings-btn' onclick='openSettings()' title='Report Settings'>
1996
+ <i class='fas fa-cog'></i>
1997
+ </button>
1998
+ {{#if runUrl}}
1999
+ <a href='{{runUrl}}' target='_blank' class='btn-primary-custom'>
2000
+ <i class='fas fa-external-link-alt'></i>
2001
+ Full Report
2002
+ </a>
2003
+ {{else}}
2004
+ <button class='btn-primary-custom' disabled>
2005
+ <i class='fas fa-external-link-alt'></i>
2006
+ Full Report
2007
+ </button>
2008
+ {{/if}}
521
2009
  </div>
2010
+ </header>
522
2011
 
523
- <!-- Schedule and information -->
524
- <div class='row level_3'>
525
- <div class='col-6'>
526
- <div class='statleft'>
527
- <div id='graff'></div>
528
- </div>
2012
+ <section class='stats-section'>
2013
+ <div class='stats-grid'>
2014
+ <div class='chart-container'>
2015
+ <div id='testChart'></div>
529
2016
  </div>
530
- <div class='col-6'>
531
- <div class='statright'>
532
- <div class='statdesc'>
533
- <!-- 1 -->
534
- <div class='statdesc__row'>
535
- <div class='statdesc__row_first'>
536
- <p>Status</p>
537
- </div>
538
- <div class='statstatus statstatus_{{status}}'>
539
- <div></div>
540
- <p>{{status}}</p>
541
- </div>
542
- </div>
543
- <!-- 1 -->
544
- <!-- 1 -->
545
- <div class='statdesc__row'>
546
- <div class='statdesc__row_first'>
547
- <p>Execution Duration</p>
548
- </div>
549
- <span>{{executionTime}}</span>
550
- </div>
551
- <!-- 1 -->
552
- <!-- 1 -->
553
- <div class='statdesc__row'>
554
- <div class='statdesc__row_first'>
555
- <p>Tests</p>
2017
+ <div class='stats-info'>
2018
+ <div class='stat-card'>
2019
+ <div class='stat-label'>
2020
+ <i class='fas fa-chart-line' style='font-size: 14px;'></i>
2021
+ Execution Status
2022
+ </div>
2023
+ <div class='stat-value'>
2024
+ <span class='status-badge status-{{status}}'>{{status}}</span>
2025
+ </div>
556
2026
  </div>
557
- <span>{{tests.length}}</span>
2027
+ <div class='stat-card'>
2028
+ <div class='stat-label'>
2029
+ <i class='fas fa-clock' style='font-size: 14px;'></i>
2030
+ Duration
2031
+ </div>
2032
+ <div class='stat-value'>{{executionTime}}</div>
558
2033
  </div>
559
- <!-- 1 -->
560
- <!-- 1 -->
561
- <div class='statdesc__row'>
562
- <div class='statdesc__row_first'>
563
- <p>Start Execution Date</p>
2034
+ <div class='stat-card'>
2035
+ <div class='stat-label'>
2036
+ <i class='fas fa-list-check' style='font-size: 14px;'></i>
2037
+ Total Tests
2038
+ </div>
2039
+ <div class='stat-value'>{{tests.length}}</div>
564
2040
  </div>
565
- <span>{{executionDate}}</span>
2041
+ <div class='stat-card'>
2042
+ <div class='stat-label'>
2043
+ <i class='fas fa-calendar-alt' style='font-size: 14px;'></i>
2044
+ Started At
2045
+ </div>
2046
+ <div class='stat-value'>{{executionDate}}</div>
566
2047
  </div>
567
- <!-- 1 -->
568
- </div>
569
2048
  </div>
570
2049
  </div>
2050
+ </section>
2051
+
2052
+ <section class='controls-section'>
2053
+ <div class='search-container'>
2054
+ <i class='fas fa-search search-icon'></i>
2055
+ <input
2056
+ type='text'
2057
+ class='search-input'
2058
+ placeholder='Search tests by name or suite...'
2059
+ id='searchInput'
2060
+ />
571
2061
  </div>
572
- <!-- Schedule and information -->
573
- <div class='row level_4'>
574
- <div class='col-12'>
575
- <div class='input-group' style='position:relative;width:100%;margin-top:45px'>
576
- <div class='input-group-prepend' style='position:absolute;z-index:1;border:none;left:20px'>
577
- <span class='hover-search input-group-text' id='basic-addon1' style='height:54px;background:#F6FAFF;border:none'>
578
- <i class='fa-solid fa-magnifying-glass' style='color:#474646;font-size:30px'></i>
579
- </span>
2062
+ <div class='filters-container'>
2063
+ <div class='filter-tabs'>
2064
+ <button class='filter-tab active' data-filter='all'>
2065
+ <i class='fas fa-list'></i>
2066
+ All
2067
+ <span class='count' id='countAll'>{{tests.length}}</span>
2068
+ </button>
2069
+ <button class='filter-tab' data-filter='passed'>
2070
+ <i class='fas fa-check-circle'></i>
2071
+ Passed
2072
+ <span class='count' id='countPassed'>{{getTestsByStatus tests "passed"}}</span>
2073
+ </button>
2074
+ <button class='filter-tab' data-filter='failed'>
2075
+ <i class='fas fa-times-circle'></i>
2076
+ Failed
2077
+ <span class='count' id='countFailed'>{{getTestsByStatus tests "failed"}}</span>
2078
+ </button>
2079
+ <button class='filter-tab' data-filter='skipped'>
2080
+ <i class='fas fa-forward'></i>
2081
+ Skipped
2082
+ <span class='count' id='countSkipped'>{{getTestsByStatus tests "skipped"}}</span>
2083
+ </button>
2084
+ <button class='filter-tab' data-filter='todo'>
2085
+ <i class='fas fa-list-check'></i>
2086
+ Todo
2087
+ <span class='count' id='countTodo'>{{getTestsByStatus tests "todo"}}</span>
2088
+ </button>
2089
+ </div>
2090
+ <div class='pagination-control'>
2091
+ <label for='pageSizeSelect' style='font-weight: 500; color: var(--gray-600);'>Tests per page:</label>
2092
+ <select id='pageSizeSelect' class='pagination-select'>
2093
+ <option value='10'>10</option>
2094
+ <option value='25'>25</option>
2095
+ <option value='50'>50</option>
2096
+ <option value='100'>100</option>
2097
+ </select>
2098
+ <label for='groupingSelect' style='font-weight: 500; color: var(--gray-600); margin-left: 20px;'>Group by:</label>
2099
+ <select id='groupingSelect' class='pagination-select'>
2100
+ <option value='all'>All Tests</option>
2101
+ <option value='suite'>By Suite</option>
2102
+ <option value='file'>By File</option>
2103
+ </select>
2104
+ </div>
2105
+ </div>
2106
+ </section>
2107
+
2108
+ <section class='tests-section'>
2109
+ <div id='testsContainer'>
2110
+ {{#if tests.length}}
2111
+ <!-- Test items will be dynamically inserted here -->
2112
+ {{else}}
2113
+ <div class='empty-state'>
2114
+ <div class='empty-icon'>
2115
+ <i class='fas fa-inbox'></i>
580
2116
  </div>
581
- <input
582
- type='text'
583
- style='border:none;height:54px;background:#F6FAFF;font-weight:600;font-size:15px;'
584
- class='form-control inputSearch'
585
- aria-label='Search'
586
- aria-describedby='basic-addon1'
587
- />
2117
+ <div class='empty-title'>No Tests Found</div>
2118
+ <div class='empty-description'>No tests were executed in this run.</div>
588
2119
  </div>
2120
+ {{/if}}
2121
+ </div>
2122
+ </section>
2123
+
2124
+ <section class='pagination-container' id='paginationContainer'>
2125
+ <!-- Pagination will be dynamically inserted here -->
2126
+ </section>
2127
+ </div>
2128
+
2129
+ <div id='settingsModal' class='settings-modal'>
2130
+ <div class='settings-modal-content'>
2131
+ <div class='settings-modal-header'>
2132
+ <div class='settings-modal-title'>
2133
+ <i class='fas fa-cog'></i>
2134
+ Report Settings
589
2135
  </div>
2136
+ <button class='settings-modal-close' onclick='closeSettings()'>
2137
+ <i class='fas fa-times'></i>
2138
+ </button>
590
2139
  </div>
2140
+ <div class='settings-modal-body'>
2141
+ <div class='settings-tabs-nav'>
2142
+ <button class='settings-tab active' data-tab='metadata' onclick='switchSettingsTab(event)'>
2143
+ <i class='fas fa-tags'></i>
2144
+ Metadata
2145
+ </button>
2146
+ <button class='settings-tab' data-tab='envvars' onclick='switchSettingsTab(event)'>
2147
+ <i class='fas fa-code'></i>
2148
+ Environment Variables
2149
+ </button>
2150
+ </div>
591
2151
 
592
- <div class='row'>
593
- <div class='col-12 d-flex justify-content-start' style='margin-top:100px;'>
594
- <div class='btn-group menuTests' role='group' aria-label='Basic radio toggle button group'>
595
- <input
596
- type='radio'
597
- class='btn-check allTest'
598
- name='groupTest'
599
- id='allTest'
600
- autocomplete='off'
601
- category='all'
602
- checked
603
- />
604
-
605
- <label class='btn btn-all' for='allTest' style='padding:9px;margin-right:8px;background:#A0CAFF;color:#fff;border:none;text-align:center;border-radius:3px;font-weight:600;font-size:12px;width:83px;height:37px'>All
606
- <span style='color:#fff;font-weight:600;font-size:12px;' class='numTest numAll' name='numTest' lcategory='all'>0</span>
607
- </label>
608
-
609
- <input
610
- type='radio'
611
- class='btn-check passedTest'
612
- name='groupTest'
613
- id='passedTest'
614
- autocomplete='off'
615
- category='passed'
616
- />
617
-
618
- <label class='btn btn-passed' for='passedTest' style='padding:9px;background:#75B583;color:#fff;border:none;text-align:center;border-radius:3px;font-weight:600;font-size:12px;width:83px;height:37px'>Passed
619
- <span style='color:#fff;font-weight:600;font-size:12px;' class='numTest numPassed' name='numTest' lcategory='passed'>0</span>
620
- </label>
621
-
622
- <input
623
- type='radio'
624
- class='btn-check failedTest'
625
- name='groupTest'
626
- id='failedTest'
627
- autocomplete='off'
628
- category='failed'
629
- />
630
-
631
- <label class='btn btn-failed' for='failedTest' style='padding:9px;margin-left:9px;background:#FF6363;color:#fff;border:none;text-align:center;border-radius:3px;font-weight:600;font-size:12px;width:83px;height:37px'>Failed
632
- <span style='color:#fff;font-weight:600;font-size:12px;' class='numTest numFailed' name='numTest' lcategory='failed'>0</span>
633
- </label>
634
-
635
- <input
636
- type='radio'
637
- class='btn-check skippedTest'
638
- name='groupTest'
639
- id='skippedTest'
640
- autocomplete='off'
641
- category='skipped'
642
- />
643
-
644
- <label class='btn btn-skipped' for='skippedTest' style='padding:9px;margin-left:9px;background:#FFC350;color:#fff;border:none;text-align:center;border-radius:3px;font-weight:600;font-size:12px;width:83px;height:37px'>Skipped
645
- <span style='color:#fff;font-weight:600;font-size:12px;' class='numTest numSkipped' name='numTest' lcategory='skipped'>0</span>
646
- </label>
2152
+ <div class='settings-tab-content active' data-tab='metadata'>
2153
+ <div class='settings-section'>
2154
+ <div class='settings-section-title'>
2155
+ <i class='fas fa-tags'></i>
2156
+ Visible Metadata Fields
2157
+ </div>
2158
+ <div id='metaFieldsList' class='settings-meta-grid'>
2159
+ <!-- Meta fields will be dynamically inserted here -->
2160
+ </div>
647
2161
  </div>
648
2162
  </div>
649
- </div>
650
-
651
- <div class="row level_5">
652
- <div class="col-12">
653
- {{#if tests.length}}
654
- <!-- TOP pagination & select components -->
655
- <div class="d-flex justify-content-between" style="height: 40px;margin-top:45px">
656
- <nav id="pagination">
657
- <ul class="pagination">
658
- </ul>
659
- </nav>
660
- {{ selectComponent }}
661
- </div>
662
- <!-- Test data section -->
663
- <div class="testWrapp">
664
- {{#each tests}}
665
- <div class="testitem d-none" name="testitem" type="dummy" category="false">
666
- <div class="testitem__top">
667
- <div class='d-flex'>
668
- <div class="testitem__icon">
669
- <i class="fa-solid fa-circle-chevron-right testitem__ico testitem__ico_right ml-2"></i>
670
- <i class="fa-solid fa-circle-chevron-down d-none testitem__ico testitem__ico_down ml-2"></i>
671
- </div>
672
- <p class="testitem__name">Test</p>
673
- </div>
674
-
675
- <svg class='folderSvg' width="17" height="17" style='margin-right:20px;margin-top: 3px' viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
676
- <path d="M16.6161 1.64526H9.84355C9.62419 1.64526 9.43226 1.64526 9.2129 1.9743L8.85645 2.46784H0.85C0.383871 2.46784 0 2.90655 0 3.37268V14.1485C0 14.6146 0.383871 15.0807 0.85 15.0807H16.15C16.6161 15.0807 17 14.6146 17 14.1485V5.81301V3.4001V1.9743C17 1.78236 16.8355 1.64526 16.6161 1.64526Z" fill="#6D6D6D"/>
677
- <path d="M16.6435 12.7227C16.8629 12.5582 17 12.3114 17 12.0372V5.81301V3.4001V1.9743C17 1.78236 16.8355 1.64526 16.6161 1.64526H9.84355C9.62419 1.64526 9.43226 1.64526 9.2129 1.9743L8.85645 2.46784H0.85C0.41129 2.46784 0.0548387 2.8243 0 3.23559L16.6435 12.7227Z" fill="#565656"/>
678
- <path d="M0.575806 3.56445L16.6435 12.6677C16.8629 12.5032 17 12.3112 17 12.037V5.81284V4.27736L15.9581 3.56445H0.575806Z" fill="#474646"/>
679
- <path d="M15.629 3.56445H1.37097V13.9838H15.629V3.56445Z" fill="white"/>
680
- <path d="M15.629 3.56445H8.77417V13.9838H15.629V3.56445Z" fill="#EFEFEF"/>
681
- <path d="M17 14.5323C17 14.9984 16.6161 15.3549 16.15 15.3549H0.85C0.383871 15.3549 0 14.9984 0 14.5323L0.274194 5.84039C0.274194 5.37426 0.658064 4.93555 1.12419 4.93555H15.9032C16.3694 4.93555 16.7532 5.37426 16.7532 5.84039L17 14.5323Z" fill="#A1A1A1"/>
682
- <path d="M16.8903 14.9437L0.329032 5.53882C0.274194 5.6485 0.274194 5.78559 0.274194 5.89527L0 14.5324C0 14.9985 0.383871 15.3549 0.85 15.3549H16.15C16.4516 15.3549 16.7258 15.1904 16.8903 14.9437Z" fill="#565656"/>
683
- <path d="M6.77259 13.2161C6.71775 13.2161 6.63549 13.1887 6.55323 13.1613C6.41613 13.1064 6.30646 12.9693 6.30646 12.8048V7.67739C6.30646 7.54029 6.41613 7.37577 6.55323 7.32093C6.69033 7.2661 6.85484 7.2661 6.99194 7.34835L10.4742 9.92577C10.5839 10.008 10.6387 10.1177 10.6387 10.2548C10.6387 10.3919 10.5839 10.5016 10.4742 10.5838L6.99194 13.1613C6.9371 13.2161 6.85484 13.2161 6.77259 13.2161Z" fill="white"/>
684
- </svg>
685
- </div>
686
2163
 
687
- <div class="testitem__body d-none">
688
- <div class='testitem__menu'>
689
- <span type='steps' class='testitem__mitem testitem__mitem_active'>Steps
690
- <i class='fa-solid fa-arrow-right'></i></span>
691
- <span type='status' class='testitem__mitem'>Status
692
- <i class='fa-solid fa-arrow-right'></i></span>
693
- <span type='message' class='testitem__mitem'>Message
694
- <i class='fa-solid fa-arrow-right'></i></span>
695
- <span type='files' class='testitem__mitem'>Files <i class='fa-solid fa-arrow-right'></i></span>
696
- </div>
697
-
698
- <div class="testitem__content">
699
- <!-- 1 -->
700
- <div class="testitem__case" type="steps">
701
- <div class="testitem__block">
702
- <span>...</span>
703
- </div>
2164
+ <div class='settings-tab-content' data-tab='envvars'>
2165
+ <div class='env-vars-container'>
2166
+ <div class='env-vars-group'>
2167
+ <div class='env-vars-group-title'>
2168
+ TESTOMATIO Variables
2169
+ <span class='env-vars-count'>{{ObjectLength envVars.testomatio}} total</span>
2170
+ </div>
2171
+ <div class='env-vars-list'>
2172
+ {{#each envVars.testomatio}}
2173
+ <div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
2174
+ <div class='env-var-info'>
2175
+ <div class='env-var-key'>{{@key}}</div>
2176
+ <div class='env-var-description'>{{this.description}}</div>
704
2177
  </div>
705
- <!-- 1 -->
706
-
707
- <!-- 2 -->
708
- <div class='testitem__case d-none' type='status'>
709
- <div class='testitem__block'>
710
- <span>...</span>
711
- </div>
2178
+ <div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
2179
+ {{#if this.isSensitive}}
2180
+ <em>confidential</em>
2181
+ {{else if this.isSet}}
2182
+ {{this.value}}
2183
+ {{else}}
2184
+ <em>(not set)</em>
2185
+ {{/if}}
712
2186
  </div>
713
- <!-- 2 -->
714
-
715
- <!-- 3 -->
716
- <div class='testitem__case d-none' type='message'>
717
- <div class='testitem__block'>
718
- <span>...</span>
719
- </div>
2187
+ </div>
2188
+ {{/each}}
2189
+ </div>
2190
+ </div>
2191
+ <div class='env-vars-group'>
2192
+ <div class='env-vars-group-title'>
2193
+ S3 Variables
2194
+ <span class='env-vars-count'>{{ObjectLength envVars.s3}} total</span>
2195
+ </div>
2196
+ <div class='env-vars-list'>
2197
+ {{#each envVars.s3}}
2198
+ <div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
2199
+ <div class='env-var-info'>
2200
+ <div class='env-var-key'>{{@key}}</div>
2201
+ <div class='env-var-description'>{{this.description}}</div>
720
2202
  </div>
721
- <!-- 3 -->
722
-
723
- <!-- 4 -->
724
- <div class='testitem__case d-none' type='files'>
725
- <div class='testitem__block'>
726
- <span>...</span>
727
- </div>
2203
+ <div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
2204
+ {{#if this.isSensitive}}
2205
+ <em>confidential</em>
2206
+ {{else if this.isSet}}
2207
+ {{this.value}}
2208
+ {{else}}
2209
+ <em>(not set)</em>
2210
+ {{/if}}
728
2211
  </div>
729
- <!-- 4 -->
730
2212
  </div>
731
- </div>
2213
+ {{/each}}
732
2214
  </div>
733
- {{/each}}
2215
+ </div>
734
2216
  </div>
735
- <!-- BOTTOM pagination & select components -->
736
- <nav class="mt-2">
737
- <ul class="pagination">
738
- </ul>
739
- </nav>
740
- {{else}}
741
- <!-- No tests found section -->
742
- {{ emptyDataComponent }}
743
- {{/if}}
744
2217
  </div>
745
2218
  </div>
2219
+ <div class='settings-footer'>
2220
+ <button class='settings-btn-action settings-btn-reset' onclick='resetToDefaults()'>
2221
+ Reset to Defaults
2222
+ </button>
2223
+ <button class='settings-btn-action settings-btn-cancel' onclick='closeSettings()'>
2224
+ Cancel
2225
+ </button>
2226
+ <button class='settings-btn-action settings-btn-save' onclick='saveSettings()'>
2227
+ Apply Changes
2228
+ </button>
2229
+ </div>
746
2230
  </div>
747
- </section>
2231
+ </div>
748
2232
 
749
2233
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
750
2234
  <script type="text/javascript">
751
- // Load google charts
2235
+ let allTests = {{{ pageDispleyElements tests }}}.totalTests;
2236
+ let currentFilter = 'all';
2237
+ let currentPage = 1;
2238
+ let pageSize = 10;
2239
+ let searchTerm = '';
2240
+ let hasTodoTests = false;
2241
+ let reportTitle = '{{title}}';
2242
+ let groupingMode = 'all';
2243
+
752
2244
  google.charts.load('current', {'packages':['corechart']});
753
- google.charts.setOnLoadCallback(drawChart);
754
-
755
- // Draw the chart and set the chart values
756
- function drawChart() {
757
- let data = {};
758
- const passedTests = {{ getTestsByStatus tests "PASSED" }};
759
- const failedTests = {{ getTestsByStatus tests "FAILED" }};
760
- const skippedTests = {{ getTestsByStatus tests "SKIPPED" }};
761
-
762
- if (passedTests === 0 && failedTests === 0 && skippedTests === 0) {
763
- data = google.visualization.arrayToDataTable([
764
- ['Task', 'Tests'],
765
- ['No Tests', 1]
766
- ])
767
- }
768
- else {
769
- data = google.visualization.arrayToDataTable([
770
- ['Task', 'Tests'],
771
- ['No Tests', 0],
772
- ['Passed', passedTests],
773
- ['Failed', failedTests],
774
- ['Skipped', skippedTests],
775
- ]);
776
- }
777
- // Optional: add a title and set the width and height of the chart
2245
+ google.charts.setOnLoadCallback(drawTestChart);
2246
+
2247
+ function createMetadataItems(metadata) {
2248
+ if (!metadata || Object.keys(metadata).length === 0) return '';
2249
+
2250
+ return Object.entries(metadata).map(([key, value]) => `
2251
+ <div class='metadata-item meta-item' data-meta-key='${key}'>
2252
+ <div class='metadata-key'>${key}</div>
2253
+ <div class='metadata-value'>${typeof value === 'object' ? JSON.stringify(value, null, 2) : value}</div>
2254
+ </div>
2255
+ `).join('');
2256
+ }
2257
+
2258
+ function drawTestChart() {
2259
+ const passedTests = {{getTestsByStatus tests "passed"}};
2260
+ const failedTests = {{getTestsByStatus tests "failed"}};
2261
+ const skippedTests = {{getTestsByStatus tests "skipped"}};
2262
+ const todoTests = {{getTestsByStatus tests "todo"}};
2263
+
2264
+ const rows = [
2265
+ ['Status', 'Count'],
2266
+ ['Passed', passedTests],
2267
+ ['Failed', failedTests],
2268
+ ['Skipped', skippedTests],
2269
+ ];
2270
+
2271
+ if (todoTests > 0) rows.push(['Todo', todoTests]);
2272
+
2273
+ const data = google.visualization.arrayToDataTable(rows);
2274
+
2275
+ const screenWidth = window.innerWidth;
2276
+ let chartHeight = 350;
2277
+ let chartAreaHeight = '80%';
2278
+ let chartAreaWidth = '95%';
2279
+ let pieSliceFontSize = 12;
2280
+ let legendFontSize = 12;
2281
+
2282
+ if (screenWidth < 480) {
2283
+ chartHeight = 180;
2284
+ chartAreaHeight = '70%';
2285
+ chartAreaWidth = '90%';
2286
+ pieSliceFontSize = 9;
2287
+ legendFontSize = 9;
2288
+ } else if (screenWidth < 768) {
2289
+ chartHeight = 220;
2290
+ chartAreaHeight = '75%';
2291
+ chartAreaWidth = '92%';
2292
+ pieSliceFontSize = 10;
2293
+ legendFontSize = 10;
2294
+ } else if (screenWidth < 1024) {
2295
+ chartHeight = 260;
2296
+ chartAreaHeight = '75%';
2297
+ chartAreaWidth = '94%';
2298
+ pieSliceFontSize = 11;
2299
+ legendFontSize = 11;
2300
+ }
2301
+
778
2302
  const options = {
779
- 'title': '',
780
- 'width': 550,
781
- 'height': 300,
782
- 'colors': ['#A9C7FF', '#75B583', '#FF6363', '#FFC350'],
783
- 'legend': 'bottom',
784
- 'is3D': true
785
- };
786
- // Display the chart inside the <div> element with id="piechart"
787
- const chart = new google.visualization.PieChart(document.getElementById('graff'));
2303
+ title: reportTitle + ' Distribution',
2304
+ pieHole: 0.4,
2305
+ colors: todoTests > 0
2306
+ ? ['#10b981', '#ef4444', '#f59e0b', '#8b5cf6']
2307
+ : ['#10b981', '#ef4444', '#f59e0b'],
2308
+ legend: { position: 'bottom', alignment: 'center' },
2309
+ height: chartHeight,
2310
+ width: '100%',
2311
+ chartArea: {
2312
+ left: '5%',
2313
+ top: 20,
2314
+ width: chartAreaWidth,
2315
+ height: chartAreaHeight
2316
+ },
2317
+ pieSliceTextStyle: {
2318
+ fontSize: pieSliceFontSize
2319
+ },
2320
+ legend: {
2321
+ position: 'bottom',
2322
+ alignment: 'center',
2323
+ textStyle: {
2324
+ fontSize: legendFontSize
2325
+ },
2326
+ maxLines: 1
2327
+ },
2328
+ titleTextStyle: {
2329
+ fontSize: screenWidth < 768 ? 14 : 16
2330
+ },
2331
+ enableInteractivity: true,
2332
+ sliceVisibilityThreshold: 0,
2333
+ tooltip: {
2334
+ trigger: 'hover',
2335
+ showColorCode: true
2336
+ }
2337
+ };
2338
+
2339
+ const chart = new google.visualization.PieChart(document.getElementById('testChart'));
2340
+
2341
+ google.visualization.events.addListener(chart, 'ready', function() {
2342
+ const chartContainer = document.getElementById('testChart');
2343
+ chartContainer.style.overflow = 'visible';
2344
+ });
2345
+
788
2346
  chart.draw(data, options);
789
2347
  }
790
2348
 
791
- // processing and adding test data to the template
792
- function addOneTest(category,testData = {}){
793
- const { suite_title, title, steps, message, files } = testData;
794
- // collapse - expand block
795
- const clone = createClone(category, suite_title, title);
796
- const testitem__top = clone.querySelector('.testitem__top');
2349
+ function createTestItem(test, index) {
2350
+ const testItem = document.createElement('div');
2351
+ testItem.className = 'test-item fade-in';
2352
+ testItem.dataset.testId = `test-${index}`;
2353
+ testItem.dataset.testIndex = index;
2354
+
2355
+ if (test.traces) {
2356
+ if (!window.testTraces) window.testTraces = {};
2357
+ if (typeof test.traces === 'string') {
2358
+ try {
2359
+ window.testTraces[index] = JSON.parse(test.traces);
2360
+ } catch (e) {
2361
+ console.error('Failed to parse traces:', e);
2362
+ window.testTraces[index] = [];
2363
+ }
2364
+ } else {
2365
+ window.testTraces[index] = test.traces;
2366
+ }
2367
+ }
2368
+
2369
+ const statusClass = test.status.toLowerCase();
2370
+ const hasRetries = test.retries && test.retries.retryCount > 0;
2371
+ const hasTraces = test.traces && (typeof test.traces === 'string' ? test.traces.trim().length > 0 : test.traces.length > 0);
2372
+ const hasArtifacts = test.artifacts && test.artifacts.length > 0;
2373
+ const hasMetadata = test.meta && Object.keys(test.meta).length > 0;
2374
+ const hasStack = statusClass === 'failed' && test.stack && (typeof test.stack === 'string' ? test.stack.trim().length > 0 : true);
2375
+ const attemptsCount = (test.retries && Array.isArray(test.retries.attempts)) ? test.retries.attempts.length : 0;
2376
+ const retryCount = Math.max(0, attemptsCount - 1);
2377
+ const isTodo = statusClass === 'todo';
2378
+ const isSkippedOrTodo = statusClass === 'skipped' || statusClass === 'todo';
2379
+ const displayTime = isSkippedOrTodo ? '-' : formatDuration(test.run_time || 0);
2380
+
2381
+ testItem.innerHTML = `
2382
+ <div class='test-header' onclick='toggleTest(this)'>
2383
+ <div class='test-info'>
2384
+ <div class='test-status-icon ${statusClass}'>
2385
+ <i class='fas fa-${getStatusIcon(statusClass)}'></i>
2386
+ </div>
2387
+ <div class='test-details'>
2388
+ <div class='test-title'>
2389
+ ${test.title || 'Untitled Test'}
2390
+ ${test.artifactsUploaded ? `
2391
+ <span class='artifact-upload-indicator' title='Artifacts uploaded to storage'>
2392
+ <i class='fas fa-cloud-upload-alt'></i>
2393
+ </span>
2394
+ ` : ''}
2395
+ </div>
2396
+ <div class='test-suite'>${test.suite_title || 'Unknown Suite'}</div>
2397
+ </div>
2398
+ </div>
2399
+ <div class='test-meta'>
2400
+ <div class='test-duration'>
2401
+ <i class='fas fa-clock'></i>
2402
+ ${displayTime}
2403
+ </div>
2404
+ <div class='test-badges'>
2405
+ ${test.flaky ? '<span class=\"badge badge-flaky\">Flaky</span>' : ''}
2406
+ ${hasRetries ? `<span class=\"badge badge-retry\">Retries: ${test.retries.retryCount}</span>` : ''}
2407
+ </div>
2408
+ <i class='fas fa-chevron-down expand-icon'></i>
2409
+ </div>
2410
+ </div>
2411
+ <div class='test-content'>
2412
+ <div class='test-tabs'>
2413
+ ${isTodo ? `
2414
+ <button class='test-tab active' onclick='showTestTab(this, \"info\")'>
2415
+ <i class='fas fa-info-circle'></i> Info
2416
+ </button>
2417
+ ` : `
2418
+ <button class='test-tab active' onclick='showTestTab(this, \"steps\")'>
2419
+ <i class='fas fa-list-ol'></i> Steps
2420
+ </button>
2421
+ <button class='test-tab' onclick='showTestTab(this, \"message\")'>
2422
+ <i class='fas fa-comment-dots'></i> Message
2423
+ </button>
2424
+ ${hasStack ? `<button class='test-tab' onclick='showTestTab(this, \"stack\")'>
2425
+ <i class='fas fa-layer-group'></i> Stack
2426
+ </button>` : ''}
2427
+ ${hasRetries ? `<button class='test-tab' onclick='showTestTab(this, \"retries\")'>
2428
+ <i class='fas fa-redo'></i> Retries
2429
+ </button>` : ''}
2430
+ ${hasArtifacts ? `<button class='test-tab' onclick='showTestTab(this, \"attachments\")'>
2431
+ <i class='fas fa-paperclip'></i> Attachments
2432
+ </button>` : ''}
2433
+ ${hasMetadata ? `<button class='test-tab' onclick='showTestTab(this, \"metadata\")'>
2434
+ <i class='fas fa-tags'></i> Metadata
2435
+ </button>` : ''}
2436
+ ${hasTraces ? `<button class='test-tab' onclick='showTestTab(this, \"traces\")'>
2437
+ <i class='fas fa-bug'></i> Traces
2438
+ </button>` : ''}
2439
+ `}
2440
+ </div>
2441
+ ${isTodo ? `
2442
+ <div class='test-tab-content active' data-tab='info'>
2443
+ <div class='todo-info'>
2444
+ <div class='todo-title'>
2445
+ <i class='fas fa-list-ul'></i>
2446
+ TODO Test
2447
+ </div>
2448
+ <div class='todo-description'>
2449
+ This test is marked as TODO and needs to be implemented.
2450
+ </div>
2451
+ <div class='todo-details'>
2452
+ <p><strong>Test Title:</strong> ${test.title || 'Untitled TODO Test'}</p>
2453
+ <p><strong>Suite:</strong> ${test.suite_title || 'Unknown Suite'}</p>
2454
+ <p><strong>Status:</strong> Implementation required</p>
2455
+ </div>
2456
+ </div>
2457
+ </div>
2458
+ ` : `
2459
+ <div class='test-tab-content active' data-tab='steps'>
2460
+ <div class='steps-container' id='steps-${index}'>
2461
+ ${(() => {
2462
+ const hasStepsArray = test.stepsArray && Array.isArray(test.stepsArray) && test.stepsArray.length > 0;
2463
+ const hasStepsString = test.steps && typeof test.steps === 'string' && test.steps.trim().length > 0;
2464
+
2465
+ if (hasStepsArray) {
2466
+ return renderStepsTree(test.stepsArray);
2467
+ } else if (hasStepsString) {
2468
+ const isErrorMessage = test.steps.match(/^Error:/i);
2469
+
2470
+ if (!isErrorMessage) {
2471
+ return renderStepsTree(test.steps);
2472
+ }
2473
+ }
2474
+
2475
+ return '<div style=\'padding: 20px; color: var(--gray-500); text-align: center;\'>No steps recorded</div>';
2476
+ })()}
2477
+ </div>
2478
+ </div>
2479
+ <div class='test-tab-content' data-tab='message'>
2480
+ <div class='message-block ${statusClass}'>
2481
+ <strong>${statusClass.toUpperCase()}:</strong><br>
2482
+ ${test.message || 'No message available'}
2483
+ </div>
2484
+ </div>
2485
+ ${hasStack ? `<div class='test-tab-content' data-tab='stack'>
2486
+ <div class='code-block'>${test.stack}</div>
2487
+ </div>` : ''}
2488
+ `}
2489
+ ${hasRetries ? `<div class='test-tab-content' data-tab='retries'>
2490
+ <div class='retry-info'>
2491
+ <div class='retry-title'>
2492
+ <i class='fas fa-redo'></i>
2493
+ Retry Information (${retryCount} retries, ${attemptsCount} attempts)
2494
+ </div>
2495
+ <div class='retry-attempts'>
2496
+ ${createRetryAttempts(test.retries)}
2497
+ </div>
2498
+ </div>
2499
+ </div>` : ''}
2500
+ ${hasArtifacts ? `<div class='test-tab-content' data-tab='attachments'>
2501
+ <div class='attachments-grid'>
2502
+ ${createAttachmentItems(test.artifacts)}
2503
+ </div>
2504
+ </div>` : ''}
2505
+ ${Object.keys(test.meta || {}).length > 0 ? `<div class='test-tab-content' data-tab='metadata'>
2506
+ <div class='metadata-grid' data-test-meta></div>
2507
+ </div>` : ''}
2508
+ ${hasTraces ? `<div class='test-tab-content' data-tab='traces'>
2509
+ <div class='trace-viewer-container'></div>
2510
+ </div>` : ''}
2511
+ </div>
2512
+ `;
797
2513
 
798
- setTestItemContent(clone, steps, category, message, files);
799
- addCollapseExpandListener(testitem__top);
800
- initializeMenu(clone);
2514
+ return testItem;
801
2515
  }
802
2516
 
803
- function createClone(category, suite_title, title) {
804
- const page = document.querySelector('.report');
805
- const wrapp = page.querySelector('.testWrapp');
806
- const dummy = wrapp.querySelector('div[name="testitem"][type="dummy"]');
807
- const clone = dummy.cloneNode(true);
2517
+ function getStatusIcon(status) {
2518
+ const icons = {
2519
+ 'passed': 'check',
2520
+ 'failed': 'times',
2521
+ 'skipped': 'forward',
2522
+ 'pending': 'clock',
2523
+ 'todo': 'circle'
2524
+ };
2525
+ return icons[status] || 'question';
2526
+ }
808
2527
 
809
- clone.setAttribute('type', 'clone');
810
- clone.setAttribute('category', category);
811
- clone.classList.remove('d-none');
812
- wrapp.append(clone);
2528
+ function formatDuration(milliseconds) {
2529
+ if (!milliseconds || milliseconds === 0 || milliseconds === null) return '0ms';
813
2530
 
814
- const testitem__name = clone.querySelector('.testitem__name');
815
- testitem__name.innerHTML = suite_title ? suite_title + " - " + title : title;
2531
+ const totalSeconds = Math.floor(milliseconds / 1000);
2532
+ const minutes = Math.floor(totalSeconds / 60);
2533
+ const seconds = totalSeconds % 60;
2534
+ const ms = milliseconds % 1000;
816
2535
 
817
- return clone;
2536
+ if (minutes > 0) {
2537
+ return `${minutes}m ${seconds}s ${ms}ms`;
2538
+ } else if (seconds > 0) {
2539
+ return `${seconds}s ${ms}ms`;
2540
+ } else {
2541
+ return `${ms}ms`;
2542
+ }
818
2543
  }
819
2544
 
820
- function setTestItemContent(clone, steps, category, message, files) {
821
- const body = clone.querySelector('.testitem__body'),
822
- top = clone.querySelector('.testitem__top'),
823
- image = top.querySelector('.testitem__icon'),
824
- folderSvg = top.querySelector('.folderSvg');
2545
+ function parseStepsToHtml(stepsText) {
2546
+ if (!stepsText || stepsText === 'No steps recorded') {
2547
+ return `<div class="step-item">
2548
+ <div class="step-content">
2549
+ <p class="step-text">${stepsText || 'No steps recorded'}</p>
2550
+ </div>
2551
+ </div>`;
2552
+ }
2553
+
2554
+ const raw = String(stepsText);
2555
+ const lines = raw
2556
+ .split(/<br\s*\/?>|\n/gi)
2557
+ .map(l => l.trim())
2558
+ .filter(Boolean);
2559
+
2560
+ let stepNumber = 1;
2561
+ let html = '';
2562
+
2563
+ lines.forEach(line => {
2564
+ const trimmedLine = line.trim();
2565
+
2566
+ const numberedMatch = trimmedLine.match(/^(\d+)\.\s*(.+)$/);
2567
+ if (numberedMatch) {
2568
+ const [, num, text] = numberedMatch;
2569
+ stepNumber = parseInt(num, 10);
2570
+ html += createStepHtml(text, stepNumber);
2571
+ stepNumber++;
2572
+ return;
2573
+ }
2574
+
2575
+ const checkmarkMatch = trimmedLine.match(/^([✓✗])\s*(.+)$/);
2576
+ if (checkmarkMatch) {
2577
+ const [, symbol, text] = checkmarkMatch;
2578
+ const status = symbol === '✓' ? 'passed' : 'failed';
2579
+ html += createStepHtml(text, stepNumber, status);
2580
+ stepNumber++;
2581
+ return;
2582
+ }
2583
+
2584
+ const arrowMatch = trimmedLine.match(/^(→|▶|▸|▹)\s*(.+)$/);
2585
+ if (arrowMatch) {
2586
+ const [, symbol, text] = arrowMatch;
2587
+ html += createStepHtml(text, stepNumber, null, symbol);
2588
+ stepNumber++;
2589
+ return;
2590
+ }
2591
+
2592
+ const bracketMatch = trimmedLine.match(/^\[([^\]]+)\]\s*(.+)$/);
2593
+ if (bracketMatch) {
2594
+ const [, category, text] = bracketMatch;
2595
+ html += createStepHtml(text, stepNumber, null, category);
2596
+ stepNumber++;
2597
+ return;
2598
+ }
2599
+
2600
+ const keywordMatch = trimmedLine.match(/^(When|Then|And|Given|But|When\s+I|Then\s+I|And\s+I|Given\s+I|But\s+I)\s+(.+)$/i);
2601
+ if (keywordMatch) {
2602
+ const [, keyword, text] = keywordMatch;
2603
+ html += createStepHtml(text, stepNumber, null, keyword);
2604
+ stepNumber++;
2605
+ return;
2606
+ }
2607
+
2608
+ html += createStepHtml(trimmedLine, stepNumber);
2609
+ stepNumber++;
2610
+ });
2611
+
2612
+ return html || `<div class="step-item">
2613
+ <div class="step-content">
2614
+ <p class="step-text">No valid steps found</p>
2615
+ </div>
2616
+ </div>`;
2617
+ }
2618
+
2619
+ function renderStepsTree(steps, depth = 0) {
2620
+ if (Array.isArray(steps) && steps.length > 0) {
2621
+ let html = '';
2622
+ let stepNumber = 1;
2623
+
2624
+ steps.forEach((step, index) => {
2625
+ if (!step) return;
2626
+
2627
+ const hasChildren = step.steps && Array.isArray(step.steps) && step.steps.length > 0;
2628
+ const stepId = `step-${depth}-${index}`;
2629
+ const status = step.status || 'passed';
2630
+ const duration = step.duration || 0;
2631
+ const category = step.category || '';
2632
+
2633
+ html += `
2634
+ <div class="step-item step-level-${depth}" data-step-id="${stepId}">
2635
+ <div class="step-number">${stepNumber}</div>
2636
+ <div class="step-content">
2637
+ <p class="step-text">${step.title || 'Untitled step'}</p>
2638
+ ${category && category !== 'user' ? `<div class="step-category">${category}</div>` : ''}
2639
+ <div class="step-time">
2640
+ Step ${stepNumber}
2641
+ ${duration > 0 ? `<span class="step-duration"><i class="fas fa-clock"></i> ${formatDuration(duration)}</span>` : ''}
2642
+ </div>
2643
+ </div>
2644
+ <div class="step-status ${status}"></div>
2645
+ </div>
2646
+ `;
2647
+
2648
+ if (hasChildren) {
2649
+ html += `
2650
+ <div class="step-children">
2651
+ ${renderStepsTree(step.steps, depth + 1)}
2652
+ </div>
2653
+ `;
2654
+ }
2655
+
2656
+ stepNumber++;
2657
+ });
2658
+
2659
+ return html;
2660
+ }
825
2661
 
826
- if(category === 'passed') {
827
- image.classList.add('passed');
2662
+ if (typeof steps === 'string') {
2663
+ return parseStepsToHtml(steps);
828
2664
  }
829
- else if(category === 'skipped') {
830
- image.classList.add('skipped');
2665
+
2666
+ return `<div class="step-item">
2667
+ <div class="step-content">
2668
+ <p class="step-text">${steps}</p>
2669
+ </div>
2670
+ </div>`;
2671
+ }
2672
+
2673
+ function createStepHtml(text, number, status = null, category = null) {
2674
+ const statusClass = status || 'passed';
2675
+ const categoryText = category && category !== 'user' ? `<div class="step-category">${category}</div>` : '';
2676
+ const durationIcon = status ? `<div class="step-duration"><i class="fas fa-clock"></i> auto</div>` : '';
2677
+
2678
+ return `<div class="step-item">
2679
+ <div class="step-number">${number}</div>
2680
+ <div class="step-content">
2681
+ <p class="step-text">${text}</p>
2682
+ ${categoryText}
2683
+ <div class="step-time">Step ${number}${durationIcon}</div>
2684
+ </div>
2685
+ <div class="step-status ${statusClass}"></div>
2686
+ </div>`;
2687
+ }
2688
+
2689
+ function createRetryAttempts(retries) {
2690
+ const attempts = Array.isArray(retries?.attempts) ? retries.attempts : [];
2691
+ if (!attempts.length) return '';
2692
+
2693
+ return attempts.map((a, idx) => {
2694
+ const status = String(a?.status || 'unknown').toLowerCase();
2695
+ const isFinal = idx === attempts.length - 1;
2696
+
2697
+ const icon =
2698
+ status === 'passed' ? 'check' :
2699
+ status === 'failed' ? 'times' :
2700
+ status === 'skipped' ? 'forward' :
2701
+ 'question';
2702
+
2703
+ const cls =
2704
+ status === 'passed' ? 'passed' :
2705
+ status === 'failed' ? 'failed' :
2706
+ status === 'skipped' ? 'skipped' :
2707
+ 'unknown';
2708
+
2709
+ return `
2710
+ <div class='retry-attempt ${cls}'>
2711
+ <i class='fas fa-${icon}'></i>
2712
+ Attempt ${idx + 1}${isFinal ? ' (final)' : ''}
2713
+ </div>
2714
+ `;
2715
+ }).join('');
2716
+ }
2717
+
2718
+ function normalizeArtifact(artifact) {
2719
+ if (!artifact) return { filePath: '', displayName: 'attachment' };
2720
+
2721
+ if (typeof artifact === 'string') {
2722
+ return { filePath: artifact, displayName: artifact };
831
2723
  }
832
- else {
833
- image.classList.add('failed');
2724
+
2725
+ if (typeof artifact === 'object') {
2726
+ const filePath =
2727
+ (typeof artifact.path === 'string' && artifact.path) ||
2728
+ (typeof artifact.url === 'string' && artifact.url) ||
2729
+ (typeof artifact.href === 'string' && artifact.href) ||
2730
+ (typeof artifact.file === 'string' && artifact.file) ||
2731
+ '';
2732
+
2733
+ const displayName =
2734
+ (typeof artifact.name === 'string' && artifact.name) ||
2735
+ (typeof artifact.fileName === 'string' && artifact.fileName) ||
2736
+ filePath ||
2737
+ 'attachment';
2738
+
2739
+ return { filePath, displayName };
834
2740
  }
835
2741
 
836
- const content = body.querySelector('.testitem__content'),
837
- content_error = content.querySelector('div[type="steps"]').querySelector('span'),
838
- content_status = content.querySelector('div[type="status"]').querySelector('span'),
839
- content_message = content.querySelector('div[type="message"]').querySelector('span'),
840
- content_files = content.querySelector('div[type="files"]').querySelector('span');
2742
+ const s = String(artifact);
2743
+ return { filePath: s, displayName: s };
2744
+ }
841
2745
 
842
- content_error.innerHTML = steps;
843
- content_status.innerHTML = category.toUpperCase();
844
- content_message.innerHTML = message;
845
- //if no file - empty message, else - files
846
- if (files.includes("This test has no files")){
847
- content_files.innerHTML = files
848
- folderSvg.classList.add('d-none');
2746
+ function safeBasename(p) {
2747
+ return String(p || '')
2748
+ .split(/[\\/]/)
2749
+ .pop();
849
2750
  }
850
- else {
851
- addFilesToContent(content_files, files);
2751
+
2752
+ function createAttachmentItems(artifacts) {
2753
+ if (!Array.isArray(artifacts) || artifacts.length === 0) return '';
2754
+
2755
+ return artifacts
2756
+ .map((artifact) => {
2757
+ const { filePath, displayName } = normalizeArtifact(artifact);
2758
+ const fileName = safeBasename(displayName || filePath) || 'attachment';
2759
+
2760
+ if (!filePath) {
2761
+ return `
2762
+ <div class='attachment-item'>
2763
+ <div class='attachment-file'>
2764
+ <i class='fas fa-file' style='font-size: 48px; color: var(--gray-400);'></i>
2765
+ <div style='margin-top: 10px; font-size: 14px; color: var(--gray-600);'>${fileName}</div>
2766
+ <div style='margin-top: 6px; font-size: 12px; color: var(--gray-500);'>inline attachment</div>
2767
+ </div>
2768
+ </div>
2769
+ `;
2770
+ }
2771
+
2772
+ const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(filePath) || /\.(jpg|jpeg|png|gif|webp)$/i.test(fileName);
2773
+
2774
+ if (isImage) {
2775
+ return `
2776
+ <div class='attachment-item' onclick='openAttachment("${filePath}")''>
2777
+ <img src='${filePath}' alt='${fileName}' class='attachment-image'>
2778
+ <div class='attachment-name'>${fileName}</div>
2779
+ </div>
2780
+ `;
2781
+ }
2782
+
2783
+ const icon = getFileIcon(filePath);
2784
+ return `
2785
+ <div class='attachment-item' onclick='openAttachment("${filePath}")''>
2786
+ <div class='attachment-file'>
2787
+ <i class='fas fa-${icon}' style='font-size: 48px; color: var(--gray-400);'></i>
2788
+ <div style='margin-top: 10px; font-size: 14px; color: var(--gray-600);'>${fileName}</div>
2789
+ </div>
2790
+ </div>
2791
+ `;
2792
+ })
2793
+ .join('');
2794
+ }
2795
+
2796
+ function getFileIcon(filePath) {
2797
+ if (typeof filePath !== 'string') return 'file';
2798
+ const ext = (filePath.split('.').pop() || '').toLowerCase();
2799
+ const icons = {
2800
+ pdf: 'file-pdf',
2801
+ zip: 'file-archive',
2802
+ rar: 'file-archive',
2803
+ mp4: 'file-video',
2804
+ avi: 'file-video',
2805
+ mov: 'file-video',
2806
+ webm: 'file-video',
2807
+ mp3: 'file-audio',
2808
+ wav: 'file-audio',
2809
+ txt: 'file-alt',
2810
+ log: 'file-alt',
2811
+ json: 'file-code',
2812
+ xml: 'file-code',
2813
+ csv: 'file-excel',
2814
+ xlsx: 'file-excel',
2815
+ doc: 'file-word',
2816
+ docx: 'file-word',
2817
+ };
2818
+ return icons[ext] || 'file';
2819
+ }
2820
+
2821
+ function toggleTest(header) {
2822
+ const testItem = header.closest('.test-item');
2823
+ testItem.classList.toggle('expanded');
2824
+ }
2825
+
2826
+ function showTestTab(tab, tabName) {
2827
+ const testContent = tab.closest('.test-content');
2828
+ const allTabs = testContent.querySelectorAll('.test-tab');
2829
+ const allContents = testContent.querySelectorAll('.test-tab-content');
2830
+
2831
+ allTabs.forEach(t => t.classList.remove('active'));
2832
+ allContents.forEach(c => c.classList.remove('active'));
2833
+
2834
+ tab.classList.add('active');
2835
+ testContent.querySelector(`[data-tab=\"${tabName}\"]`).classList.add('active');
2836
+
2837
+ if (tabName === 'traces') {
2838
+ const testItem = tab.closest('.test-item');
2839
+ const testIndex = testItem.dataset.testIndex;
2840
+ loadTraceForTest(testIndex);
852
2841
  }
853
2842
  }
854
2843
 
855
- function addFilesToContent(content_files, files) {
856
- const filesList = document.createElement('div');
2844
+ async function loadTraceForTest(testIndex) {
2845
+ const testItem = document.querySelector(`.test-item[data-test-index="${testIndex}"]`);
2846
+ if (!testItem) return;
857
2847
 
858
- filesList.classList.add('d-flex', 'flex-wrap', 'flex-column');
859
- content_files.innerHTML = "";
2848
+ let traces = window.testTraces && window.testTraces[testIndex];
2849
+ if (!traces || !Array.isArray(traces) || traces.length === 0) return;
860
2850
 
861
- if (Array.isArray(files) && files.length > 0) {
862
- for (let i = 0; i < files.length; i += 2) {
863
- const filePairContainer = document.createElement('div');
864
- filePairContainer.classList.add('d-flex', 'mb-3', 'justify-content-around');
2851
+ traces = traces.filter(t => {
2852
+ if (typeof t === 'string' && t.trim().length > 0) return true;
2853
+ if (t && t.dataUrl && typeof t.dataUrl === 'string' && t.dataUrl.trim().length > 0) return true;
2854
+ return false;
2855
+ });
2856
+ if (traces.length === 0) return;
865
2857
 
866
- for (let j = i; j < i + 2 && j < files.length; j++) {
867
- const file = files[j];
868
- const fileItemContainer = createFileItemContainer(file);
869
- filePairContainer.appendChild(fileItemContainer);
870
- }
2858
+ const viewerDiv = testItem.querySelector('.trace-viewer-container');
2859
+ if (!viewerDiv) return;
2860
+
2861
+ if (viewerDiv.dataset.loaded === 'true') return;
2862
+
2863
+ let html = `
2864
+ <div class="traces-instruction">
2865
+ <div class="instruction-header">
2866
+ <i class="fas fa-info-circle"></i>
2867
+ <span>How to view trace files</span>
2868
+ </div>
2869
+ <div class="instruction-content">
2870
+ <ol>
2871
+ <li>Download the trace file using the button below</li>
2872
+ <li>Extract the .zip archive if needed</li>
2873
+ <li><strong>Playwright traces:</strong><br>
2874
+ <code>npx playwright show-trace trace.zip</code><br>
2875
+ Or VS Code extension: "Playwright Test for VS Code"
2876
+ </li>
2877
+ <li><strong>TestCafe traces:</strong><br>
2878
+ <code>testcafe show-trace trace.zip</code>
2879
+ </li>
2880
+ <li><strong>Other traces:</strong><br>
2881
+ Check your testing framework documentation
2882
+ </li>
2883
+ </ol>
2884
+ </div>
2885
+ </div>
2886
+ `;
2887
+
2888
+ html += '<div class="traces-list">';
871
2889
 
872
- filesList.appendChild(filePairContainer);
2890
+ for (let idx = 0; idx < traces.length; idx++) {
2891
+ const trace = traces[idx];
2892
+ const traceDataUrl = typeof trace === 'string' ? trace : trace.dataUrl;
2893
+ let traceName = 'trace.zip';
2894
+
2895
+ if (typeof trace === 'object' && trace.name) {
2896
+ traceName = trace.name;
2897
+ } else if (idx === 0) {
2898
+ traceName = 'test-failed-trace.zip';
2899
+ } else {
2900
+ traceName = `trace-${idx + 1}.zip`;
873
2901
  }
874
2902
 
875
- content_files.appendChild(filesList);
876
- }
877
- }
878
-
879
- function createFileItemContainer(file) {
880
- let fileIcon;
881
-
882
- const filepath = file?.path || file;
883
- const fileName = createFileName(file);
884
- const fileItemContainer = document.createElement('div');
885
- const fileExtension = filepath.split('.').pop().toLowerCase();
886
-
887
- fileItemContainer.classList.add('d-flex', 'flex-column', 'align-items-center', 'mr-3');
888
-
889
- switch (fileExtension) {
890
- case 'jpg':
891
- case 'jpeg':
892
- case 'png':
893
- case 'gif':
894
- fileIcon = createImagePreview(file);
895
- break;
896
- case 'zip':
897
- const svgZipCode =
898
- `<svg width="95" height="94" viewBox="0 0 95 94" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
899
- <rect x="0.5" width="94" height="94" fill="url(#pattern0)"/>
900
- <defs>
901
- <pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
902
- <use xlink:href="#image0_212_109" transform="scale(0.01)"/>
903
- </pattern>
904
- <image id="image0_212_109" width="100" height="100" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAABwdJREFUeAHtnEmIJFUQhntEEVTwoCAouODNDWREEFzK7ow/XrW26yAi4kFwBcGLCKLQBxEPoh4GRLyoBwWvA6LixYMXQbypILiBOuLOqKOjzuhrOseXQS6RVS8jqpocaCpfvog/vvijqqa6qypXVsZ/owOjA6MDowOjA8YOMHBkyX4+2JhMTjW2ya7ckg2jvPN8SESn27lkWGlJB3IERfHxtaurZxhaZVNqWQcSuUH0GYBzbJwyqiIHYlS2dxnJmay/mE6n5/YWXNSEpLGt5+dl4Uy5QfT1dDo9b1HZe3GljcXjXsmGwZKzZr0fwAWGSMOUko0NU2V+VclJRC/Lcwx8G0K4aP5qjgqyKUeU1tI1nLuYea88D6IfmfnSVrFF3pQNLSprA2ccyrNyj4GfQwiXLWovrVyymdZgx802TgYel/sM/MrMVzsiz1ZaNjKbyvBZXZwB2JQxIPqNmYvh6TJWkE1klM4qpeFk5odlHIj+CETXZYUZUkw2MGStebS1nAF4SMaC6M9AdOM89c1yJbxZ4Z6F+nACuI+BwyLnEIA9PcvahwvopfnFsMspZr4LRP+k/YHob2a+oyvXdT8FjseuMC3FZ+Fk5tsY+CvNjUMBcGdLKd+tFHanDSQ6C+BWOZT4dAbgAV/nG6rv9IFsD2UPA4dEr4eZ+cEGW/xOC8gd9ZSVusrM1zBwUPYbgEfTOPdjCegO1ACQg5OZA4DfpRYTPdlQ1v60hLMn0FXMxTkluoqBA1JvYYYiwXT22Efl5AxFcTmIfpGaTPTUysrKLvvukooSKtlaqMPcnER0CRP9IHWJ6LnNzc1j3JqXQG4gHYWH4JwWxcXM/J3UZuYX3IYiYTp8cdseijO+Fx/fk5f6zPzKZDI51rxhCWIOoCwoOS3WAF5V4uULk43lU86rJDmt1nm7UKjJxhQpLiGS02pt3qxszBxAWVByWq2VePnCZGP5lJdTyd0Pd4AFm5u7H+4A40CqDowDWTA/xoGMA6k6sGAr9zuoO8A4kKoD40AWzI9xIONAqg4s2Mr9DuoOMA6k6sCyDCReLCCEMAlEtzPzvfGD1QDuDiHcFEI4u9rV7Ct3P3IBSJ2utbRMxst37OS+XKMo3gdwg9Ttu5a6ffPnjs8FIHW61hJcxvcdSJkP4Amp3Wdd6pS3fXKzxJaFy9tZRct85e0hWUfmdQ6E+Q1mfo2JvpS5zHy91NeupZY2L1tcLoD4TaWmnwA8XanDvFc2UNn/70PfXQMp8+P73vLLn/Hpq9zveys5+ubPHT80wJZhRB8ldQ6sra2dJsGT/a0LGGgHEnWm0+nxAL5JNWa9OE2qEY8l5+DroQHiK6G0RvwuYF1Tacy2EZUPrNXsV2QC8HwaE4jWKgHKRaqxzaHMzBQ2JMDGxsYJDHyV1Ng/mUxOqkNPYspLfPQaCBM9VtFgvrmuTte5isZOe4Qw0SNpgwG4v8mQNC4ey4901uxXpBh4Jo0BsFoJUC5SjW0OZWamsKEA1tbWTmHgp6P6RJ/s3r37uCbso3HbV7jrO5AAvJlqrK+vn9VUq+18qhGP22IH2RsKIH5wuaLNfEtbA5XYno8QZj4//ZZUvLhZW622vRqOtvD8e0MAFEVxZvrlGCJ6T97jZSc1HJr/Q3bFpyYi+jzND0T3SH3tOtWJx9q8bHFDAATgxYqu4hIXlXjFI4SBTxn4XuYx89vyJXMfs6Ren9wssbkB1oviwq2vH/9/tdPXNaA1HK2PEBm/tS6KfQBO1NRripG6TXGDnc8NwEWxr9SM3xPXXr+qzClv5VNceb7mNn7t+Z1cV2qQ+oMZ3yScEwDAlULvpaa68rzI63zZy8xXxEdjURQnS6151jUc88j1z80JwETvJnoH+7z0TPLKXwwrzXTtV4LnWFjVaUTMBRCfMipaRG81/bGx7pJJldyaVzdd+40N9tywqtOIlQuAq39A3LqXS+10LYHSvXjcd1/Gz7ru4phVV52XC0DqdK0loIzvuy/jZ113ccyqq87LBSB1utYSUMb33Zfxs667OGbVVee5A6hJbQLd/XAHsPFZXcXdD3cAtVU2ge5+uAPY+Kyu4u6HO4DaKptAdz/cAWx8Vldx98MdQG2VTaC7H+4ANj6rq7j74Q6gtsom0N0PdwAbn9VV3P1wB1BbZRPo7oc7gI3P6irufrgDqK2yCXT3wx3Axmd1FXc/3AHUVtkEuvvhDmDjs7qKux/uAGqrbALd/XAHsPFZXcXdD3cAtVU2ge5+uAPY+Kyu4u6HO4DaKptAdz/cAWx8Vldx98MdQG2VTaC7H+4ANj6rq7j74Q6gtsom0N0PdwAbn9VV3P2QAOMalQ+KqyeZK3AcQHUA0o9cPqt1JMC4rg5IbWSuwHEA1QFIP3L5POqMDowOjA6MDowOaB34F113oQg53J+KAAAAAElFTkSuQmCC"/>
905
- </defs>
906
- </svg>`;
907
- fileIcon = createFileIcon(file,svgZipCode);
908
- break;
909
- case 'mp4':
910
- case 'avi':
911
- case 'mov':
912
- case 'webm':
913
- const svgVideoCode =
914
- `<svg width="95" height="95" viewBox="0 0 95 95" fill="none" xmlns="http://www.w3.org/2000/svg">
915
- <g clip-path="url(#clip0_212_104)">
916
- <path d="M35.75 27.4702V67.5298L65.125 47.5L35.75 27.4702ZM39.6667 34.8844L58.1729 47.5L39.6667 60.1156V34.8844ZM4.41666 12.25V82.75H90.5833V12.25H4.41666ZM86.6667 78.8333H8.33333V16.1667H86.6667V78.8333Z" fill="#474646"/>
917
- </g>
918
- <defs>
919
- <clipPath id="clip0_212_104">
920
- <rect width="94" height="94" fill="white" transform="translate(0.5 0.5)"/>
921
- </clipPath>
922
- </defs>
923
- </svg>`;
924
- fileIcon = createFileIcon(file, svgVideoCode);
925
- break;
926
- default:
927
- const svgFileCode =
928
- `<svg width="95" height="94" viewBox="0 0 95 94" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
929
- <rect x="0.5" width="94" height="94" fill="url(#pattern0)"/>
930
- <defs>
931
- <pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
932
- <use xlink:href="#image0_212_112" transform="scale(0.01)"/>
933
- </pattern>
934
- <image id="image0_212_112" width="100" height="100" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAABtNJREFUeAHtnE2IHEUUxzeiHhQUUcQPPKjgQfxkRRQV10y9/+vZdQUhC+JBQYngQRBB9OBhQTEiigqCgh78AL0IHuLJPXgRJAcRRBYvEo0gBvE7Go0xqzW7NVQ/ume6a7vq9SwVGLrr4733r9+/M8vMdNfcXP6XCWQCmUAmkAkkJsDAxoy9PlteWDgnMaZ05WbMDHfxrBPRBekoJaw0o4ZswJgvb9+9+8KEqNKUmlVDrG4QHQRwcRpSiapIQxKVbV1G6vTa3wyHw0tbJ+xrgLew0fvzrOj0dYPou+FweHlftbfS5S/MnrcKTjhZ6qxofw/gioSS4pSSC4tTZftZpU4iekv2MXC4KIqrtl9NMYNclKKUiaUrdO5i5pdlP4h+YubrJybr86BcUF+11ui0prwoxxj4pSiKG/u6lom65GImTlYcnKSTgafkOANHmPk2RclhpeVCwrLEj5qmswBW5RwQ/cHMJr66DivIRXSYutNUTXQy82NyHoj+Koju6FRMzGRyATFrbSd3U50F8KicC6K/C6I7t1M/WawUn6xwy0JtdAJ4kIETIuYYgD0ty6afLkTPzAfDaaSYeS+I/vXXB6LjzHzPtFjVcV+wPVcVM6F4iE5mvpuBf/xYawqA+yaU0h3yxe40QyxZAHdJU+zbGYCHdMnXVN/phmyZsoeBY2KtJ5j54Roset1C5I56y/KpMvMSA0flegvgCX+e+rkUqC6oRkAXOpm5APCnzMVEz9SUTd8txaVX0KxiVzqHRLcy8LvM1xtTpLBmeNLP6lJnYczNIPpV5mSi5+bm5nalX51XUYryhnp12rVOIrqOiX6UeYnoldXV1ZPUFi8FqQmZUjiGzqEx1zLzDzI3M7+mZooUM4WL2nAsnfa3ePubvMzPzO8sLCycnHzBUkhyAQ0LSp0p2gDebSivu2lyYd1l7jaT1Jmq3e0qGmSTC2sQojJF6kzVTr5YubDkAhoWlDpTtRvK626aXFh3mWczkzoPdQE9802dh7qAbEiZQDakZzyyIdmQMoGetdQvUHUB2ZAygWxIz3hkQ7IhZQI9a6lfoOoCsiFlAtmQnvHIhuxAQwCcWwBfNX1ZBGzMfn8+Ed3k0Nhf6kD0hRsH8In9SdXvc2N1RwAXuXxtjuoXaBcC7DYXMs+ktgUE4HEx5wUHjplvKY0xPz+KsQ/gNNybZTAYXOLytTnK/G1iO5nbhYAQQ+y2GKO70bcAE9HX7hYcAE+WdDFfbRc7eiIqGzLd98FgcDaAtfGL6GAJKLA+HgPWXEYm+tCfZ+8CsWNMdMD1w5hP3fz/b935wOVhYN3NscetbTbGGpj5fBfX5ujntOdtYjuZG0NAAezz8wJ4oEqsfVbDn2fvszXGnOn/z6m7S93mFLH7qmq07fNz2vO28dueH0NAU0MAnA5jfnMaCuBj+zyga9tH0er2xsqGtLC+qSE2ZQG84RlwnIE3XZuB9+rKZkPqyFT0tzKEaOAZYP8WWFNGG+EAWK5IP+rKhtSRqehvY4j9fMFEh5wJ3vHw/Pz8KRXpsyF1UOr62xhicwB42jNicxu/zTvR60rYmPxHvZaOGAgw5FlpSEH0iEhbamZDSjgmN9oYsvWVyLfSEPs5Y1KVbMgkOmKsjSFEhAozRm9bQ2NuEKnHzWzIGMX0kzaGAHh7bIgx+/3PJQBer6uWDakjU9Hf1BD7wdB/3m+0JYYx748NAo4Mh8MzKkrkP+pVUOr6Whhyrwd/Y3EwuKwA7vf77BYZVXXy/5AqKjV9LQxZG8MnOmTTLS4unlfao4ToQFWZbEgVlZq+JobIr98BvOrSEdFHY6OADSK6xo25YzbEkWhwbGKI3GzM/5pEwgbwkiwr59iack5I278Q7HlIjm3FxBDQxBAAn3u1jy4vL5/mFrK0tHSW/abXG//ZH7fzsiGOVoPjojFXFkQr7iV/Tl1ZWTnVjdkjAJJp7ecTf479EcyfY3P647amPx567l0EOjt9qwsIJRcpTp2HuoBIYEPTqvNQFxBKLlKcOg91AZHAhqZV56EuIJRcpDh1HuoCIoENTavOQ11AKLlIceo81AVEAhuaVp2HuoBQcpHi1HmoC4gENjStOg91AaHkIsWp81AXEAlsaFp1HuoCQslFilPnoS4gEtjQtOo81AWEkosUp85DXUAksKFp1XmoCwglFylOnYe6gEhgQ9Oq81AXEEouUpw6D3UBkcCGplXnoS4glFykOHUe6gIigQ1Nq85DXUAouUhx6jzUBUQCG5pWnYcUkNubTwA7DqHGBse5wvlYNsLxCAYbGugK52M2ZPPx54a7+2hdMKEXeo7LBDKBTCATyARCCfwHp/izMKqRVXgAAAAASUVORK5CYII="/>
935
- </defs>
936
- </svg>`;
937
- fileIcon = createFileIcon(file, svgFileCode);
2903
+ html += `
2904
+ <div class="trace-download-item">
2905
+ <div class="trace-info">
2906
+ <i class="fas fa-file-archive"></i>
2907
+ <span class="trace-name">${traceName}</span>
2908
+ </div>
2909
+ <button class="trace-download-btn" onclick='downloadTrace("${testIndex}", ${idx})'>
2910
+ <i class="fas fa-download"></i>
2911
+ Download Trace
2912
+ </button>
2913
+ </div>
2914
+ `;
938
2915
  }
939
-
940
- fileItemContainer.appendChild(fileIcon);
941
- fileItemContainer.appendChild(fileName);
942
2916
 
943
- return fileItemContainer;
2917
+ html += '</div>';
2918
+ viewerDiv.innerHTML = html;
2919
+ viewerDiv.dataset.loaded = 'true';
944
2920
  }
945
2921
 
946
- function createImagePreview(file) {
947
- //TODO: consider using "../${file}", maybe we should get full paths when generating artifacts in Mocha!
948
- const filepath = file?.path || `../${file}`;
949
- const imagePreview = document.createElement('img');
950
- // component styles
951
- imagePreview.src = filepath;
952
- imagePreview.alt = 'Image Preview';
953
- imagePreview.style.maxWidth = '200px';
954
- imagePreview.style.height = '150px';
955
- imagePreview.style.cursor = 'pointer';
2922
+ function downloadTrace(testIndex, traceIdx) {
2923
+ const traces = window.testTraces && window.testTraces[testIndex];
2924
+ if (!traces || !Array.isArray(traces) || traceIdx >= traces.length) return;
956
2925
 
957
- imagePreview.addEventListener('click', () => {
958
- window.open(filepath, '_blank');
959
- });
2926
+ const trace = traces[traceIdx];
2927
+ const traceDataUrl = typeof trace === 'string' ? trace : trace.dataUrl;
2928
+ const traceName = typeof trace === 'object' && trace.name ? trace.name :
2929
+ (traceIdx === 0 ? 'test-failed-trace.zip' : `trace-${traceIdx + 1}.zip`);
960
2930
 
961
- return imagePreview;
2931
+ const link = document.createElement('a');
2932
+ link.href = traceDataUrl;
2933
+ link.download = traceName;
2934
+ document.body.appendChild(link);
2935
+ link.click();
2936
+ document.body.removeChild(link);
962
2937
  }
963
2938
 
964
- function createFileIcon(file, svg) {
965
- //TODO: consider using "../${file}", maybe we should get full paths when generating artifacts in Mocha!
966
- const filepath = file?.path || `../${file}`;
967
- const fileIcon = document.createElement('div');
968
- // component styles
969
- fileIcon.innerHTML = svg;
970
- fileIcon.style.width = '150px';
971
- fileIcon.style.height = '150px';
972
- fileIcon.style.cursor = 'pointer';
973
- fileIcon.style.textAlign = 'center';
2939
+ function openAttachment(filePath) {
2940
+ const url = String(filePath || '').trim();
974
2941
 
975
- fileIcon.addEventListener('click', () => {
976
- window.open(filepath, '_blank');
977
- });
2942
+ const ok = /^(https?:\/\/|file:\/\/|\/|\.\/|\.\.\/)/i.test(url);
2943
+ if (!ok) return;
978
2944
 
979
- return fileIcon;
2945
+ const w = window.open(url, '_blank', 'noopener,noreferrer');
2946
+ if (w) w.opener = null;
980
2947
  }
981
2948
 
982
- function createFileName(file) {
983
- //TODO: consider using "../${file}", maybe we should get full paths when generating artifacts in Mocha!
984
- const filepath = file?.path || `../${file}`;
985
- const fileName = document.createElement('div');
986
- // component styles
987
- fileName.style.fontSize = '14px';
988
- fileName.style.marginTop = '10px';
989
- fileName.style.cursor = 'pointer';
2949
+ function groupTestsBySuite(tests) {
2950
+ const groups = {};
2951
+
2952
+ tests.forEach(test => {
2953
+ const suite = test.suite_title || 'Unknown Suite';
2954
+ if (!groups[suite]) {
2955
+ groups[suite] = [];
2956
+ }
2957
+ groups[suite].push(test);
2958
+ });
2959
+
2960
+ return Object.entries(groups).map(([name, tests]) => ({
2961
+ name,
2962
+ type: 'suite',
2963
+ tests
2964
+ }));
2965
+ }
990
2966
 
991
- fileName.textContent = filepath.split('/').pop();
2967
+ function groupTestsByFile(tests) {
2968
+ const groups = {};
992
2969
 
993
- fileName.addEventListener('click', () => {
994
- window.open(filepath, '_blank');
2970
+ tests.forEach(test => {
2971
+ const file = test.file || 'Unknown File';
2972
+ if (!groups[file]) {
2973
+ groups[file] = [];
2974
+ }
2975
+ groups[file].push(test);
995
2976
  });
996
2977
 
997
- return fileName;
2978
+ return Object.entries(groups).map(([name, tests]) => ({
2979
+ name,
2980
+ type: 'file',
2981
+ tests
2982
+ }));
998
2983
  }
999
2984
 
1000
- function addCollapseExpandListener(testitem__top) {
1001
- testitem__top.addEventListener("click", function () {
1002
- collapse_expand(testitem__top);
2985
+ function filterTests() {
2986
+ const filteredTests = allTests.filter(test => {
2987
+ const matchesFilter = currentFilter === 'all' || test.status.toLowerCase() === currentFilter;
2988
+ const matchesSearch = searchTerm === '' ||
2989
+ (test.title && test.title.toLowerCase().includes(searchTerm.toLowerCase())) ||
2990
+ (test.suite_title && test.suite_title.toLowerCase().includes(searchTerm.toLowerCase()));
2991
+
2992
+ return matchesFilter && matchesSearch;
1003
2993
  });
2994
+
2995
+ return filteredTests;
1004
2996
  }
1005
2997
 
1006
- function collapse_expand(top) {
1007
- const block = top.closest('.testitem');
1008
- const icon = top.querySelector('.testitem__icon');
1009
- const testitem__ico_right = icon.querySelector('.testitem__ico_right');
1010
- const testitem__ico_down = icon.querySelector('.testitem__ico_down');
1011
- const body = block.querySelector('.testitem__body');
2998
+ function renderTests() {
2999
+ const container = document.getElementById('testsContainer');
3000
+ const filteredTests = filterTests();
1012
3001
 
1013
- if (body.classList.contains('d-none')) {
1014
- body.classList.remove('d-none');
1015
- testitem__ico_right.classList.add('d-none');
1016
- testitem__ico_down.classList.remove('d-none');
1017
- }
1018
- else {
1019
- body.classList.add('d-none');
1020
- testitem__ico_down.classList.add('d-none');
1021
- testitem__ico_right.classList.remove('d-none');
3002
+ if (groupingMode === 'suite') {
3003
+ const groups = groupTestsBySuite(filteredTests);
3004
+ renderGroupedTests(container, groups);
3005
+ return;
3006
+ } else if (groupingMode === 'file') {
3007
+ const groups = groupTestsByFile(filteredTests);
3008
+ renderGroupedTests(container, groups);
3009
+ return;
1022
3010
  }
1023
- }
1024
3011
 
1025
- function initializeMenu(clone) {
1026
- const menu = clone.querySelector('.testitem__menu');
1027
- const item = menu.querySelectorAll('.testitem__mitem');
3012
+ const startIndex = (currentPage - 1) * pageSize;
3013
+ const endIndex = startIndex + pageSize;
3014
+ const paginatedTests = filteredTests.slice(startIndex, endIndex);
1028
3015
 
1029
- for (let i = 0; i < item.length; i++) {
1030
- item[i].addEventListener('click', function () {
1031
- show_content(item[i])
1032
- })
3016
+ if (paginatedTests.length === 0) {
3017
+ container.innerHTML = `
3018
+ <div class='empty-state'>
3019
+ <div class='empty-icon'>
3020
+ <i class='fas fa-search'></i>
3021
+ </div>
3022
+ <div class='empty-title'>No Tests Found</div>
3023
+ <div class='empty-description'>No tests match your current filter criteria.</div>
3024
+ </div>
3025
+ `;
3026
+ } else {
3027
+ container.innerHTML = '';
3028
+ paginatedTests.forEach((test, index) => {
3029
+ const testItem = createTestItem(test, startIndex + index);
3030
+ container.appendChild(testItem);
3031
+ });
1033
3032
  }
3033
+
3034
+ renderPagination(filteredTests.length);
3035
+ setTimeout(populateMetadataFields, 50);
1034
3036
  }
1035
3037
 
1036
- function show_content(elem) {
1037
- removeAddActive(elem);
3038
+ function populateMetadataFields() {
3039
+ const metaGrids = document.querySelectorAll('[data-test-meta]');
3040
+ metaGrids.forEach(grid => {
3041
+ const testItem = grid.closest('.test-item');
3042
+ if (!testItem) return;
3043
+
3044
+ const testIndex = parseInt(testItem.getAttribute('data-test-index'));
3045
+ const test = allTests[testIndex];
3046
+ if (!test || !test.meta) return;
3047
+
3048
+ Object.entries(test.meta).forEach(([key, value]) => {
3049
+ const metaItem = document.createElement('div');
3050
+ metaItem.className = 'metadata-item meta-item';
3051
+ metaItem.setAttribute('data-meta-key', key);
3052
+
3053
+ const keyDiv = document.createElement('div');
3054
+ keyDiv.className = 'metadata-key';
3055
+ keyDiv.textContent = key;
3056
+
3057
+ const valueDiv = document.createElement('div');
3058
+ valueDiv.className = 'metadata-value';
3059
+ valueDiv.textContent = typeof value === 'object' ? JSON.stringify(value, null, 2) : value;
1038
3060
 
1039
- const body = elem.closest('.testitem__body');
1040
- const content = body.querySelector('.testitem__content');
1041
- const blocks = content.querySelectorAll('.testitem__case');
3061
+ metaItem.appendChild(keyDiv);
3062
+ metaItem.appendChild(valueDiv);
3063
+ grid.appendChild(metaItem);
3064
+ });
3065
+ });
3066
+
3067
+ setTimeout(applyMetaVisibility, 10);
3068
+ }
1042
3069
 
1043
- for (let i = 0; i < blocks.length; i++) {
1044
- blocks[i].classList.add('d-none');
3070
+ function renderGroupedTests(container, groups) {
3071
+ if (groups.length === 0) {
3072
+ container.innerHTML = `
3073
+ <div class='empty-state'>
3074
+ <div class='empty-icon'>
3075
+ <i class='fas fa-search'></i>
3076
+ </div>
3077
+ <div class='empty-title'>No Tests Found</div>
3078
+ <div class='empty-description'>No tests match your current filter criteria.</div>
3079
+ </div>
3080
+ `;
3081
+ return;
1045
3082
  }
1046
3083
 
1047
- const type = elem.getAttribute('type');
1048
- const show_elem = content.querySelector('div[type="' + type + '"]');
3084
+ container.innerHTML = '';
3085
+
3086
+ const startIndex = (currentPage - 1) * pageSize;
3087
+ const endIndex = startIndex + pageSize;
3088
+ const paginatedGroups = groups.slice(startIndex, endIndex);
3089
+
3090
+ paginatedGroups.forEach(group => {
3091
+ const groupItem = createTestGroup(group);
3092
+ container.appendChild(groupItem);
3093
+ });
1049
3094
 
1050
- show_elem.classList.remove('d-none');
3095
+ renderPagination(groups.length);
3096
+ setTimeout(populateMetadataFields, 50);
1051
3097
  }
1052
3098
 
1053
- function removeAddActive(elem) {
1054
- const menu = elem.closest('.testitem__menu');
1055
- const item = menu.querySelectorAll('.testitem__mitem');
3099
+ function createTestGroup(group) {
3100
+ const groupElement = document.createElement('div');
3101
+ groupElement.className = 'test-group';
1056
3102
 
1057
- for (let i = 0; i < item.length; i++) {
1058
- item[i].classList.remove('testitem__mitem_active');
1059
- }
1060
- elem.classList.add('testitem__mitem_active');
3103
+ const passedCount = group.tests.filter(t => t.status.toLowerCase() === 'passed').length;
3104
+ const failedCount = group.tests.filter(t => t.status.toLowerCase() === 'failed').length;
3105
+ const skippedCount = group.tests.filter(t => t.status.toLowerCase() === 'skipped').length;
3106
+ const todoCount = group.tests.filter(t => t.status.toLowerCase() === 'todo').length;
3107
+
3108
+ const groupIcon = group.type === 'suite' ? 'layer-group' : 'file-code';
3109
+
3110
+ const header = document.createElement('div');
3111
+ header.className = 'test-group-header';
3112
+ header.innerHTML = `
3113
+ <div class='test-group-title'>
3114
+ <i class='fas fa-${groupIcon}'></i>
3115
+ <div class='test-group-name'>${group.name}</div>
3116
+ <div class='test-group-count'>${group.tests.length} tests</div>
3117
+ </div>
3118
+ <div class='test-group-stats'>
3119
+ ${passedCount > 0 ? `<div class='test-group-stat' style='color: #10b981;'><i class='fas fa-check-circle'></i> ${passedCount}</div>` : ''}
3120
+ ${failedCount > 0 ? `<div class='test-group-stat' style='color: #ef4444;'><i class='fas fa-times-circle'></i> ${failedCount}</div>` : ''}
3121
+ ${skippedCount > 0 ? `<div class='test-group-stat' style='color: #f59e0b;'><i class='fas fa-forward'></i> ${skippedCount}</div>` : ''}
3122
+ ${todoCount > 0 ? `<div class='test-group-stat' style='color: #8b5cf6;'><i class='fas fa-clock'></i> ${todoCount}</div>` : ''}
3123
+ </div>
3124
+ <i class='fas fa-chevron-down test-group-expand'></i>
3125
+ `;
3126
+ header.onclick = () => toggleTestGroup(header);
3127
+ groupElement.appendChild(header);
3128
+
3129
+ const content = document.createElement('div');
3130
+ content.className = 'test-group-content';
3131
+
3132
+ group.tests.forEach((test, index) => {
3133
+ const testItem = createTestItem(test, index);
3134
+ content.appendChild(testItem);
3135
+ });
3136
+
3137
+ groupElement.appendChild(content);
3138
+
3139
+ return groupElement;
1061
3140
  }
1062
3141
 
1063
- // GET test data
1064
- const testEntries = {{{ pageDispleyElements tests }}};
1065
- const allEntries = testEntries['totalTests'];
3142
+ function toggleTestGroup(header) {
3143
+ const group = header.parentElement;
3144
+ group.classList.toggle('expanded');
3145
+ }
1066
3146
 
1067
- if (allEntries.length === 0) {
1068
- const searchEl = document.querySelector('.input-group');
1069
- const radioEl = document.querySelector('.menuTests');
3147
+ function renderPagination(totalItems) {
3148
+ const container = document.getElementById('paginationContainer');
3149
+ const totalPages = Math.ceil(totalItems / pageSize);
1070
3150
 
1071
- searchEl.classList.add('d-none');
1072
- radioEl.classList.add('d-none');
1073
- }
1074
- else {
1075
- function search(array, testEntries) {
1076
- const handleSearch = (input) => {
1077
- const value = input.value.trim().toLowerCase();
1078
- const select = document.querySelector('.form-select');
1079
- const pagination = document.querySelectorAll('nav');
3151
+ if (totalPages <= 1) {
3152
+ container.innerHTML = '';
3153
+ return;
3154
+ }
1080
3155
 
1081
- if (value) {
1082
- select.classList.add('d-none');
1083
- pagination.forEach(item => item.classList.add('d-none'));
1084
- }
1085
- else {
1086
- select.classList.remove('d-none');
1087
- pagination.forEach(item => item.classList.remove('d-none'));
3156
+ let paginationHTML = '<div class=\"pagination\">';
1088
3157
 
1089
- remove(array);
3158
+ paginationHTML += `
3159
+ <button class='pagination-btn' onclick='goToPage(${currentPage - 1})'
3160
+ ${currentPage === 1 ? 'disabled' : ''}>
3161
+ <i class='fas fa-chevron-left'></i>
3162
+ </button>
3163
+ `;
1090
3164
 
1091
- const paginationCount = select.value;
1092
- const category = select.getAttribute('status');
1093
-
1094
- if (testEntries[category][paginationCount][0]) {
1095
- add(testEntries[category][paginationCount][0]);
1096
- }
1097
- }
3165
+ const maxVisiblePages = 5;
3166
+ let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
3167
+ let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
1098
3168
 
1099
- remove(array);
1100
-
1101
- const filteredTests = array.filter(test => test.title.toLowerCase().includes(value) || test.suite_title.toLowerCase().includes(value));
1102
-
1103
- const existingNoDataElement = document.querySelector('.noResults');
1104
-
1105
- if (filteredTests.length === 0) {
1106
- if (!existingNoDataElement) {
1107
- const testElement = document.createElement('div');
1108
- testElement.classList.add('noData', 'noResults');
1109
- testElement.innerHTML =
1110
- `<div class='noData'>
1111
- <svg width="185" height="185" viewBox="0 0 370 370" fill="none" xmlns="http://www.w3.org/2000/svg">
1112
- <g clip-path="url(#clip0_215_375)">
1113
- <path opacity="0.1" d="M185 369.998C287.173 369.998 370.001 287.171 370.001 184.999C370.001 82.8268 287.173 0 185 0C82.8274 0 0 82.8268 0 184.999C0 287.171 82.8274 369.998 185 369.998Z" fill="#A9C7FF"/>
1114
- <path d="M104.96 129.507C105.629 128.757 106.439 128.147 107.344 127.71C108.249 127.273 109.231 127.019 110.234 126.961C111.237 126.904 112.242 127.045 113.191 127.376C114.14 127.707 115.014 128.221 115.764 128.89L140.183 150.67L129.997 162.091L105.577 140.311C104.827 139.642 104.216 138.832 103.779 137.927C103.342 137.022 103.088 136.04 103.031 135.037C102.974 134.034 103.114 133.029 103.446 132.08C103.777 131.132 104.291 130.257 104.96 129.507Z" fill="white"/>
1115
- <path d="M130.056 163.157L105.074 140.874C104.25 140.139 103.579 139.249 103.099 138.255C102.619 137.261 102.34 136.182 102.277 135.079C102.214 133.977 102.368 132.873 102.732 131.831C103.096 130.788 103.661 129.828 104.396 129.004C105.131 128.18 106.021 127.508 107.015 127.028C108.009 126.548 109.088 126.269 110.191 126.206C111.293 126.143 112.397 126.298 113.439 126.661C114.482 127.025 115.443 127.59 116.267 128.325L141.251 150.608L130.056 163.157ZM110.677 127.703C110.544 127.703 110.409 127.707 110.275 127.714C108.913 127.792 107.605 128.273 106.516 129.094C105.427 129.916 104.607 131.042 104.158 132.329C103.709 133.617 103.652 135.01 103.994 136.33C104.336 137.65 105.062 138.84 106.08 139.747L129.936 161.024L139.117 150.731L115.261 129.454C114.002 128.325 112.371 127.701 110.68 127.703H110.677Z" fill="#474646"/>
1116
- <path d="M240 162.09L229.814 150.669L254.234 128.89C255.748 127.539 257.737 126.845 259.763 126.961C261.789 127.076 263.686 127.992 265.037 129.507C266.388 131.021 267.082 133.01 266.966 135.036C266.85 137.062 265.935 138.96 264.42 140.31L240 162.09Z" fill="white"/>
1117
- <path d="M239.942 163.157L228.749 150.608L253.733 128.325C255.397 126.841 257.583 126.079 259.809 126.206C262.035 126.333 264.12 127.34 265.604 129.004C267.088 130.668 267.851 132.853 267.723 135.079C267.596 137.306 266.59 139.39 264.926 140.874L239.942 163.157ZM230.883 150.73L240.064 161.023L263.92 139.747C264.596 139.144 265.146 138.414 265.54 137.599C265.934 136.783 266.163 135.898 266.214 134.994C266.266 134.09 266.139 133.185 265.841 132.329C265.543 131.474 265.079 130.686 264.476 130.011C263.873 129.335 263.143 128.784 262.328 128.391C261.512 127.997 260.627 127.768 259.723 127.716C258.819 127.664 257.913 127.791 257.058 128.09C256.203 128.388 255.415 128.852 254.739 129.454L230.883 150.73Z" fill="#474646"/>
1118
- <path d="M250.171 176.257C255.618 200.379 263.075 215.898 259.362 224.811C257.41 229.495 253.403 233.424 246.771 233.424C236.433 233.424 236.433 221.232 226.095 221.232C215.756 221.232 215.758 233.424 205.42 233.424C195.083 233.424 195.084 221.232 184.748 221.232C174.411 221.232 174.411 233.424 164.074 233.424C153.737 233.424 153.738 221.232 143.401 221.232C133.065 221.232 133.066 233.424 122.731 233.424C116.595 233.424 113.126 229.81 110.926 225.633C105.639 215.6 117.96 193.521 122.731 174.051C128.002 152.536 127.558 125.739 128.265 109.576C129.629 78.4083 153.554 53.0876 184.755 53.0876C215.955 53.0876 238.971 78.4613 241.244 109.576C242.469 126.351 245.184 154.176 250.171 176.257Z" fill="white"/>
1119
- <path d="M246.771 234.186C241.249 234.186 238.506 230.95 235.852 227.818C233.311 224.822 230.911 221.992 226.095 221.992C221.278 221.992 218.882 224.822 216.339 227.818C213.685 230.948 210.942 234.186 205.42 234.186C199.899 234.186 197.156 230.95 194.503 227.818C191.962 224.822 189.562 221.992 184.748 221.992C179.933 221.992 177.535 224.822 174.992 227.818C172.339 230.948 169.596 234.186 164.074 234.186C158.552 234.186 155.808 230.95 153.16 227.822C150.619 224.825 148.219 221.995 143.404 221.995C138.589 221.995 136.191 224.825 133.65 227.822C130.998 230.951 128.255 234.189 122.734 234.189C117.243 234.189 113.161 231.508 110.254 225.992C106.568 218.996 110.995 206.627 115.683 193.534C118.011 187.03 120.416 180.305 121.994 173.874C126.21 156.668 126.743 136.45 127.172 120.204C127.272 116.362 127.368 112.732 127.51 109.548C128.186 94.1198 134.32 79.7607 144.785 69.1146C149.986 63.7845 156.204 59.5527 163.071 56.6697C169.937 53.7867 177.313 52.3113 184.76 52.3308C192.159 52.3133 199.486 53.7933 206.298 56.6817C213.11 59.5701 219.267 63.8071 224.399 69.1377C234.622 79.6633 240.876 94.0076 242.01 109.526C243.102 124.489 245.712 153.033 250.92 176.094C252.686 183.916 254.663 190.825 256.407 196.921C260.048 209.651 262.679 218.848 260.069 225.109C258.652 228.498 255.046 234.186 246.771 234.186ZM226.095 220.47C231.617 220.47 234.361 223.705 237.014 226.837C239.556 229.834 241.956 232.664 246.771 232.664C254.162 232.664 257.391 227.561 258.658 224.519C261.062 218.751 258.489 209.769 254.935 197.338C253.188 191.222 251.203 184.292 249.428 176.428C244.198 153.268 241.58 124.639 240.484 109.635C238.161 77.833 214.203 53.8517 184.754 53.8517C154.874 53.8517 130.395 78.3441 129.024 109.612C128.885 112.782 128.789 116.406 128.686 120.241C128.278 135.699 127.72 156.869 123.465 174.234C121.871 180.742 119.45 187.505 117.109 194.046C112.538 206.815 108.22 218.877 111.594 225.28C114.212 230.248 117.853 232.662 122.725 232.662C127.54 232.662 129.938 229.832 132.48 226.836C135.132 223.706 137.875 220.468 143.396 220.468C148.916 220.468 151.661 223.704 154.313 226.836C156.854 229.832 159.253 232.662 164.068 232.662C168.884 232.662 171.284 229.832 173.824 226.836C176.478 223.706 179.221 220.468 184.742 220.468C190.263 220.468 193.007 223.704 195.659 226.836C198.2 229.832 200.599 232.662 205.415 232.662C210.23 232.662 212.63 229.832 215.171 226.836C217.83 223.708 220.573 220.47 226.095 220.47Z" fill="#474646"/>
1120
- <path d="M163.527 113.221C166.986 113.221 169.789 110.418 169.789 106.96C169.789 103.501 166.986 100.698 163.527 100.698C160.069 100.698 157.266 103.501 157.266 106.96C157.266 110.418 160.069 113.221 163.527 113.221Z" fill="#474646"/>
1121
- <path d="M206.465 113.221C209.923 113.221 212.726 110.418 212.726 106.96C212.726 103.501 209.923 100.698 206.465 100.698C203.006 100.698 200.203 103.501 200.203 106.96C200.203 110.418 203.006 113.221 206.465 113.221Z" fill="#474646"/>
1122
- <path d="M196.211 123.859C196.573 123.859 196.93 123.938 197.256 124.093C197.583 124.247 197.871 124.473 198.1 124.752C198.329 125.032 198.492 125.36 198.578 125.71C198.665 126.061 198.672 126.427 198.599 126.781C197.256 133.059 191.677 137.005 184.998 137.005C178.319 137.005 172.74 133.061 171.397 126.781C171.324 126.427 171.331 126.061 171.418 125.71C171.504 125.359 171.668 125.032 171.896 124.752C172.125 124.472 172.413 124.247 172.74 124.093C173.067 123.938 173.424 123.858 173.785 123.859H196.211Z" fill="white"/>
1123
- <path d="M184.997 137.767C177.802 137.767 172.037 133.416 170.652 126.94C170.553 126.479 170.559 126.002 170.67 125.543C170.78 125.085 170.992 124.657 171.29 124.292C171.59 123.919 171.97 123.618 172.402 123.412C172.834 123.206 173.306 123.099 173.785 123.1H196.212C196.691 123.099 197.164 123.206 197.595 123.412C198.027 123.618 198.407 123.919 198.707 124.292C199.005 124.657 199.217 125.085 199.327 125.543C199.438 126.002 199.444 126.479 199.345 126.94C197.958 133.416 192.192 137.767 184.997 137.767ZM173.784 124.621C173.532 124.621 173.284 124.677 173.057 124.786C172.83 124.894 172.631 125.052 172.473 125.248C172.318 125.437 172.208 125.659 172.151 125.897C172.093 126.135 172.09 126.382 172.141 126.622C173.371 132.377 178.537 136.243 184.996 136.243C191.455 136.243 196.621 132.377 197.852 126.622C197.903 126.382 197.899 126.135 197.842 125.897C197.784 125.659 197.674 125.437 197.519 125.248C197.361 125.052 197.162 124.894 196.935 124.786C196.708 124.677 196.46 124.621 196.209 124.621H173.784Z" fill="#474646"/>
1124
- <path d="M332.184 297.095C298.385 341.408 245.033 370 185.003 370C124.973 370 71.6211 341.408 37.8223 297.095C76.4052 282.579 128.135 273.704 185.003 273.704C241.872 273.704 293.598 282.579 332.184 297.095Z" fill="#A9C7FF"/>
1125
- <path d="M347.711 96.8905L252.766 96.4848C252.766 80.628 261.697 77.5062 271.192 74.931C280.687 72.3558 284.959 54.9269 301.458 53.9351C311.202 53.3491 316.312 55.8566 320.544 59.0855C320.549 59.0911 320.549 59.0967 320.555 59.0967C331.158 70.5047 340.281 83.2025 347.711 96.8905Z" fill="white"/>
1126
- <path d="M76.8801 214.712L2.43035 215.033C1.15867 207.26 0.386967 199.413 0.119995 191.541C16.4277 189.231 14.7598 176.27 34.2344 177.448C48.6825 178.316 52.4355 193.581 60.7471 195.835C69.0587 198.089 76.8801 200.83 76.8801 214.712Z" fill="white"/>
1127
- </g>
1128
- <defs>
1129
- <clipPath id="clip0_215_375">
1130
- <rect width="370" height="370" fill="white"/>
1131
- </clipPath>
1132
- </defs>
1133
- </svg>
1134
-
1135
- <div class='noDataText'>NO SEARCH RESULTS</div>
1136
- </div>`;
1137
- document.querySelector('.testWrapp').appendChild(testElement);
1138
- }
1139
- }
1140
- else {
1141
- const paginationCount = select.value;
1142
- const category = select.getAttribute('status');
1143
-
1144
- if (existingNoDataElement) {
1145
- existingNoDataElement.remove();
1146
- }
1147
-
1148
- if (testEntries[category][paginationCount][0]) {
1149
- add(filteredTests);
1150
- if (!value) {
1151
- remove(filteredTests);
1152
- add(testEntries[category][paginationCount][0]);
1153
- }
1154
- }
1155
- else {
1156
- if (value) {
1157
- add(filteredTests);
1158
- }
1159
- else {
1160
- if (!existingNoDataElement) {
1161
- const testElement = document.createElement('div');
1162
-
1163
- select.classList.add('d-none');
1164
- testElement.classList.add('noData', 'noResults');
1165
- testElement.innerHTML = 'NO RESULTS';
1166
- document.querySelector('.testWrapp').appendChild(testElement);
1167
- }
1168
- }
1169
- }
1170
- }
1171
- };
3169
+ if (endPage - startPage < maxVisiblePages - 1) {
3170
+ startPage = Math.max(1, endPage - maxVisiblePages + 1);
3171
+ }
1172
3172
 
1173
- const inputSearch = document.querySelector('.report .inputSearch');
1174
- inputSearch.addEventListener('input', () => handleSearch(inputSearch));
3173
+ for (let i = startPage; i <= endPage; i++) {
3174
+ paginationHTML += `
3175
+ <button class='pagination-btn ${i === currentPage ? 'active' : ''}'
3176
+ onclick='goToPage(${i})'>${i}</button>
3177
+ `;
1175
3178
  }
1176
3179
 
1177
- function radioMenuStart(){
1178
- function showBlockForCategory(input){
1179
- const searchEl = document.querySelector('.input-group'),
1180
- element = document.querySelector('.form-select')
1181
- let category = 'all';
3180
+ paginationHTML += `
3181
+ <button class='pagination-btn' onclick='goToPage(${currentPage + 1})'
3182
+ ${currentPage === totalPages ? 'disabled' : ''}>
3183
+ <i class='fas fa-chevron-right'></i>
3184
+ </button>
3185
+ `;
1182
3186
 
1183
- remove(testEntries[category][0][0]);
3187
+ paginationHTML += '</div>';
3188
+ container.innerHTML = paginationHTML;
3189
+ }
1184
3190
 
1185
- category = input.getAttribute('category');
1186
- element.setAttribute('status', category);
3191
+ function goToPage(page) {
3192
+ const filteredTests = filterTests();
3193
+ const totalPages = Math.ceil(filteredTests.length / pageSize);
1187
3194
 
1188
- const pagination = document.querySelectorAll('nav');
1189
- pagination.forEach(item => item.classList.add('d-none'));
3195
+ if (page < 1 || page > totalPages) return;
1190
3196
 
1191
- let inputEl = document.querySelector('.inputSearch');
1192
- inputEl.value = '';
1193
-
1194
- pagination.forEach(item => item.classList.remove('d-none'));
3197
+ currentPage = page;
3198
+ renderTests();
3199
+ }
1195
3200
 
1196
- displayPagination(testEntries,element.value,category);
3201
+ function updateCounts() {
3202
+ const by = (s) => allTests.filter(t => String(t.status || '').toLowerCase() === s).length;
1197
3203
 
1198
- if(testEntries[category][0][0]){
1199
- element.classList.remove('d-none');
1200
- const existingNoDataElement = document.querySelector('.noResults');
3204
+ const counts = {
3205
+ all: allTests.length,
3206
+ passed: by('passed'),
3207
+ failed: by('failed'),
3208
+ skipped: by('skipped'),
3209
+ todo: by('todo'),
3210
+ };
1201
3211
 
1202
- if (existingNoDataElement) {
1203
- existingNoDataElement.remove();
1204
- }
3212
+ document.getElementById('countAll').textContent = counts.all;
3213
+ document.getElementById('countPassed').textContent = counts.passed;
3214
+ document.getElementById('countFailed').textContent = counts.failed;
3215
+ document.getElementById('countSkipped').textContent = counts.skipped;
1205
3216
 
1206
- searchEl.classList.remove('d-none');
1207
- add(testEntries[category][element.value][0]);
1208
- }
1209
- else {
1210
- element.classList.add('d-none');
1211
- const existingNoDataElement = document.querySelector('.noResults');
1212
-
1213
- if (existingNoDataElement) {
1214
- existingNoDataElement.remove();
1215
- }
1216
-
1217
- const testElement = document.createElement('div');
1218
- testElement.classList.add('noData');
1219
- testElement.classList.add('noResults');
1220
- testElement.innerHTML = 'NO RESULTS';
1221
- searchEl.classList.add('d-none');
1222
-
1223
- const testWrapp = document.querySelector('.testWrapp');
1224
- testWrapp.appendChild(testElement);
1225
- }
1226
- }
3217
+ const todoEl = document.getElementById('countTodo');
3218
+ if (todoEl) todoEl.textContent = counts.todo;
3219
+
3220
+ const todoTab = document.querySelector('.filter-tab[data-filter="todo"]');
3221
+ if (todoTab) todoTab.style.display = counts.todo > 0 ? '' : 'none';
3222
+ }
1227
3223
 
1228
- const page = document.querySelector('.report');
1229
- const menuTests = page.querySelector('.menuTests');
1230
- const items = menuTests.querySelectorAll('input[name="groupTest"]');
3224
+ function updateTitleColor() {
3225
+ const by = (s) => allTests.filter(t => String(t.status || '').toLowerCase() === s).length;
3226
+ const failed = by('failed');
3227
+ const passed = by('passed');
3228
+ const skipped = by('skipped');
3229
+ const todo = by('todo');
1231
3230
 
1232
- for(let i=0; i<items.length; i++){
1233
- items[i].addEventListener('change',function(){
1234
- showBlockForCategory(items[i])
1235
- })
1236
- }
3231
+ const mainTitle = document.getElementById('mainTitle');
3232
+ if (!mainTitle) return;
3233
+
3234
+ mainTitle.classList.remove('status-passed', 'status-failed', 'status-skipped', 'status-todo');
3235
+
3236
+ if (failed > 0) {
3237
+ mainTitle.classList.add('status-failed');
3238
+ } else if (passed > 0) {
3239
+ mainTitle.classList.add('status-passed');
3240
+ } else if (skipped > 0) {
3241
+ mainTitle.classList.add('status-skipped');
3242
+ } else if (todo > 0) {
3243
+ mainTitle.classList.add('status-todo');
1237
3244
  }
3245
+ }
1238
3246
 
1239
- radioMenuStart();
1240
-
1241
- search(allEntries, testEntries);
1242
-
1243
- const radioCount = (allEntries) => {
1244
- const allRadio = document.querySelectorAll('.numTest');
1245
-
1246
- allRadio.forEach((radio) => {
1247
- const attribute = radio.getAttribute('lcategory');
1248
-
1249
- switch (attribute) {
1250
- case 'all':
1251
- radio.innerHTML = allEntries.length;
1252
- break;
1253
- case 'passed':
1254
- const passedCount = allEntries.filter(test => test.status === 'passed').length;
1255
- radio.innerHTML = passedCount;
1256
- break;
1257
- case 'failed':
1258
- const failedCount = allEntries.filter(test => test.status === 'failed').length;
1259
- radio.innerHTML = failedCount;
1260
- break;
1261
- case 'skipped':
1262
- const skippedCount = allEntries.filter(test => test.status === 'skipped').length;
1263
- radio.innerHTML = skippedCount;
1264
- break;
1265
- default: // no default action for now
1266
- }
3247
+ document.addEventListener('DOMContentLoaded', function() {
3248
+ const filterTabs = document.querySelectorAll('.filter-tab');
3249
+ filterTabs.forEach(tab => {
3250
+ tab.addEventListener('click', function() {
3251
+ filterTabs.forEach(t => t.classList.remove('active'));
3252
+ this.classList.add('active');
3253
+
3254
+ currentFilter = this.dataset.filter;
3255
+ currentPage = 1;
3256
+ renderTests();
1267
3257
  });
1268
- }
3258
+ });
3259
+
3260
+
3261
+ const searchInput = document.getElementById('searchInput');
3262
+ searchInput.addEventListener('input', function() {
3263
+ searchTerm = this.value.trim();
3264
+ currentPage = 1;
3265
+ renderTests();
3266
+ });
1269
3267
 
1270
- radioCount(allEntries);
3268
+ const pageSizeSelect = document.getElementById('pageSizeSelect');
3269
+ pageSizeSelect.addEventListener('change', function() {
3270
+ pageSize = parseInt(this.value);
3271
+ currentPage = 1;
3272
+ renderTests();
3273
+ });
1271
3274
 
1272
- function removeTest(category) {
1273
- const page = document.querySelector('.report');
1274
- const wrapp = page.querySelector('.testWrapp');
1275
- const clones = wrapp.querySelectorAll('div[name="testitem"][type="clone"]');
3275
+ const groupingSelect = document.getElementById('groupingSelect');
3276
+ groupingSelect.addEventListener('change', function() {
3277
+ groupingMode = this.value;
3278
+ currentPage = 1;
3279
+ renderTests();
3280
+ });
1276
3281
 
1277
- clones.forEach((clone) => {
1278
- if (clone.getAttribute('category') === category) clone.remove();
1279
- });
3282
+ const badge = document.querySelector('.status-badge');
3283
+ if (badge) {
3284
+ const s = badge.textContent.trim().toLowerCase();
3285
+ badge.classList.add(`status-${s}`);
1280
3286
  }
1281
3287
 
1282
- const add = (array) => {
1283
- for (let i = 0; i < array.length; i++) {
1284
- const status = array[i].status;
1285
- addOneTest(status, array[i]);
1286
- }
3288
+ updateCounts();
3289
+ updateTitleColor();
3290
+ renderTests();
3291
+ });
3292
+
3293
+ window.addEventListener('resize', function() {
3294
+ if (document.getElementById('testChart')) {
3295
+ drawTestChart();
1287
3296
  }
3297
+ });
3298
+
3299
+ function formatFileSize(bytes) {
3300
+ if (!bytes) return '';
3301
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
3302
+ if (bytes === 0) return '0 Bytes';
3303
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
3304
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
3305
+ }
3306
+
3307
+
3308
+ let visibleMetaKeys = new Set();
3309
+ const SETTINGS_STORAGE_KEY = 'testomatio_report_settings';
3310
+
3311
+ function collectAllMetaKeys() {
3312
+ const metaKeys = new Map();
1288
3313
 
1289
- const remove = (array) => {
1290
- for (let i = 0; i < array.length; i++) {
1291
- const status = array[i].status;
1292
- removeTest(status, array[i]);
3314
+ allTests.forEach(test => {
3315
+ if (test.meta && typeof test.meta === 'object') {
3316
+ Object.keys(test.meta).forEach(key => {
3317
+ metaKeys.set(key, (metaKeys.get(key) || 0) + 1);
3318
+ });
1293
3319
  }
3320
+ });
3321
+
3322
+ return metaKeys;
3323
+ }
3324
+
3325
+ function openSettings() {
3326
+ const modal = document.getElementById('settingsModal');
3327
+ modal.classList.add('active');
3328
+
3329
+ const savedSettings = localStorage.getItem(SETTINGS_STORAGE_KEY);
3330
+ if (savedSettings) {
3331
+ visibleMetaKeys = new Set(JSON.parse(savedSettings));
3332
+ } else {
3333
+ const metaKeys = collectAllMetaKeys();
3334
+ visibleMetaKeys = new Set(metaKeys.keys());
1294
3335
  }
1295
3336
 
1296
- add(testEntries['all'][0][0]);
3337
+ renderSettingsMetaList();
3338
+ }
3339
+
3340
+ function closeSettings() {
3341
+ const modal = document.getElementById('settingsModal');
3342
+ modal.classList.remove('active');
3343
+ }
3344
+
3345
+ function switchSettingsTab(event) {
3346
+ const clickedTab = event.currentTarget;
3347
+ const targetTab = clickedTab.getAttribute('data-tab');
1297
3348
 
1298
- const selectElement = document.querySelector('.form-select');
3349
+ const allTabs = document.querySelectorAll('.settings-tab');
3350
+ allTabs.forEach(tab => tab.classList.remove('active'));
1299
3351
 
1300
- selectElement.setAttribute('status', 'all');
3352
+ const allContents = document.querySelectorAll('.settings-tab-content');
3353
+ allContents.forEach(content => content.classList.remove('active'));
1301
3354
 
1302
- selectElement.addEventListener('change', function() {
1303
- let value = 0;
1304
- const category = selectElement.getAttribute('status');
3355
+ clickedTab.classList.add('active');
3356
+
3357
+ const targetContent = document.querySelector(`.settings-tab-content[data-tab="${targetTab}"]`);
3358
+ if (targetContent) {
3359
+ targetContent.classList.add('active');
3360
+ }
3361
+ }
1305
3362
 
1306
- remove(testEntries[category][value][0]);
1307
- value = parseInt(this.value);
1308
- add(testEntries[category][value][0]);
3363
+ function resetToDefaults() {
3364
+ const metaKeys = collectAllMetaKeys();
3365
+ visibleMetaKeys = new Set(metaKeys.keys());
1309
3366
 
1310
- displayPagination(testEntries, value, category);
3367
+ const checkboxes = document.querySelectorAll('#metaFieldsList input[type="checkbox"]');
3368
+ checkboxes.forEach(cb => {
3369
+ cb.checked = true;
1311
3370
  });
1312
3371
 
1313
- const displayPagination = (testEntries, index, category) => {
1314
- let totalPages = testEntries[category][index].length;
1315
- let currentPage = 1;
1316
-
1317
- function createPagination() {
1318
- const paginationContainers = document.querySelectorAll('.pagination');
1319
- paginationContainers.forEach((paginationContainer) => {
1320
- paginationContainer.innerHTML = '';
1321
-
1322
- let startPage = 1;
1323
- let endPage = totalPages;
1324
- if (totalPages > 10) {
1325
- if (currentPage > 5) {
1326
- startPage = currentPage - 1;
1327
- endPage = currentPage + 1;
1328
- }
1329
- else {
1330
- endPage = 5;
1331
- }
1332
- }
1333
-
1334
- if (startPage > 1) {
1335
- const startEllipsis = document.createElement('li');
1336
-
1337
- startEllipsis.classList.add('page-item');
1338
- startEllipsis.innerHTML = '<a class="page-link" href="#pagination">...</a>';
1339
- startEllipsis.addEventListener('click', () => {
1340
- remove(testEntries[category][index][currentPage - 1]);
1341
- currentPage = 1;
1342
- add(testEntries[category][index][currentPage - 1]);
1343
- createPagination();
1344
- });
1345
-
1346
- paginationContainer.appendChild(startEllipsis);
1347
- }
1348
-
1349
- for (let i = startPage; i <= endPage; i++) {
1350
- if (i <= totalPages) {
1351
- const pageButton = document.createElement('li');
1352
- pageButton.classList.add('page-item');
1353
-
1354
- if (i === currentPage) {
1355
- pageButton.classList.add('active');
1356
- }
1357
-
1358
- pageButton.innerHTML = `<a class="page-link" href="#pagination">${i}</a>`;
1359
- pageButton.addEventListener('click', () => {
1360
- remove(testEntries[category][index][currentPage - 1]);
1361
- currentPage = i;
1362
- add(testEntries[category][index][currentPage - 1]);
1363
- createPagination();
1364
- });
1365
-
1366
- paginationContainer.appendChild(pageButton);
1367
- }
1368
- }
1369
-
1370
- if (endPage < totalPages) {
1371
- const endEllipsis = document.createElement('li');
1372
-
1373
- endEllipsis.classList.add('page-item');
1374
- endEllipsis.innerHTML = '<a class="page-link" href="#pagination">...</a>';
1375
- endEllipsis.addEventListener('click', () => {
1376
- remove(testEntries[category][index][currentPage - 1]);
1377
- currentPage = totalPages;
1378
- add(testEntries[category][index][currentPage - 1]);
1379
- createPagination();
1380
- });
1381
-
1382
- paginationContainer.appendChild(endEllipsis);
1383
- }
1384
-
1385
- if (currentPage > 1) {
1386
- const prevButton = document.createElement('li');
1387
-
1388
- prevButton.classList.add('page-item');
1389
- prevButton.innerHTML = '<a class="page-link" href="#pagination">Previous</a>';
1390
- prevButton.addEventListener('click', () => {
1391
- remove(testEntries[category][index][currentPage - 1]);
1392
- currentPage--;
1393
- add(testEntries[category][index][currentPage - 1]);
1394
- createPagination();
1395
- });
1396
-
1397
- paginationContainer.insertBefore(prevButton, paginationContainer.firstChild);
1398
- }
1399
- if (currentPage < totalPages) {
1400
- const nextButton = document.createElement('li');
1401
-
1402
- nextButton.classList.add('page-item');
1403
- nextButton.innerHTML = '<a class="page-link" href="#pagination">Next</a>';
1404
- nextButton.addEventListener('click', () => {
1405
- remove(testEntries[category][index][currentPage - 1]);
1406
- currentPage++;
1407
- add(testEntries[category][index][currentPage - 1]);
1408
- createPagination();
1409
- });
1410
-
1411
- paginationContainer.appendChild(nextButton);
1412
- }
1413
- });
3372
+ console.log('[Settings] Reset to defaults - all meta fields visible');
3373
+ }
3374
+
3375
+ function renderSettingsMetaList() {
3376
+ const container = document.getElementById('metaFieldsList');
3377
+ const metaKeys = collectAllMetaKeys();
3378
+
3379
+ if (metaKeys.size === 0) {
3380
+ container.innerHTML = `
3381
+ <div style='grid-column: 1/-1; text-align: center; color: var(--gray-500); padding: 40px;'>
3382
+ <i class='fas fa-tag' style='font-size: 48px; margin-bottom: 15px; opacity: 0.3;'></i>
3383
+ <p style='margin: 0;'>No metadata fields found in tests</p>
3384
+ </div>
3385
+ `;
3386
+ return;
3387
+ }
3388
+
3389
+ const sortedKeys = Array.from(metaKeys.entries()).sort((a, b) => b[1] - a[1]);
3390
+
3391
+ container.innerHTML = sortedKeys.map(([key, count]) => `
3392
+ <label class='settings-meta-item'>
3393
+ <input
3394
+ type='checkbox'
3395
+ value='${key}'
3396
+ ${visibleMetaKeys.has(key) ? 'checked' : ''}
3397
+ onchange='toggleMetaKey("${key}")'
3398
+ >
3399
+ <span class='settings-meta-label'>${key}</span>
3400
+ <span class='settings-meta-count'>${count}</span>
3401
+ </label>
3402
+ `).join('');
3403
+ }
3404
+
3405
+ function toggleMetaKey(key) {
3406
+ if (visibleMetaKeys.has(key)) {
3407
+ visibleMetaKeys.delete(key);
3408
+ } else {
3409
+ visibleMetaKeys.add(key);
3410
+ }
3411
+ }
3412
+
3413
+ function saveSettings() {
3414
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(Array.from(visibleMetaKeys)));
3415
+
3416
+ applyMetaVisibility();
3417
+
3418
+ closeSettings();
3419
+ }
3420
+
3421
+ function applyMetaVisibility() {
3422
+ const allMetaItems = document.querySelectorAll('.meta-item[data-meta-key]');
3423
+
3424
+ allMetaItems.forEach(item => {
3425
+ const key = item.getAttribute('data-meta-key');
3426
+ if (visibleMetaKeys.has(key)) {
3427
+ item.classList.remove('hidden');
3428
+ } else {
3429
+ item.classList.add('hidden');
1414
3430
  }
1415
- createPagination();
3431
+ });
3432
+ }
3433
+
3434
+ function initializeSettings() {
3435
+ const savedSettings = localStorage.getItem(SETTINGS_STORAGE_KEY);
3436
+
3437
+ if (savedSettings) {
3438
+ try {
3439
+ const parsed = JSON.parse(savedSettings);
3440
+ visibleMetaKeys = new Set(parsed);
3441
+ } catch (e) {
3442
+ console.error('[Settings] Failed to parse saved settings, using defaults', e);
3443
+ const metaKeys = collectAllMetaKeys();
3444
+ visibleMetaKeys = new Set(metaKeys.keys());
3445
+ }
3446
+ } else {
3447
+ const metaKeys = collectAllMetaKeys();
3448
+ visibleMetaKeys = new Set(metaKeys.keys());
1416
3449
  }
1417
- displayPagination(testEntries, 0, 'all');
3450
+
1418
3451
  }
3452
+
3453
+ document.addEventListener('DOMContentLoaded', function() {
3454
+ initializeSettings();
3455
+ });
3456
+
3457
+ const originalRenderTests = renderTests;
3458
+ renderTests = function() {
3459
+ originalRenderTests();
3460
+ setTimeout(applyMetaVisibility, 50);
3461
+ };
1419
3462
  </script>
1420
3463
  </body>
1421
3464
  </html>