@juliendu11/japa-ui-reporter 0.0.2 → 0.0.4

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.
@@ -1,538 +1,687 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Test Dashboard</title>
7
- <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
-
10
- body {
11
- font-family: 'Segoe UI', system-ui, sans-serif;
12
- background: #0f1117;
13
- color: #e2e8f0;
14
- min-height: 100vh;
15
- padding: 2rem;
16
- }
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Test Dashboard</title>
7
+ <style>
8
+ *, *::before, *::after {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
17
13
 
18
- header {
19
- display: flex;
20
- align-items: center;
21
- justify-content: space-between;
22
- margin-bottom: 2rem;
23
- }
14
+ body {
15
+ font-family: 'Segoe UI', system-ui, sans-serif;
16
+ background: #0f1117;
17
+ color: #e2e8f0;
18
+ min-height: 100vh;
19
+ padding: 2rem;
20
+ }
24
21
 
25
- h1 {
26
- font-size: 1.5rem;
27
- font-weight: 600;
28
- color: #f8fafc;
29
- }
22
+ header {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ margin-bottom: 2rem;
27
+ }
30
28
 
31
- #status {
32
- display: flex;
33
- align-items: center;
34
- gap: 0.5rem;
35
- font-size: 0.85rem;
36
- color: #94a3b8;
37
- }
29
+ h1 {
30
+ font-size: 1.5rem;
31
+ font-weight: 600;
32
+ color: #f8fafc;
33
+ }
38
34
 
39
- #status-dot {
40
- width: 8px;
41
- height: 8px;
42
- border-radius: 50%;
43
- background: #ef4444;
44
- transition: background 0.3s;
45
- }
35
+ #status {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 0.5rem;
39
+ font-size: 0.85rem;
40
+ color: #94a3b8;
41
+ }
46
42
 
47
- #status-dot.connected { background: #22c55e; }
48
- #status-dot.running { background: #f59e0b; animation: pulse 1s infinite; }
43
+ #status-dot {
44
+ width: 8px;
45
+ height: 8px;
46
+ border-radius: 50%;
47
+ background: #ef4444;
48
+ transition: background 0.3s;
49
+ }
49
50
 
50
- @keyframes pulse {
51
- 0%, 100% { opacity: 1; }
52
- 50% { opacity: 0.4; }
53
- }
51
+ #status-dot.connected {
52
+ background: #22c55e;
53
+ }
54
54
 
55
- #summary {
56
- display: flex;
57
- gap: 1rem;
58
- margin-bottom: 2rem;
59
- }
55
+ #status-dot.running {
56
+ background: #f59e0b;
57
+ animation: pulse 1s infinite;
58
+ }
60
59
 
61
- .stat {
62
- background: #1e2130;
63
- border-radius: 8px;
64
- padding: 0.75rem 1.25rem;
65
- min-width: 90px;
66
- text-align: center;
67
- }
60
+ @keyframes pulse {
61
+ 0%, 100% {
62
+ opacity: 1;
63
+ }
64
+ 50% {
65
+ opacity: 0.4;
66
+ }
67
+ }
68
68
 
69
- .stat-value {
70
- font-size: 1.75rem;
71
- font-weight: 700;
72
- line-height: 1;
73
- }
69
+ #summary {
70
+ display: flex;
71
+ gap: 1rem;
72
+ margin-bottom: 2rem;
73
+ }
74
74
 
75
- .stat-label {
76
- font-size: 0.75rem;
77
- color: #64748b;
78
- margin-top: 0.25rem;
79
- text-transform: uppercase;
80
- letter-spacing: 0.05em;
81
- }
75
+ .stat {
76
+ background: #1e2130;
77
+ border-radius: 8px;
78
+ padding: 0.75rem 1.25rem;
79
+ min-width: 90px;
80
+ text-align: center;
81
+ }
82
82
 
83
- .stat.pass .stat-value { color: #22c55e; }
84
- .stat.fail .stat-value { color: #ef4444; }
85
- .stat.total .stat-value { color: #94a3b8; }
83
+ .stat-value {
84
+ font-size: 1.75rem;
85
+ font-weight: 700;
86
+ line-height: 1;
87
+ }
86
88
 
87
- #groups {
88
- display: flex;
89
- flex-direction: column;
90
- gap: 1.5rem;
91
- }
89
+ .stat-label {
90
+ font-size: 0.75rem;
91
+ color: #64748b;
92
+ margin-top: 0.25rem;
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.05em;
95
+ }
92
96
 
93
- .group {
94
- background: #1e2130;
95
- border-radius: 10px;
96
- overflow: hidden;
97
- border: 1px solid #2d3348;
98
- }
97
+ .stat.pass .stat-value {
98
+ color: #22c55e;
99
+ }
99
100
 
100
- .group-header {
101
- display: flex;
102
- align-items: center;
103
- justify-content: space-between;
104
- padding: 0.85rem 1.25rem;
105
- background: #252840;
106
- cursor: pointer;
107
- user-select: none;
108
- }
101
+ .stat.fail .stat-value {
102
+ color: #ef4444;
103
+ }
109
104
 
110
- .group-header:hover { background: #2c304d; }
105
+ .stat.total .stat-value {
106
+ color: #94a3b8;
107
+ }
111
108
 
112
- .group-title {
113
- font-weight: 600;
114
- font-size: 0.95rem;
115
- color: #c7d2fe;
116
- }
109
+ #groups {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 1.5rem;
113
+ }
117
114
 
118
- .group-badges {
119
- display: flex;
120
- gap: 0.5rem;
121
- align-items: center;
122
- font-size: 0.8rem;
123
- }
115
+ .group {
116
+ background: #1e2130;
117
+ border-radius: 10px;
118
+ overflow: hidden;
119
+ border: 1px solid #2d3348;
120
+ }
124
121
 
125
- .badge {
126
- padding: 0.2rem 0.55rem;
127
- border-radius: 999px;
128
- font-weight: 600;
129
- }
122
+ .group-header {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ padding: 0.85rem 1.25rem;
127
+ background: #252840;
128
+ cursor: pointer;
129
+ user-select: none;
130
+ }
130
131
 
131
- .badge.pass { background: #14532d; color: #86efac; }
132
- .badge.fail { background: #450a0a; color: #fca5a5; }
132
+ .group-header:hover {
133
+ background: #2c304d;
134
+ }
133
135
 
134
- .group-tests {
135
- list-style: none;
136
- }
136
+ .group-title {
137
+ font-weight: 600;
138
+ font-size: 0.95rem;
139
+ color: #c7d2fe;
140
+ }
137
141
 
138
- .test {
139
- display: flex;
140
- align-items: flex-start;
141
- gap: 0.75rem;
142
- padding: 0.75rem 1.25rem;
143
- border-top: 1px solid #2d3348;
144
- transition: background 0.15s;
145
- }
142
+ .group-badges {
143
+ display: flex;
144
+ gap: 0.5rem;
145
+ align-items: center;
146
+ font-size: 0.8rem;
147
+ }
146
148
 
147
- .test:hover { background: #242840; }
149
+ .badge {
150
+ padding: 0.2rem 0.55rem;
151
+ border-radius: 999px;
152
+ font-weight: 600;
153
+ }
148
154
 
149
- .test-icon {
150
- flex-shrink: 0;
151
- margin-top: 2px;
152
- font-size: 0.9rem;
153
- }
155
+ .badge.pass {
156
+ background: #14532d;
157
+ color: #86efac;
158
+ }
154
159
 
155
- .test-body {
156
- flex: 1;
157
- min-width: 0;
158
- }
160
+ .badge.fail {
161
+ background: #450a0a;
162
+ color: #fca5a5;
163
+ }
159
164
 
160
- .test-title {
161
- font-size: 0.875rem;
162
- color: #e2e8f0;
163
- word-break: break-word;
164
- margin-bottom: 10px;
165
- }
165
+ .group-tests {
166
+ list-style: none;
167
+ }
166
168
 
167
- .test-filename {
168
- font-size: 0.80rem;
169
- color: #e2e8f0;
170
- word-break: break-word;
171
- }
169
+ .test {
170
+ display: flex;
171
+ align-items: flex-start;
172
+ gap: 0.75rem;
173
+ padding: 0.75rem 1.25rem;
174
+ border-top: 1px solid #2d3348;
175
+ transition: background 0.15s;
176
+ }
172
177
 
173
- .test-duration {
174
- margin-top: 10px;
175
- font-size: 0.75rem;
176
- color: #475569;
177
- }
178
+ .test:hover {
179
+ background: #242840;
180
+ }
178
181
 
179
- .test-errors {
180
- margin-top: 0.5rem;
181
- display: flex;
182
- flex-direction: column;
183
- gap: 0.4rem;
184
- }
182
+ .test-icon {
183
+ flex-shrink: 0;
184
+ margin-top: 2px;
185
+ font-size: 0.9rem;
186
+ }
185
187
 
186
- .test-error {
187
- background: #1a0a0a;
188
- border-left: 3px solid #ef4444;
189
- border-radius: 4px;
190
- padding: 0.5rem 0.75rem;
191
- font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
192
- font-size: 0.78rem;
193
- color: #fca5a5;
194
- white-space: pre-wrap;
195
- word-break: break-word;
196
- }
188
+ .test-body {
189
+ flex: 1;
190
+ min-width: 0;
191
+ }
197
192
 
198
- .test-error-phase {
199
- font-size: 0.7rem;
200
- color: #ef4444;
201
- font-family: inherit;
202
- margin-bottom: 0.25rem;
203
- text-transform: uppercase;
204
- letter-spacing: 0.05em;
205
- }
193
+ .test-title {
194
+ font-size: 0.875rem;
195
+ color: #e2e8f0;
196
+ word-break: break-word;
197
+ margin-bottom: 10px;
198
+ }
206
199
 
207
- #empty {
208
- text-align: center;
209
- color: #475569;
210
- padding: 4rem 0;
211
- font-size: 0.95rem;
212
- }
200
+ .test-filename {
201
+ font-size: 0.80rem;
202
+ color: #e2e8f0;
203
+ word-break: break-word;
204
+ }
213
205
 
214
- #empty p:first-child {
215
- font-size: 2rem;
216
- margin-bottom: 0.5rem;
217
- }
206
+ .test-duration {
207
+ margin-top: 10px;
208
+ font-size: 0.75rem;
209
+ color: #475569;
210
+ }
218
211
 
219
- .diff-block {
220
- margin-top: 0.5rem;
221
- border-radius: 4px;
222
- overflow: hidden;
223
- border: 1px solid #2d3348;
224
- }
212
+ .test-errors {
213
+ margin-top: 0.5rem;
214
+ display: flex;
215
+ flex-direction: column;
216
+ gap: 0.4rem;
217
+ }
225
218
 
226
- .diff-legend {
227
- display: flex;
228
- gap: 1.5rem;
229
- padding: 0.3rem 0.5rem;
230
- background: #1e2130;
231
- font-size: 0.7rem;
232
- border-bottom: 1px solid #2d3348;
233
- }
219
+ .test-error {
220
+ background: #1a0a0a;
221
+ border-left: 3px solid #ef4444;
222
+ border-radius: 4px;
223
+ padding: 0.5rem 0.75rem;
224
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
225
+ font-size: 0.78rem;
226
+ color: #fca5a5;
227
+ white-space: pre-wrap;
228
+ word-break: break-word;
229
+ }
234
230
 
235
- .diff-legend-removed { color: #fca5a5; }
236
- .diff-legend-added { color: #86efac; }
231
+ .test-error-phase {
232
+ font-size: 0.7rem;
233
+ color: #ef4444;
234
+ font-family: inherit;
235
+ margin-bottom: 0.25rem;
236
+ text-transform: uppercase;
237
+ letter-spacing: 0.05em;
238
+ }
237
239
 
238
- .diff-line {
239
- padding: 1px 0.5rem;
240
- white-space: pre-wrap;
241
- word-break: break-all;
242
- font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
243
- font-size: 0.78rem;
244
- line-height: 1.5;
245
- }
240
+ #empty {
241
+ text-align: center;
242
+ color: #475569;
243
+ padding: 4rem 0;
244
+ font-size: 0.95rem;
245
+ }
246
+
247
+ #empty p:first-child {
248
+ font-size: 2rem;
249
+ margin-bottom: 0.5rem;
250
+ }
251
+
252
+ .diff-block {
253
+ margin-top: 0.5rem;
254
+ border-radius: 4px;
255
+ overflow: hidden;
256
+ border: 1px solid #2d3348;
257
+ }
258
+
259
+ .diff-legend {
260
+ display: flex;
261
+ gap: 1.5rem;
262
+ padding: 0.3rem 0.5rem;
263
+ background: #1e2130;
264
+ font-size: 0.7rem;
265
+ border-bottom: 1px solid #2d3348;
266
+ }
267
+
268
+ .diff-legend-removed {
269
+ color: #fca5a5;
270
+ }
271
+
272
+ .diff-legend-added {
273
+ color: #86efac;
274
+ }
275
+
276
+ .diff-line {
277
+ padding: 1px 0.5rem;
278
+ white-space: pre-wrap;
279
+ word-break: break-all;
280
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
281
+ font-size: 0.78rem;
282
+ line-height: 1.5;
283
+ }
284
+
285
+ .diff-line.removed {
286
+ background: #3b0a0a;
287
+ color: #fca5a5;
288
+ }
289
+
290
+ .diff-line.added {
291
+ background: #052e16;
292
+ color: #86efac;
293
+ }
294
+
295
+ .diff-line.unchanged {
296
+ color: #475569;
297
+ }
298
+
299
+ #filters {
300
+ display: flex;
301
+ gap: 0.5rem;
302
+ margin-bottom: 1.5rem;
303
+ }
304
+
305
+ .filter-btn {
306
+ padding: 0.35rem 0.9rem;
307
+ border-radius: 999px;
308
+ border: 1px solid #2d3348;
309
+ background: #1e2130;
310
+ color: #94a3b8;
311
+ font-size: 0.8rem;
312
+ font-weight: 500;
313
+ cursor: pointer;
314
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
315
+ }
316
+
317
+ .filter-btn:hover {
318
+ background: #252840;
319
+ color: #e2e8f0;
320
+ }
321
+
322
+ .filter-btn.active {
323
+ border-color: #6366f1;
324
+ background: #2d2f5e;
325
+ color: #c7d2fe;
326
+ }
246
327
 
247
- .diff-line.removed { background: #3b0a0a; color: #fca5a5; }
248
- .diff-line.added { background: #052e16; color: #86efac; }
249
- .diff-line.unchanged { color: #475569; }
250
- </style>
328
+ .filter-btn.active.pass {
329
+ border-color: #22c55e;
330
+ background: #14532d;
331
+ color: #86efac;
332
+ }
333
+
334
+ .filter-btn.active.fail {
335
+ border-color: #ef4444;
336
+ background: #450a0a;
337
+ color: #fca5a5;
338
+ }
339
+ </style>
251
340
  </head>
252
341
  <body>
253
342
  <header>
254
- <h1>Test Dashboard</h1>
255
- <div id="status">
256
- <div id="status-dot"></div>
257
- <span id="status-text">Connecting...</span>
258
- </div>
343
+ <h1>Test Dashboard</h1>
344
+ <div id="status">
345
+ <div id="status-dot"></div>
346
+ <span id="status-text">Connecting...</span>
347
+ </div>
259
348
  </header>
260
349
 
261
350
  <div id="summary">
262
- <div class="stat total">
263
- <div class="stat-value" id="count-total">0</div>
264
- <div class="stat-label">Total</div>
265
- </div>
266
- <div class="stat pass">
267
- <div class="stat-value" id="count-pass">0</div>
268
- <div class="stat-label">Passed</div>
269
- </div>
270
- <div class="stat fail">
271
- <div class="stat-value" id="count-fail">0</div>
272
- <div class="stat-label">Failed</div>
273
- </div>
351
+ <div class="stat total">
352
+ <div class="stat-value" id="count-total">0</div>
353
+ <div class="stat-label">Total</div>
354
+ </div>
355
+ <div class="stat pass">
356
+ <div class="stat-value" id="count-pass">0</div>
357
+ <div class="stat-label">Passed</div>
358
+ </div>
359
+ <div class="stat fail">
360
+ <div class="stat-value" id="count-fail">0</div>
361
+ <div class="stat-label">Failed</div>
362
+ </div>
363
+ </div>
364
+
365
+ <div id="filters">
366
+ <button class="filter-btn active" data-filter="all">All</button>
367
+ <button class="filter-btn pass" data-filter="pass">Passed</button>
368
+ <button class="filter-btn fail" data-filter="fail">Failed</button>
274
369
  </div>
275
370
 
276
371
  <div id="groups">
277
- <div id="empty">
278
- <p>&#9634;</p>
279
- <p>Waiting for test results...</p>
280
- </div>
372
+ <div id="empty">
373
+ <p>&#9634;</p>
374
+ <p>Waiting for test results...</p>
375
+ </div>
281
376
  </div>
282
377
 
283
378
  <script>
284
- const statusDot = document.getElementById('status-dot');
285
- const statusText = document.getElementById('status-text');
286
- const groupsContainer = document.getElementById('groups');
287
- const emptyState = document.getElementById('empty');
288
- const countTotal = document.getElementById('count-total');
289
- const countPass = document.getElementById('count-pass');
290
- const countFail = document.getElementById('count-fail');
379
+ const statusDot = document.getElementById('status-dot');
380
+ const statusText = document.getElementById('status-text');
381
+ const groupsContainer = document.getElementById('groups');
382
+ const emptyState = document.getElementById('empty');
383
+ const countTotal = document.getElementById('count-total');
384
+ const countPass = document.getElementById('count-pass');
385
+ const countFail = document.getElementById('count-fail');
386
+
387
+ // groupTitle -> { element, tests: [] }
388
+ const groups = new Map();
389
+ let stats = {total: 0, pass: 0, fail: 0};
390
+ let activeFilter = 'all';
391
+
392
+ function applyFilter() {
393
+ for (const group of groups.values()) {
394
+ let visibleCount = 0;
395
+ for (const li of group.testList.children) {
396
+ const isFailed = li.dataset.failed === '1';
397
+ const visible =
398
+ activeFilter === 'all' ||
399
+ (activeFilter === 'pass' && !isFailed) ||
400
+ (activeFilter === 'fail' && isFailed);
401
+ li.style.display = visible ? '' : 'none';
402
+ if (visible) visibleCount++;
403
+ }
404
+ group.element.style.display = visibleCount === 0 ? 'none' : '';
405
+ }
406
+ }
407
+
408
+ document.getElementById('filters').addEventListener('click', (e) => {
409
+ const btn = e.target.closest('.filter-btn');
410
+ if (!btn) return;
411
+ activeFilter = btn.dataset.filter;
412
+ for (const b of document.querySelectorAll('.filter-btn')) {
413
+ b.classList.toggle('active', b === btn);
414
+ }
415
+ applyFilter();
416
+ });
291
417
 
292
- // groupTitle -> { element, tests: [] }
293
- const groups = new Map();
294
- let stats = { total: 0, pass: 0, fail: 0 };
418
+ function setStatus(state, text) {
419
+ statusDot.className = state;
420
+ statusText.textContent = text;
421
+ }
295
422
 
296
- function setStatus(state, text) {
297
- statusDot.className = state;
298
- statusText.textContent = text;
299
- }
423
+ function updateStats() {
424
+ countTotal.textContent = stats.total;
425
+ countPass.textContent = stats.pass;
426
+ countFail.textContent = stats.fail;
427
+ }
300
428
 
301
- function updateStats() {
302
- countTotal.textContent = stats.total;
303
- countPass.textContent = stats.pass;
304
- countFail.textContent = stats.fail;
305
- }
429
+ function getOrCreateGroup(groupTitle) {
430
+ if (groups.has(groupTitle)) return groups.get(groupTitle);
306
431
 
307
- function getOrCreateGroup(groupTitle) {
308
- if (groups.has(groupTitle)) return groups.get(groupTitle);
432
+ emptyState.style.display = 'none';
309
433
 
310
- emptyState.style.display = 'none';
434
+ const groupEl = document.createElement('div');
435
+ groupEl.className = 'group';
311
436
 
312
- const groupEl = document.createElement('div');
313
- groupEl.className = 'group';
437
+ const header = document.createElement('div');
438
+ header.className = 'group-header';
314
439
 
315
- const header = document.createElement('div');
316
- header.className = 'group-header';
440
+ const title = document.createElement('span');
441
+ title.className = 'group-title';
442
+ title.textContent = groupTitle;
317
443
 
318
- const title = document.createElement('span');
319
- title.className = 'group-title';
320
- title.textContent = groupTitle;
444
+ const badges = document.createElement('div');
445
+ badges.className = 'group-badges';
321
446
 
322
- const badges = document.createElement('div');
323
- badges.className = 'group-badges';
447
+ header.appendChild(title);
448
+ header.appendChild(badges);
324
449
 
325
- header.appendChild(title);
326
- header.appendChild(badges);
450
+ const testList = document.createElement('ul');
451
+ testList.className = 'group-tests';
327
452
 
328
- const testList = document.createElement('ul');
329
- testList.className = 'group-tests';
453
+ header.addEventListener('click', () => {
454
+ testList.style.display = testList.style.display === 'none' ? '' : 'none';
455
+ });
330
456
 
331
- header.addEventListener('click', () => {
332
- testList.style.display = testList.style.display === 'none' ? '' : 'none';
333
- });
457
+ groupEl.appendChild(header);
458
+ groupEl.appendChild(testList);
459
+ groupsContainer.appendChild(groupEl);
334
460
 
335
- groupEl.appendChild(header);
336
- groupEl.appendChild(testList);
337
- groupsContainer.appendChild(groupEl);
461
+ const entry = {element: groupEl, header, badges, testList, pass: 0, fail: 0};
462
+ groups.set(groupTitle, entry);
463
+ return entry;
464
+ }
338
465
 
339
- const entry = { element: groupEl, header, badges, testList, pass: 0, fail: 0 };
340
- groups.set(groupTitle, entry);
341
- return entry;
466
+ function updateGroupBadges(group) {
467
+ group.badges.innerHTML = '';
468
+ if (group.pass > 0) {
469
+ const b = document.createElement('span');
470
+ b.className = 'badge pass';
471
+ b.textContent = `${group.pass} passed`;
472
+ group.badges.appendChild(b);
342
473
  }
343
-
344
- function updateGroupBadges(group) {
345
- group.badges.innerHTML = '';
346
- if (group.pass > 0) {
347
- const b = document.createElement('span');
348
- b.className = 'badge pass';
349
- b.textContent = `${group.pass} passed`;
350
- group.badges.appendChild(b);
351
- }
352
- if (group.fail > 0) {
353
- const b = document.createElement('span');
354
- b.className = 'badge fail';
355
- b.textContent = `${group.fail} failed`;
356
- group.badges.appendChild(b);
357
- }
474
+ if (group.fail > 0) {
475
+ const b = document.createElement('span');
476
+ b.className = 'badge fail';
477
+ b.textContent = `${group.fail} failed`;
478
+ group.badges.appendChild(b);
479
+ }
480
+ }
481
+
482
+ function formatError(err) {
483
+ if (typeof err === 'string') return err;
484
+ if (err && typeof err === 'object') return JSON.stringify(err, null, 2);
485
+ return String(err);
486
+ }
487
+
488
+ function buildDiffElement(actual, expected) {
489
+ const allKeys = Array.from(new Set([...Object.keys(actual), ...Object.keys(expected)]));
490
+ const lines = [];
491
+
492
+ for (const key of allKeys) {
493
+ const hasA = Object.prototype.hasOwnProperty.call(actual, key);
494
+ const hasE = Object.prototype.hasOwnProperty.call(expected, key);
495
+ if (hasA && hasE) {
496
+ const av = JSON.stringify(actual[key], null, 2);
497
+ const ev = JSON.stringify(expected[key], null, 2);
498
+ if (av === ev) {
499
+ lines.push({type: 'unchanged', text: ` ${key}: ${av}`});
500
+ } else {
501
+ lines.push({type: 'removed', text: `- ${key}: ${av}`});
502
+ lines.push({type: 'added', text: `+ ${key}: ${ev}`});
503
+ }
504
+ } else if (hasA) {
505
+ lines.push({type: 'removed', text: `- ${key}: ${JSON.stringify(actual[key], null, 2)}`});
506
+ } else {
507
+ lines.push({type: 'added', text: `+ ${key}: ${JSON.stringify(expected[key], null, 2)}`});
508
+ }
358
509
  }
359
510
 
360
- function formatError(err) {
361
- if (typeof err === 'string') return err;
362
- if (err && typeof err === 'object') return JSON.stringify(err, null, 2);
363
- return String(err);
364
- }
365
-
366
- function buildDiffElement(actual, expected) {
367
- const allKeys = Array.from(new Set([...Object.keys(actual), ...Object.keys(expected)]));
368
- const lines = [];
369
-
370
- for (const key of allKeys) {
371
- const hasA = Object.prototype.hasOwnProperty.call(actual, key);
372
- const hasE = Object.prototype.hasOwnProperty.call(expected, key);
373
- if (hasA && hasE) {
374
- const av = JSON.stringify(actual[key], null, 2);
375
- const ev = JSON.stringify(expected[key], null, 2);
376
- if (av === ev) {
377
- lines.push({ type: 'unchanged', text: ` ${key}: ${av}` });
378
- } else {
379
- lines.push({ type: 'removed', text: `- ${key}: ${av}` });
380
- lines.push({ type: 'added', text: `+ ${key}: ${ev}` });
381
- }
382
- } else if (hasA) {
383
- lines.push({ type: 'removed', text: `- ${key}: ${JSON.stringify(actual[key], null, 2)}` });
384
- } else {
385
- lines.push({ type: 'added', text: `+ ${key}: ${JSON.stringify(expected[key], null, 2)}` });
386
- }
387
- }
511
+ const block = document.createElement('div');
512
+ block.className = 'diff-block';
513
+
514
+ const legend = document.createElement('div');
515
+ legend.className = 'diff-legend';
516
+ legend.innerHTML = '<span class="diff-legend-removed">- actual</span><span class="diff-legend-added">+ expected</span>';
517
+ block.appendChild(legend);
388
518
 
389
- const block = document.createElement('div');
390
- block.className = 'diff-block';
519
+ for (const {type, text} of lines) {
520
+ const row = document.createElement('div');
521
+ row.className = `diff-line ${type}`;
522
+ row.textContent = text;
523
+ block.appendChild(row);
524
+ }
391
525
 
392
- const legend = document.createElement('div');
393
- legend.className = 'diff-legend';
394
- legend.innerHTML = '<span class="diff-legend-removed">- actual</span><span class="diff-legend-added">+ expected</span>';
395
- block.appendChild(legend);
526
+ return block;
527
+ }
396
528
 
397
- for (const { type, text } of lines) {
398
- const row = document.createElement('div');
399
- row.className = `diff-line ${type}`;
400
- row.textContent = text;
401
- block.appendChild(row);
402
- }
529
+ function addTestResult(data) {
530
+ const groupTitle = data.group?.title || 'Ungrouped';
531
+ const group = getOrCreateGroup(groupTitle);
403
532
 
404
- return block;
405
- }
406
-
407
- function addTestResult(data) {
408
- const groupTitle = data.group?.title || 'Ungrouped';
409
- const group = getOrCreateGroup(groupTitle);
410
-
411
- const passed = !data.hasError;
412
-
413
- stats.total++;
414
- if (passed) { stats.pass++; group.pass++; }
415
- else { stats.fail++; group.fail++; }
416
-
417
- updateStats();
418
- updateGroupBadges(group);
419
-
420
- const li = document.createElement('li');
421
- li.className = 'test';
422
- li.dataset.failed = passed ? '0' : '1';
423
-
424
- const icon = document.createElement('span');
425
- icon.className = 'test-icon';
426
- icon.textContent = passed ? '✓' : '✗';
427
- icon.style.color = passed ? '#22c55e' : '#ef4444';
428
-
429
- const body = document.createElement('div');
430
- body.className = 'test-body';
431
-
432
- const titleEl = document.createElement('div');
433
- titleEl.className = 'test-title';
434
- titleEl.textContent = data.title;
435
-
436
- const filename = document.createElement('code');
437
- filename.className = 'test-filename';
438
- filename.textContent = data.file.name
439
- filename.addEventListener('click', () => {
440
- navigator.clipboard.writeText(data.file.name)
441
- })
442
-
443
- const duration = document.createElement('div');
444
- duration.className = 'test-duration';
445
- duration.textContent = `${(data.duration || 0).toFixed(2)} ms`;
446
-
447
- body.appendChild(titleEl);
448
- body.appendChild(filename);
449
- body.appendChild(duration);
450
-
451
- if (!passed && data.errors?.length) {
452
- const errorsEl = document.createElement('div');
453
- errorsEl.className = 'test-errors';
454
- for (const e of data.errors) {
455
- const errEl = document.createElement('div');
456
- errEl.className = 'test-error';
457
- const phase = document.createElement('div');
458
- phase.className = 'test-error-phase';
459
- phase.textContent = e.phase || 'error';
460
- errEl.appendChild(phase);
461
- if (e.error?.operator === 'deepStrictEqual') {
462
- errEl.appendChild(buildDiffElement(e.error.actual, e.error.expected));
463
- } else {
464
- errEl.appendChild(document.createTextNode(formatError(e.error)));
465
- }
466
- errorsEl.appendChild(errEl);
467
- }
468
- body.appendChild(errorsEl);
469
- }
533
+ const passed = !data.hasError;
470
534
 
471
- li.appendChild(icon);
472
- li.appendChild(body);
473
- group.testList.appendChild(li);
535
+ stats.total++;
536
+ if (passed) {
537
+ stats.pass++;
538
+ group.pass++;
539
+ } else {
540
+ stats.fail++;
541
+ group.fail++;
542
+ }
474
543
 
475
- // Scroll to keep latest visible
476
- li.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
544
+ updateStats();
545
+ updateGroupBadges(group);
546
+
547
+ const li = document.createElement('li');
548
+ li.className = 'test';
549
+ li.dataset.failed = passed ? '0' : '1';
550
+
551
+ const icon = document.createElement('span');
552
+ icon.className = 'test-icon';
553
+ icon.textContent = passed ? '✓' : '✗';
554
+ icon.style.color = passed ? '#22c55e' : '#ef4444';
555
+
556
+ const body = document.createElement('div');
557
+ body.className = 'test-body';
558
+
559
+ const titleEl = document.createElement('div');
560
+ titleEl.className = 'test-title';
561
+ titleEl.textContent = data.title;
562
+
563
+ const filename = document.createElement('code');
564
+ filename.className = 'test-filename';
565
+ filename.textContent = data.file.name
566
+ filename.addEventListener('click', () => {
567
+ navigator.clipboard.writeText(data.file.name)
568
+ })
569
+
570
+ const duration = document.createElement('div');
571
+ duration.className = 'test-duration';
572
+ duration.textContent = `${(data.duration || 0).toFixed(2)} ms`;
573
+
574
+ body.appendChild(titleEl);
575
+ body.appendChild(filename);
576
+ body.appendChild(duration);
577
+
578
+ if (!passed && data.errors?.length) {
579
+ const errorsEl = document.createElement('div');
580
+ errorsEl.className = 'test-errors';
581
+ for (const e of data.errors) {
582
+ const errEl = document.createElement('div');
583
+ errEl.className = 'test-error';
584
+ const phase = document.createElement('div');
585
+ phase.className = 'test-error-phase';
586
+ phase.textContent = e.phase || 'error';
587
+ errEl.appendChild(phase);
588
+ if (e.error?.operator === 'deepStrictEqual') {
589
+ errEl.appendChild(buildDiffElement(e.error.actual, e.error.expected));
590
+ } else {
591
+ errEl.appendChild(document.createTextNode(formatError(e.error)));
592
+ }
593
+ errorsEl.appendChild(errEl);
594
+ }
595
+ body.appendChild(errorsEl);
477
596
  }
478
597
 
479
- function sortResults() {
480
- // Sort tests within each group: failed first
481
- for (const group of groups.values()) {
482
- const items = [...group.testList.children];
483
- items.sort((a, b) => b.dataset.failed - a.dataset.failed);
484
- for (const item of items) group.testList.appendChild(item);
485
- }
486
- // Sort groups: groups with failures first
487
- const groupEls = [...groupsContainer.children].filter(el => el !== emptyState);
488
- groupEls.sort((a, b) => {
489
- const aEntry = [...groups.values()].find(g => g.element === a);
490
- const bEntry = [...groups.values()].find(g => g.element === b);
491
- return (bEntry?.fail ?? 0) - (aEntry?.fail ?? 0);
492
- });
493
- for (const el of groupEls) groupsContainer.appendChild(el);
494
- }
495
-
496
- function clearResults() {
497
- groups.clear();
498
- stats = { total: 0, pass: 0, fail: 0 };
499
- updateStats();
500
- groupsContainer.innerHTML = '';
501
- groupsContainer.appendChild(emptyState);
502
- emptyState.style.display = '';
503
- }
504
-
505
- function connect() {
506
- const ws = new WebSocket(`ws://${location.host}`);
507
-
508
- ws.onopen = () => setStatus('connected', 'Connected');
509
-
510
- ws.onmessage = (event) => {
511
- let msg;
512
- try { msg = JSON.parse(event.data); } catch { return; }
513
-
514
- if (msg.type === 'run:start') {
515
- clearResults();
516
- setStatus('running', 'Running...');
517
- } else if (msg.type === 'test:result') {
518
- addTestResult(msg.data);
519
- } else if (msg.type === 'run:sort') {
520
- sortResults();
521
- } else if (msg.type === 'run:end') {
522
- const failText = stats.fail > 0 ? ` — ${stats.fail} failed` : '';
523
- setStatus('connected', `Done: ${stats.pass}/${stats.total} passed${failText}`);
524
- }
525
- };
526
-
527
- ws.onclose = () => {
528
- setStatus('', 'Disconnected retrying...');
529
- setTimeout(connect, 2000);
530
- };
531
-
532
- ws.onerror = () => ws.close();
533
- }
534
-
535
- connect();
598
+ li.appendChild(icon);
599
+ li.appendChild(body);
600
+ group.testList.appendChild(li);
601
+
602
+ // Apply current filter to the new test
603
+ const isFailed = !passed;
604
+ const visible =
605
+ activeFilter === 'all' ||
606
+ (activeFilter === 'pass' && !isFailed) ||
607
+ (activeFilter === 'fail' && isFailed);
608
+ li.style.display = visible ? '' : 'none';
609
+
610
+ // Scroll to keep latest visible
611
+ if (visible) li.scrollIntoView({behavior: 'smooth', block: 'nearest'});
612
+ }
613
+
614
+ function sortResults() {
615
+ console.log('Sort in progress')
616
+ // Sort tests within each group: failed first
617
+ for (const group of groups.values()) {
618
+ const items = [...group.testList.children];
619
+ items.sort((a, b) => b.dataset.failed - a.dataset.failed);
620
+ for (const item of items) group.testList.appendChild(item);
621
+ }
622
+ // Sort groups: groups with failures first
623
+ const groupEls = [...groupsContainer.children].filter(el => el !== emptyState);
624
+ groupEls.sort((a, b) => {
625
+ const aEntry = [...groups.values()].find(g => g.element === a);
626
+ const bEntry = [...groups.values()].find(g => g.element === b);
627
+ return (bEntry?.fail ?? 0) - (aEntry?.fail ?? 0);
628
+ });
629
+ for (const el of groupEls) groupsContainer.appendChild(el);
630
+ applyFilter();
631
+ }
632
+
633
+ function clearResults() {
634
+ groups.clear();
635
+ stats = {total: 0, pass: 0, fail: 0};
636
+ updateStats();
637
+ groupsContainer.innerHTML = '';
638
+ groupsContainer.appendChild(emptyState);
639
+ emptyState.style.display = '';
640
+ activeFilter = 'all';
641
+ for (const b of document.querySelectorAll('.filter-btn')) {
642
+ b.classList.toggle('active', b.dataset.filter === 'all');
643
+ }
644
+ }
645
+
646
+ function connect() {
647
+ const ws = new WebSocket(`ws://${location.host}`);
648
+
649
+ ws.onopen = () => setStatus('connected', 'Connected');
650
+
651
+ ws.onmessage = (event) => {
652
+ let msg;
653
+ try {
654
+ msg = JSON.parse(event.data);
655
+ } catch {
656
+ return;
657
+ }
658
+
659
+ if (msg.type === 'run:start') {
660
+ clearResults();
661
+ setStatus('running', 'Running...');
662
+ } else if (msg.type === 'test:result') {
663
+ addTestResult(msg.data);
664
+ } else if (msg.type === 'run:sort') {
665
+ sortResults();
666
+ } else if (msg.type === 'run:end') {
667
+ const failText = stats.fail > 0 ? ` — ${stats.fail} failed` : '';
668
+ setStatus('connected', `Done: ${stats.pass}/${stats.total} passed${failText}`);
669
+ window.scrollTo({top: 0, behavior: 'smooth'});
670
+ }
671
+ };
672
+
673
+ ws.onclose = () => {
674
+ sortResults();
675
+ window.scrollTo({top: 0, behavior: 'smooth'});
676
+ setStatus('', 'Disconnected — retrying...');
677
+ setTimeout(connect, 2000);
678
+
679
+ };
680
+
681
+ ws.onerror = () => ws.close();
682
+ }
683
+
684
+ connect();
536
685
  </script>
537
686
  </body>
538
687
  </html>