ortoni-report 2.0.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,577 +1,1117 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="{{preferredTheme}}">
3
- {{> head}}
4
- <style>
5
- {{{inlineCss}}}
6
- </style>
7
- <body>
8
- {{> navbar }}
9
- <section class="section mt-6">
10
- <main class="container">
11
- <div class="columns">
12
- <aside class="column is-two-fifths">
13
- {{> testPanel}}
14
- {{> project}}
15
- </aside>
16
- <section class="column is-three-fifths">
17
- <div id="summary">
18
- <div class="columns is-multiline has-text-centered">
19
- {{> summaryCard bg="primary" status="all" statusHeader="All Tests" statusCount=totalCount}}
20
- {{> summaryCard bg="success" status="passed" statusHeader="Passed" statusCount=passCount}}
21
- {{> summaryCard bg="danger" status="failed" statusHeader="Failed" statusCount=failCount}}
22
- {{> summaryCard bg="info" status="skipped" statusHeader="Skipped" statusCount=skipCount}}
23
- {{> summaryCard bg="warning" status="flaky" statusHeader="Flaky" statusCount=flakyCount}}
24
- {{> summaryCard bg="retry" status="retry" statusHeader="Retry" statusCount=retryCount}}
25
- </div>
26
- <div class="box">
27
- {{> userInfo}}
28
- </div>
29
- </div>
30
- <div id="testDetails" style="display: none;"></div>
31
- </section>
32
- </div>
33
- </main>
34
- </section>
35
- <script>
36
- document.addEventListener('DOMContentLoaded', () => {
37
- const testData = {{{ json results }}};
38
- const testHistory = {{{ json testHistories }}};
39
- let testHistoriesMap = {};
40
- let testHistoryTitle = '';
41
-
42
- const elements = {
43
- testDetails: document.getElementById('testDetails'),
44
- summary: document.getElementById('summary'),
45
- themeButton: document.getElementById("toggle-theme"),
46
- themeIcon: document.getElementById("theme-icon"),
47
- htmlElement: document.documentElement,
48
- searchInput: document.querySelector('input[name="search"]'),
49
- detailsElements: document.querySelectorAll('details'),
50
- filtersDisplay: document.getElementById('selected-filters')
51
- };
52
-
53
- const themeManager = {
54
- init() {
55
- const preferredTheme = elements.themeButton.getAttribute("data-theme-status");
56
- this.setTheme(preferredTheme);
57
- elements.themeButton.addEventListener('click', () => this.toggleTheme());
58
- },
59
- setTheme(theme) {
60
- elements.htmlElement.setAttribute('data-theme', theme);
61
- elements.themeIcon.className = `fa fa-${theme === 'dark' ? 'moon' : 'sun'}`;
62
- },
63
- toggleTheme() {
64
- const currentTheme = elements.htmlElement.getAttribute('data-theme');
65
- this.setTheme(currentTheme === 'light' ? 'dark' : 'light');
66
- }
67
- };
68
-
69
- const testDetailsManager = {
70
- show(test) {
71
- elements.summary.style.display = 'none';
72
- elements.testDetails.style.opacity = '0';
73
- elements.testDetails.style.display = 'block';
74
- setTimeout(() => {
75
- elements.testDetails.style.opacity = '1';
76
- }, 50);
77
- this.render(test);
78
- },
79
- hide() {
80
- elements.summary.style.display = 'block';
81
- elements.testDetails.style.display = 'none';
82
- },
83
- render(test) {
84
- let currentScreenshotIndex = 0;
85
- const statusClass = this.getStatusClass(test.status);
86
- const statusIcon = this.getStatusIcon(test.status);
87
- const projectIcon = this.getProjectIcon(test.projectName);
88
-
89
- elements.testDetails.innerHTML = `
90
- <div class="sticky-header">
91
- <div class="card mb-3">
92
- <button class="button is-primary mb-3" id="back-to-summary" onclick="showSummary()">
93
- <span class="icon"><i class="fa fa-chevron-left"></i></span>
94
- <span>Back to Summary</span>
95
- </button>
96
- <div class="card-content">
97
- <div class="content has-text-centered">
98
- <h1 class="title is-2">${test.title}</h1>
99
- <p class="subtitle is-5" id="filepath">${test.location}</p>
100
- </div>
101
- </div>
102
- <footer class="card-footer">
103
- <div class="card-footer-item">
104
- <div class="columns is-mobile">
105
- <div class="column is-half">
106
- <div class="is-flex is-align-items-center">
107
- <span class="icon status-icon has-text-${statusClass}">
108
- <i class="fa fa-${statusIcon}"></i>
109
- </span>
110
- <span class="has-text-weight-bold is-uppercase has-text-${statusClass}">${test.status}</span>
111
- </div>
112
- </div>
113
- </div>
114
- </div>
115
- ${test.duration ? `
116
- <div class="card-footer-item">
117
- <div class="column is-half">
118
- <div class="is-flex is-align-items-center">
119
- <span class="icon status-icon has-text-info">
120
- <i class="fa fa-clock"></i>
121
- </span>
122
- <span class="has-text-info has-text-weight-semibold">${test.duration}</span>
123
- </div>
124
- </div>
125
- </div>
126
- ` : ''}
127
- ${test.projectName ? `
128
- <div class="card-footer-item">
129
- <div class="is-flex is-align-items-center">
130
- <span class="icon status-icon has-text-link">
131
- ${projectIcon}
132
- </span>
133
- <span> ${test.projectName}</span>
134
- </div>
135
- </div>
136
- ` : ''}
137
- </footer>
138
- </div>
139
- </div>
140
- <div class="content-wrapper">
141
- ${this.renderTestContent(test)}
142
- </div>
143
- `;
144
-
145
- this.attachScreenshotListeners(test);
146
- this.attachSteps(test);
147
- },
148
- getStatusClass(status) {
149
- if (status.startsWith('passed')) return 'success';
150
- if (status === 'flaky') return 'warning';
151
- if (status === 'failed') return 'danger';
152
- return 'info';
153
- },
154
- getStatusIcon(status) {
155
- if (status.startsWith('passed')) return 'check-circle';
156
- if (status === 'flaky') return 'exclamation-triangle';
157
- if (status === 'failed') return 'times-circle';
158
- return 'question-circle';
159
- },
160
- getProjectIcon(project) {
161
- if (project === 'webkit') return `<i class="fa-brands fa-safari"></i>`;
162
- if (project === 'firefox') return `<i class="fa-brands fa-firefox"></i>`;
163
- return `<i class="fa-brands fa-chrome"></i>`;
164
- },
165
- renderTestContent(test) {
166
- let content = '';
167
- if (test.status !== "skipped") {
168
- content += this.renderScreenshotsAndVideo(test);
169
- }
170
- content += this.renderAdditionalInfo(test);
171
- content += this.renderSteps(test);
172
- content += this.renderErrors(test);
173
- content += this.renderLogs(test);
174
- return content;
175
- },
176
- renderScreenshotsAndVideo(test) {
177
- let content = '<div class="card mb-5"><div class="card-content"><div class="columns is-multiline">';
178
- if (test.screenshots && test.screenshots.length > 0) {
179
- content += `
180
- <div class="column is-half">
181
- <div id="testImage" class="modal">
182
- <div class="modal-background"></div>
183
- <div class="modal-content">
184
- <p class="image">
185
- <img id="screenshot-modal-img" src="${test.screenshots[0]}" alt="Screenshot">
186
- </p>
187
- </div>
188
- <button onclick="closeModal()" class="modal-close is-large" aria-label="close"></button>
189
- </div>
190
- <figure class="image">
191
- <img id="screenshot-main-img" onclick="openModal()" src="${test.screenshots[0]}" alt="Screenshot">
192
- </figure>
193
- <nav class="mt-4 pagination is-small is-centered ${test.screenshots.length > 1 ? '' : 'is-hidden'}" role="navigation" aria-label="pagination">
194
- <a class="pagination-previous">Previous</a>
195
- <a class="pagination-next">Next</a>
196
- <ul class="pagination-list">
197
- ${test.screenshots.map((_, index) => `
198
- <li>
199
- <a class="pagination-link ${index === 0 ? 'is-current' : ''}" aria-label="Goto screenshot ${index + 1}">${index + 1}</a>
200
- </li>`).join('')}
201
- </ul>
202
- </nav>
203
- </div>
204
- `;
205
- }
206
- if (test.videoPath) {
207
- content += `
208
- <div class="column is-half">
209
- <div class="video-preview">
210
- <video controls width="100%" height="auto" preload="metadata">
211
- <source src="${test.videoPath}" type="video/webm">
212
- Your browser does not support the video tag.
213
- </video>
214
- </div>
215
- </div>
216
- `;
217
- }
218
- content += '</div>';
219
- content += `
220
- <div class="columns">
221
- <div class="column">
222
- <button
223
- onclick="openHistory()"
224
- class="button is-primary is-fullwidth mt-3">
225
- <span class="icon"><i class="fa-solid fa-timeline"></i></span>
226
- <span class="has-text-white pl-2">Open history</span>
227
- </button>
228
- <div id="historyModal" class="modal">
229
- <div class="modal-background"></div>
230
- </div>
231
- </div>
232
- ${test.tracePath ? `
233
- <div class="column">
234
- <button
235
- data-trace="${test.tracePath}"
236
- onclick="openTraceViewer(this)"
237
- class="button is-primary is-fullwidth mt-3">
238
- <span class="icon"><i class="fa-solid fa-tv"></i></span>
239
- <span class="has-text-white pl-2">View Trace</span>
240
- </button>
241
- </div>
242
- ` : ''}
243
- </div>
244
- `;
245
-
246
- content += '</div></div>';
247
- return content;
248
- },
249
- renderAdditionalInfo(test) {
250
- if (!(test.annotations.length || test.testTags.length > 0)) return '';
251
- return `
252
- <div class="card mb-5">
253
- <header class="card-header">
254
- <p class="card-header-title">Additional Information</p>
255
- </header>
256
- <div class="card-content">
257
- <div class="content">
258
- ${test.testTags.length > 0 ? `
259
- <div class="control mb-4">
260
- <div class="tags is-rounded">
261
- ${test.testTags.map(tag => `<span class="tag is-primary is-medium">${tag}</span>`).join('')}
262
- </div>
263
- </div>` : ""}
264
- ${test.annotations
265
- .filter(annotation => annotation !== null && annotation !== undefined)
266
- .map(annotation => `
267
- <div class="mb-4">
268
- ${annotation?.type ? `<strong class="has-text-link">Type: </strong><span>${annotation.type}</span>` : ''}
269
- <br>
270
- ${annotation?.description ? `<strong class="has-text-link">Description: </strong><span>${annotation.description}</span>` : ''}
271
- </div>
272
- `).join('')}
273
- </div>
274
- </div>
275
- </div>
276
- `;
277
- },
278
- renderSteps(test) {
279
- if (test.steps.length === 0) return '';
280
- return `
281
- <div class="card">
282
- <header class="card-header">
283
- <p class="card-header-title">Steps</p>
284
- </header>
285
- <div class="card-content">
286
- <div class="content">
287
- <span id="stepDetails" class="content"></span>
288
- </div>
289
- </div>
290
- </div>
291
- `;
292
- },
293
- renderErrors(test) {
294
- if (!test.errors.length) return '';
295
- return `
296
- <div class="card mt-5">
297
- <header class="card-header">
298
- <p class="card-header-title">Errors</p>
299
- </header>
300
- <div class="card-content">
301
- <div class="content">
302
- <pre><code class="data-lang=js">${test.errors.join('\n')}</code></pre>
303
- </div>
304
- </div>
305
- </div>
306
- `;
307
- },
308
- renderLogs(test) {
309
- if (!test.logs) return '';
310
- return `
311
- <div class="card mt-5">
312
- <header class="card-header">
313
- <p class="card-header-title">Logs</p>
314
- </header>
315
- <div class="card-content">
316
- <div class="content">
317
- <pre>${test.logs}</pre>
318
- </div>
319
- </div>
320
- </div>
321
- `;
322
- },
323
- attachScreenshotListeners(test) {
324
- if (test.screenshots && test.screenshots.length > 0) {
325
- let currentScreenshotIndex = 0;
326
- const changeScreenshot = (direction) => {
327
- currentScreenshotIndex = (currentScreenshotIndex + direction + test.screenshots.length) % test.screenshots.length;
328
- this.updateScreenshot(test.screenshots, currentScreenshotIndex);
329
- };
330
- const gotoScreenshot = (index) => {
331
- currentScreenshotIndex = index;
332
- this.updateScreenshot(test.screenshots, currentScreenshotIndex);
333
- };
334
- document.querySelector('.pagination-previous').addEventListener('click', () => changeScreenshot(-1));
335
- document.querySelector('.pagination-next').addEventListener('click', () => changeScreenshot(1));
336
- document.querySelectorAll('.pagination-link').forEach((link, index) => {
337
- link.addEventListener('click', () => gotoScreenshot(index));
338
- });
339
- }
340
- },
341
- updateScreenshot(screenshots, index) {
342
- document.getElementById('screenshot-main-img').src = screenshots[index];
343
- document.getElementById('screenshot-modal-img').src = screenshots[index];
344
- document.querySelectorAll('.pagination-link').forEach((link, i) => {
345
- link.classList.toggle('is-current', i === index);
346
- });
347
- },
348
- attachSteps(test) {
349
- const stepDetailsDiv = document.getElementById('stepDetails');
350
- if (stepDetailsDiv) {
351
- const stepsList = document.createElement("ul");
352
- stepsList.setAttribute("id", "steps");
353
- test.steps.forEach(step => {
354
- const li = document.createElement('li');
355
- li.innerHTML = `<strong class="${step.snippet ? 'has-text-danger' : ''}">${step.title}</strong>`;
356
- if (step.snippet) {
357
- const pre = document.createElement('pre');
358
- const code = document.createElement('code');
359
- const locationText = step.location ? `\n\nat: ${step.location}` : '';
360
- code.innerHTML = `${step.snippet}${locationText}`;
361
- code.setAttribute('data-lang', 'js');
362
- pre.appendChild(code);
363
- li.appendChild(pre);
364
- }
365
- stepsList.appendChild(li);
366
- });
367
- stepDetailsDiv.appendChild(stepsList);
368
- }
369
- },
370
- attachEventListeners() {
371
- const testItems = document.querySelectorAll('[data-test-id]');
372
- testItems.forEach(item => {
373
- item.addEventListener('click', () => {
374
- testItems.forEach(i => i.classList.remove('listselected'));
375
- item.classList.add('listselected');
376
- const testId = item.getAttribute('data-test-id');
377
- const testHistoryId = item.getAttribute('data-test-history-id');
378
- const test = testData[testId];
379
- const historyEntry = testHistory.find(entry => entry.testId === testHistoryId);
380
- testHistoriesMap = historyEntry ? historyEntry.history : null;
381
- testHistoryTitle = historyEntry.testId ? historyEntry.testId.split(":")[2] : '';
382
- this.show(test);
383
- });
384
- });
385
- }
386
- };
387
-
388
- const filterManager = {
389
- init() {
390
- this.attachEventListeners();
391
- },
392
- attachEventListeners() {
393
- const checkboxes = document.querySelectorAll('#select-filter input[type="checkbox"]');
394
- checkboxes.forEach(checkbox => {
395
- checkbox.addEventListener('change', () => this.applyFilters());
396
- });
397
-
398
- const filters = document.querySelectorAll('.filter');
399
- filters.forEach(filter => {
400
- filter.addEventListener('click', () => {
401
- filters.forEach(f => f.classList.remove('active'));
402
- filter.classList.add('active');
403
- this.applyFilters();
404
- });
405
- });
406
- },
407
- applyFilters() {
408
- const selectedProjects = this.getSelectedValues('project');
409
- const selectedTags = this.getSelectedValues('test-tags');
410
- const selectedStatus = document.querySelector('.filter.active')?.getAttribute('data-status') || 'all';
411
-
412
- elements.detailsElements.forEach(details => {
413
- const items = details.querySelectorAll('div[data-test-id]');
414
- let shouldShowDetails = false;
415
-
416
- items.forEach(item => {
417
- const isVisible = this.shouldShowItem(item, selectedProjects, selectedTags, selectedStatus);
418
- item.classList.toggle('is-hidden', !isVisible);
419
- shouldShowDetails = shouldShowDetails || isVisible;
420
- });
421
-
422
- details.open = shouldShowDetails;
423
- details.classList.toggle('is-hidden', !shouldShowDetails);
424
- });
425
-
426
- this.updateSelectedFiltersDisplay(selectedProjects, selectedTags, selectedStatus);
427
- },
428
- getSelectedValues(type) {
429
- return Array.from(document.querySelectorAll(`#select-filter input[type="checkbox"][data-filter-type="${type}"]:checked`))
430
- .map(checkbox => checkbox.value.trim());
431
- },
432
- shouldShowItem(item, projects, tags, status) {
433
- const testTags = item.getAttribute('data-test-tags').trim().split(' ').filter(Boolean);
434
- const projectName = item.getAttribute('data-project-name').trim();
435
- const testStatus = item.getAttribute('data-test-status').trim();
436
-
437
- const matchesProject = projects.length === 0 || projects.includes(projectName);
438
- const matchesTags = tags.length === 0 || tags.every(tag => testTags.includes(tag));
439
- const matchesStatus = this.matchesStatus(testStatus, status);
440
-
441
- return matchesProject && matchesTags && matchesStatus;
442
- },
443
- matchesStatus(testStatus, selectedStatus) {
444
- if (selectedStatus === 'all') return testStatus !== 'skipped';
445
- if (selectedStatus === 'failed') return testStatus === 'failed' || testStatus === 'timedOut';
446
- if (selectedStatus === 'retry') return testStatus.includes('retry');
447
- if (selectedStatus === 'flaky') return testStatus.includes('flaky');
448
- return testStatus === selectedStatus;
449
- },
450
- updateSelectedFiltersDisplay(projects, tags, status) {
451
- let displayText = [];
452
- if (projects.length > 0) displayText.push(`Projects: ${projects.join(', ')}`);
453
- if (tags.length > 0) displayText.push(`Tags: ${tags.join(', ')}`);
454
- if (status !== 'all') displayText.push(`Status: ${status}`);
455
- elements.filtersDisplay.innerHTML = displayText.length > 0 ? displayText.join(' | ') : 'All Tests';
456
- }
457
- };
458
-
459
- const searchManager = {
460
- init() {
461
- elements.searchInput.addEventListener('input', this.debounce(this.filterTests, 300));
462
- },
463
- filterTests(event) {
464
- const searchTerm = event.target.value.toLowerCase();
465
- const testItems = document.querySelectorAll('[data-test-id]');
466
-
467
- elements.detailsElements.forEach(detail => detail.open = !!searchTerm);
468
-
469
- testItems.forEach(item => {
470
- const isVisible = item.textContent.toLowerCase().includes(searchTerm);
471
- item.style.display = isVisible ? 'flex' : 'none';
472
- if (isVisible && searchTerm) searchManager.openParentDetails(item);
473
- });
474
- },
475
- openParentDetails(item) {
476
- let parent = item.parentElement;
477
- while (parent && parent.tagName !== 'ASIDE') {
478
- if (parent.tagName === 'DETAILS') parent.open = true;
479
- parent = parent.parentElement;
480
- }
481
- },
482
- debounce(func, wait) {
483
- let timeout;
484
- return function(...args) {
485
- clearTimeout(timeout);
486
- timeout = setTimeout(() => func.apply(this, args), wait);
487
- };
488
- }
489
- };
490
-
491
- // Initialize all managers
492
- themeManager.init();
493
- testDetailsManager.attachEventListeners();
494
- filterManager.init();
495
- searchManager.init();
496
-
497
- // Expose necessary functions to the global scope
498
- window.showSummary = testDetailsManager.hide;
499
- window.openModal = () => document.querySelector("#testImage").classList.add("is-active");
500
- window.closeModal = () => document.querySelector("#testImage").classList.remove("is-active");
501
- window.closeErrorModal = (modalId) => document.getElementById(modalId).classList.remove("is-active");
502
- window.openTraceViewer = (button) => {
503
- const tracePath = button.getAttribute("data-trace");
504
- if (tracePath) {
505
- const normalizedTracePath = tracePath.replace(/\\/g, '/');
506
- const baseUrl = getAdjustedBaseUrl();
507
- window.open(`${baseUrl}/trace/index.html?trace=${baseUrl}/${normalizedTracePath}`, "_blank");
508
- }
509
- };
510
- window.getAdjustedBaseUrl = () => {
511
- const origin = window.location.origin;
512
- const pathname = window.location.pathname;
513
- if (pathname.endsWith('.html')) {
514
- const directoryPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
515
- return `${origin}${directoryPath}`;
516
- }
517
- return origin;
518
- }
519
- window.openHistory = () => {
520
- const historyElement = document.getElementById("historyModal");
521
- historyElement.classList.add('is-active');
522
-
523
- let historyContent = '';
524
- if (testHistoriesMap && testHistoriesMap.length > 0) {
525
- historyContent = testHistoriesMap.map((h, index) => `
526
- <tr>
527
- <td>${h.run_date}</td>
528
- <td>${h.status}</td>
529
- <td>${h.duration}</td>
530
- ${h.error_message ? `<td><div class="modal" id="${index}">
531
- <div class="modal-background"></div>
532
- <div class="modal-content">
533
- <pre><code>${h.error_message}</code></pre>
534
- </div>
535
- <button class="button is-primary" onclick="closeErrorModal(${index})">Close</button>
536
- </div><a class="button is-link" onclick="showHistoryErrorMessage(${index})">Show error</a></td>` : '<td>No Error</td>'}
537
- </tr>
538
- `).join('');
539
- } else {
540
- historyContent = '<p class="title">No history available</p>';
541
- }
542
-
543
- historyElement.innerHTML = `
544
- <div class="modal-background"></div>
545
- <div class="modal-card">
546
- <header class="modal-card-head">
547
- <p class="modal-card-title">${testHistoryTitle}</p>
548
- <button class="button is-primary" onclick="closeHistoryModal()">Close</button>
549
- </header>
550
- <section class="modal-card-body">
551
- <table class="table is-hoverable is-fullwidth">
552
- <thead>
553
- <tr>
554
- <th title="Run Date">Run Date</th>
555
- <th title="Status">Status</th>
556
- <th title="Duration">Duration</th>
557
- <th title="Reason">Reason</th>
558
- </tr>
559
- </thead>
560
- <tbody>
561
- ${historyContent}
562
- </tbody>
563
- </table>
564
- </section>
565
- </div>
566
- `;
567
- };
568
- window.closeHistoryModal = () => {
569
- document.getElementById("historyModal").classList.remove('is-active');
570
- };
571
- window.showHistoryErrorMessage = (modalId) => {
572
- document.getElementById(modalId)?.classList.add('is-active');
573
- };
574
- });
575
- </script>
576
- </body>
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="{{preferredTheme}}">
3
+ {{> head}}
4
+ <style>
5
+ {{{inlineCss}}}
6
+
7
+ </style>
8
+
9
+ <body>
10
+ <div class="app-container">
11
+ {{> sidebar}}
12
+ <main class="main-content">
13
+ <div id="dashboard-section" class="content-section">
14
+ <h1 class="title is-3">Dashboard</h1>
15
+ <div class="columns is-multiline has-text-centered">
16
+ {{> summaryCard bg="hsl(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l)) !important" status="all" statusHeader="All Tests" statusCount=totalCount}}
17
+ {{> summaryCard bg="hsl(var(--bulma-success-h), var(--bulma-success-s), var(--bulma-success-l)) !important" status="passed" statusHeader="Passed" statusCount=passCount}}
18
+ {{> summaryCard bg="hsl(var(--bulma-danger-h), var(--bulma-danger-s), var(--bulma-danger-l)) !important" status="failed" statusHeader="Failed" statusCount=failCount}}
19
+ {{> summaryCard bg="hsl(var(--bulma-info-h), var(--bulma-info-s), var(--bulma-info-l)) !important" status="skipped" statusHeader="Skipped" statusCount=skipCount}}
20
+ {{> summaryCard bg="hsl(var(--bulma-warning-h), var(--bulma-warning-s), var(--bulma-warning-l)) !important" status="flaky" statusHeader="Flaky" statusCount=flakyCount}}
21
+ {{> summaryCard bg="#69748c" status="retry" statusHeader="Retry" statusCount=retryCount}}
22
+ </div>
23
+ {{> userInfo}}
24
+ </div>
25
+ <div id="tests-section" class="content-section" style="display: none;">
26
+ <div class="test-list">
27
+ <div class="columns">
28
+ <div class="column column is-two-fifths">
29
+ <h1 class="title is-3">Tests</h1>
30
+ <div class="columns is-multiline has-text-centered">
31
+ {{> summaryCard icon="fa-solid fa-vial" filter="filter" bg="hsl(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l)) !important" status="all" statusHeader="All Tests" statusCount=totalCount}}
32
+ {{> summaryCard icon="fa fa-check-circle" filter="filter" bg="hsl(var(--bulma-success-h), var(--bulma-success-s), var(--bulma-success-l)) !important" status="passed" statusHeader="Passed" statusCount=passCount}}
33
+ {{> summaryCard icon="fa fa-times-circle" filter="filter" bg="hsl(var(--bulma-danger-h), var(--bulma-danger-s), var(--bulma-danger-l)) !important" status="failed" statusHeader="Failed" statusCount=failCount}}
34
+ {{> summaryCard icon="fa fa-question-circle" filter="filter" bg="hsl(var(--bulma-info-h), var(--bulma-info-s), var(--bulma-info-l)) !important" status="skipped" statusHeader="Skipped" statusCount=skipCount}}
35
+ {{> summaryCard icon="fa fa-exclamation-triangle" filter="filter" bg="hsl(var(--bulma-warning-h), var(--bulma-warning-s), var(--bulma-warning-l)) !important" status="flaky" statusHeader="Flaky" statusCount=flakyCount}}
36
+ {{> summaryCard icon="fa-solid fa-repeat" filter="filter" bg="#69748c" status="retry" statusHeader="Retry" statusCount=retryCount}}
37
+ </div>
38
+ {{> testPanel}}
39
+ {{> project}}
40
+ </div>
41
+ <div class="column column is-three-fifths">
42
+ <div id="testDetails" style="display: none;"></div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </main>
48
+ </div>
49
+ <script>
50
+ document.addEventListener('DOMContentLoaded', () => {
51
+ /**
52
+ * ====================================
53
+ * UTILITY FUNCTIONS
54
+ * ====================================
55
+ */
56
+ const Utils = {
57
+ /**
58
+ * Debounce function to limit how often a function can be called
59
+ * @param {Function} func - Function to debounce
60
+ * @param {number} wait - Wait time in milliseconds
61
+ * @returns {Function} Debounced function
62
+ */
63
+ debounce(func, wait) {
64
+ let timeout;
65
+ return function (...args) {
66
+ clearTimeout(timeout);
67
+ timeout = setTimeout(() => func.apply(this, args), wait);
68
+ };
69
+ },
70
+
71
+ /**
72
+ * Get status class based on test status
73
+ * @param {string} status - Test status
74
+ * @returns {string} CSS class
75
+ */
76
+ getStatusClass(status) {
77
+ if (status.startsWith('passed')) return 'success';
78
+ if (status === 'flaky') return 'warning';
79
+ if (status === 'failed') return 'danger';
80
+ return 'info';
81
+ },
82
+
83
+ /**
84
+ * Get status icon based on test status
85
+ * @param {string} status - Test status
86
+ * @returns {string} Icon class
87
+ */
88
+ getStatusIcon(status) {
89
+ if (status.startsWith('passed')) return 'check-circle';
90
+ if (status === 'flaky') return 'exclamation-triangle';
91
+ if (status === 'failed') return 'times-circle';
92
+ return 'question-circle';
93
+ },
94
+
95
+ /**
96
+ * Get project icon based on browser
97
+ * @param {string} project - Project name
98
+ * @returns {string} Icon HTML
99
+ */
100
+ getProjectIcon(project) {
101
+ if (project === 'webkit') return `<i class="fa-brands fa-safari"></i>`;
102
+ if (project === 'firefox') return `<i class="fa-brands fa-firefox"></i>`;
103
+ return `<i class="fa-brands fa-chrome"></i>`;
104
+ },
105
+
106
+ /**
107
+ * Get adjusted base URL for trace viewer
108
+ * @returns {string} Adjusted base URL
109
+ */
110
+ getAdjustedBaseUrl() {
111
+ const origin = window.location.origin;
112
+ const pathname = window.location.pathname;
113
+ if (pathname.endsWith('.html')) {
114
+ const directoryPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
115
+ return `${origin}${directoryPath}`;
116
+ }
117
+ return origin;
118
+ }
119
+ };
120
+
121
+ /**
122
+ * ====================================
123
+ * SIDEBAR MANAGER
124
+ * ====================================
125
+ */
126
+ class SidebarManager {
127
+ /**
128
+ * Initialize the sidebar manager
129
+ */
130
+ constructor() {
131
+ this.sidebarLinks = document.querySelectorAll('.sidebar-menu-link');
132
+ this.sections = document.querySelectorAll('.content-section');
133
+ this.testDetailsSection = document.getElementById('testDetails');
134
+ this.sidebar = document.getElementById('sidebar');
135
+ this.mainContent = document.querySelector('.main-content');
136
+ this.init();
137
+ }
138
+
139
+ /**
140
+ * Initialize sidebar manager
141
+ */
142
+ init() {
143
+ this.attachEventListeners();
144
+ }
145
+
146
+ /**
147
+ * Attach event listeners
148
+ */
149
+ attachEventListeners() {
150
+ this.sidebarLinks.forEach(link => {
151
+ link.addEventListener('click', (e) => {
152
+ e.preventDefault();
153
+ this.setActiveLink(link);
154
+ this.showSection(link.getAttribute('data-section'));
155
+ });
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Set active link
161
+ * @param {HTMLElement} activeLink - Active link
162
+ */
163
+ setActiveLink(activeLink) {
164
+ this.sidebarLinks.forEach(link => {
165
+ link.classList.remove('active');
166
+ });
167
+ activeLink.classList.add('active');
168
+ }
169
+
170
+ /**
171
+ * Show section
172
+ * @param {string} sectionId - Section ID
173
+ */
174
+ showSection(sectionId) {
175
+ // Hide test details if showing a main section
176
+ this.testDetailsSection.style.display = 'none';
177
+
178
+ // Show the selected section
179
+ this.sections.forEach(section => {
180
+ section.style.display = 'none';
181
+ });
182
+ document.getElementById(`${sectionId}-section`).style.display = 'block';
183
+ }
184
+
185
+ /**
186
+ * Show test details
187
+ */
188
+ showTestDetails() {
189
+ /*
190
+ this.sections.forEach(section => {
191
+ section.style.display = 'none';
192
+ });
193
+ */
194
+ this.testDetailsSection.style.display = 'block';
195
+ }
196
+ }
197
+
198
+ /* Add this new class to the JavaScript section, after the SidebarManager class */
199
+ /**
200
+ * ====================================
201
+ * SIDEBAR COLLAPSE MANAGER
202
+ * ====================================
203
+ */
204
+ class SidebarCollapseManager {
205
+ /**
206
+ * Initialize the sidebar collapse manager
207
+ */
208
+ constructor() {
209
+ this.sidebar = document.getElementById('sidebar');
210
+ this.mainContent = document.querySelector('.main-content');
211
+ this.toggleButton = document.getElementById('sidebar-toggle');
212
+ this.toggleIcon = document.getElementById('sidebar-toggle-icon');
213
+ this.isInitialLoad = true;
214
+ this.init();
215
+ }
216
+
217
+ /**
218
+ * Initialize sidebar collapse manager
219
+ */
220
+ init() {
221
+ // Only add the click event listener once
222
+ this.toggleButton.addEventListener('click', (e) => {
223
+ e.preventDefault();
224
+ e.stopPropagation();
225
+ this.toggleSidebar();
226
+ });
227
+
228
+ // Check for saved state, but only apply it on initial load
229
+ if (this.isInitialLoad) {
230
+ const sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
231
+ if (sidebarCollapsed) {
232
+ this.collapseSidebar(false);
233
+ }
234
+ this.isInitialLoad = false;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Toggle sidebar collapsed state
240
+ */
241
+ toggleSidebar() {
242
+ if (this.sidebar.classList.contains('collapsed')) {
243
+ this.expandSidebar(true);
244
+ } else {
245
+ this.collapseSidebar(true);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Collapse sidebar
251
+ * @param {boolean} saveState - Whether to save state to localStorage
252
+ */
253
+ collapseSidebar(saveState = true) {
254
+ this.sidebar.classList.add('collapsed');
255
+ this.mainContent.classList.add('expanded');
256
+ this.toggleIcon.classList.remove('fa-chevron-left');
257
+ this.toggleIcon.classList.add('fa-chevron-right');
258
+ if (saveState) {
259
+ localStorage.setItem('sidebarCollapsed', 'true');
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Expand sidebar
265
+ * @param {boolean} saveState - Whether to save state to localStorage
266
+ */
267
+ expandSidebar(saveState = true) {
268
+ this.sidebar.classList.remove('collapsed');
269
+ this.mainContent.classList.remove('expanded');
270
+ this.toggleIcon.classList.remove('fa-chevron-right');
271
+ this.toggleIcon.classList.add('fa-chevron-left');
272
+ if (saveState) {
273
+ localStorage.setItem('sidebarCollapsed', 'false');
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * ====================================
280
+ * THEME MANAGER
281
+ * ====================================
282
+ */
283
+ class ThemeManager {
284
+ /**
285
+ * Initialize the theme manager
286
+ * @param {Object} elements - DOM elements
287
+ */
288
+ constructor(elements) {
289
+ this.elements = elements;
290
+ this.init();
291
+ }
292
+
293
+ /**
294
+ * Initialize theme manager
295
+ */
296
+ init() {
297
+ const preferredTheme = this.elements.themeButton.getAttribute("data-theme-status");
298
+ this.setTheme(preferredTheme);
299
+ this.elements.themeButton.addEventListener('click', () => this.toggleTheme());
300
+ }
301
+
302
+ /**
303
+ * Set theme (light or dark)
304
+ * @param {string} theme - Theme name
305
+ */
306
+ setTheme(theme) {
307
+ this.elements.htmlElement.setAttribute('data-theme', theme);
308
+ this.elements.themeIcon.className = `fa fa-${theme === 'dark' ? 'moon' : 'sun'}`;
309
+ }
310
+
311
+ /**
312
+ * Toggle between light and dark theme
313
+ */
314
+ toggleTheme() {
315
+ const currentTheme = this.elements.htmlElement.getAttribute('data-theme');
316
+ this.setTheme(currentTheme === 'light' ? 'dark' : 'light');
317
+ }
318
+ }
319
+
320
+ /**
321
+ * ====================================
322
+ * TEST DETAILS MANAGER
323
+ * ====================================
324
+ */
325
+ class TestDetailsManager {
326
+ /**
327
+ * Initialize the test details manager
328
+ * @param {Object} elements - DOM elements
329
+ * @param {Object} testData - Test data
330
+ * @param {Array} testHistory - Test history data
331
+ * @param {SidebarManager} sidebarManager - Sidebar manager
332
+ */
333
+ constructor(elements, testData, testHistory, sidebarManager) {
334
+ this.elements = elements;
335
+ this.testData = testData;
336
+ this.testHistory = testHistory;
337
+ this.sidebarManager = sidebarManager;
338
+ this.testHistoriesMap = null;
339
+ this.testHistoryTitle = '';
340
+ this.currentScreenshotIndex = 0;
341
+ }
342
+
343
+ /**
344
+ * Show test details
345
+ * @param {Object} test - Test data
346
+ */
347
+ show(test) {
348
+ this.sidebarManager.showTestDetails();
349
+ this.elements.testDetails.style.opacity = '0';
350
+ setTimeout(() => {
351
+ this.elements.testDetails.style.opacity = '1';
352
+ }, 50);
353
+ this.render(test);
354
+ }
355
+
356
+ /**
357
+ * Hide test details and show dashboard
358
+ */
359
+ hide() {
360
+ // Find the dashboard link and click it
361
+ document.querySelector('.sidebar-menu-link[data-section="dashboard"]').click();
362
+ }
363
+
364
+ /**
365
+ * Render test details
366
+ * @param {Object} test - Test data
367
+ */
368
+ render(test) {
369
+ this.currentScreenshotIndex = 0;
370
+ const statusClass = Utils.getStatusClass(test.status);
371
+ const statusIcon = Utils.getStatusIcon(test.status);
372
+ const projectIcon = Utils.getProjectIcon(test.projectName);
373
+
374
+ this.elements.testDetails.innerHTML = `
375
+ <div class="sticky-header">
376
+ <div class="card mb-3">
377
+ <div class="card-content">
378
+ <div class="content has-text-centered">
379
+ <h1 class="title is-4">${test.title}</h1>
380
+ <p class="subtitle is-5" id="filepath">${test.location}</p>
381
+ </div>
382
+ </div>
383
+ <footer class="card-footer">
384
+ <div class="card-footer-item">
385
+ <div class="columns is-mobile">
386
+ <div class="column is-half">
387
+ <div class="is-flex is-align-items-center">
388
+ <span class="icon status-icon has-text-${statusClass}">
389
+ <i class="fa fa-${statusIcon}"></i>
390
+ </span>
391
+ <span class="has-text-weight-bold is-capitalized has-text-${statusClass}">${test.status}</span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ ${test.duration ? `
397
+ <div class="card-footer-item">
398
+ <div class="column is-half">
399
+ <div class="is-flex is-align-items-center">
400
+ <span class="icon status-icon has-text-info">
401
+ <i class="fa fa-clock"></i>
402
+ </span>
403
+ <span class="has-text-info has-text-weight-semibold">${test.duration}</span>
404
+ </div>
405
+ </div>
406
+ </div>
407
+ ` : ''}
408
+ ${test.projectName ? `
409
+ <div class="card-footer-item">
410
+ <div class="is-flex is-align-items-center">
411
+ <span class="icon status-icon has-text-link">
412
+ ${projectIcon}
413
+ </span>
414
+ <span> ${test.projectName}</span>
415
+ </div>
416
+ </div>
417
+ ` : ''}
418
+ </footer>
419
+ </div>
420
+ </div>
421
+ <div class="content-wrapper">
422
+ ${this.renderTestContent(test)}
423
+ </div>
424
+ `;
425
+ this.attachScreenshotListeners(test);
426
+ this.attachSteps(test);
427
+ this.attachTabListeners();
428
+ }
429
+
430
+ /**
431
+ * Render test content
432
+ * @param {Object} test - Test data
433
+ * @returns {string} HTML content
434
+ */
435
+ renderTestContent(test) {
436
+ let content = '';
437
+ if (test.status !== "skipped") {
438
+ content += this.renderScreenshotsAndVideo(test);
439
+ }
440
+ content += this.renderAdditionalInfo(test);
441
+ content += this.renderTabs(test);
442
+ return content;
443
+ }
444
+
445
+ /**
446
+ * Render screenshots and video
447
+ * @param {Object} test - Test data
448
+ * @returns {string} HTML content
449
+ */
450
+ renderScreenshotsAndVideo(test) {
451
+ let content = '<div class="card mb-5"><div class="card-content"><div class="columns is-multiline">';
452
+ if (test.screenshots && test.screenshots.length > 0) {
453
+ content += `
454
+ <div class="column is-half">
455
+ <div id="testImage" class="modal">
456
+ <div class="modal-background"></div>
457
+ <div class="modal-content">
458
+ <p class="image">
459
+ <img id="screenshot-modal-img" src="${test.screenshots[0]}" alt="Screenshot">
460
+ </p>
461
+ </div>
462
+ <button onclick="closeModal()" class="modal-close is-large" aria-label="close"></button>
463
+ </div>
464
+ <figure class="image">
465
+ <img id="screenshot-main-img" onclick="openModal()" src="${test.screenshots[0]}" alt="Screenshot">
466
+ </figure>
467
+ <nav class="mt-4 pagination is-small is-centered ${test.screenshots.length > 1 ? '' : 'is-hidden'}" role="navigation" aria-label="pagination">
468
+ <a class="pagination-previous">Previous</a>
469
+ <a class="pagination-next">Next</a>
470
+ <ul class="pagination-list">
471
+ ${test.screenshots.map((_, index) => `
472
+ <li>
473
+ <a class="pagination-link ${index === 0 ? 'is-current' : ''}" aria-label="Goto screenshot ${index + 1}">${index + 1}</a>
474
+ </li>`).join('')}
475
+ </ul>
476
+ </nav>
477
+ </div>
478
+ `;
479
+ }
480
+ if (test.videoPath) {
481
+ content += `
482
+ <div class="column is-half">
483
+ <div class="video-preview">
484
+ <video controls width="100%" height="auto" preload="metadata">
485
+ <source src="${test.videoPath}" type="video/webm">
486
+ Your browser does not support the video tag.
487
+ </video>
488
+ </div>
489
+ </div>
490
+ `;
491
+ }
492
+ content += '</div>';
493
+ content += `
494
+ <div class="columns">
495
+ <div class="column">
496
+ <button
497
+ onclick="openHistory()"
498
+ class="button is-primary is-fullwidth mt-3">
499
+ <span class="icon"><i class="fa-solid fa-timeline"></i></span>
500
+ <span class="has-text-white pl-2">Open history</span>
501
+ </button>
502
+ <div id="historyModal" class="modal">
503
+ <div class="modal-background"></div>
504
+ </div>
505
+ </div>
506
+ ${test.tracePath ? `
507
+ <div class="column">
508
+ <button
509
+ data-trace="${test.tracePath}"
510
+ onclick="openTraceViewer(this)"
511
+ class="button is-primary is-fullwidth mt-3">
512
+ <span class="icon"><i class="fa-solid fa-tv"></i></span>
513
+ <span class="has-text-white pl-2">View Trace</span>
514
+ </button>
515
+ </div>
516
+ ` : ''}
517
+ </div>
518
+ `;
519
+
520
+ content += '</div></div>';
521
+ return content;
522
+ }
523
+
524
+ /**
525
+ * Render additional information
526
+ * @param {Object} test - Test data
527
+ * @returns {string} HTML content
528
+ */
529
+ renderAdditionalInfo(test) {
530
+ if (!(test.annotations.length || test.testTags.length > 0)) return '';
531
+ return `
532
+ <div class="card mb-5">
533
+ <header class="card-header">
534
+ <p class="card-header-title">Additional Information</p>
535
+ </header>
536
+ <div class="card-content">
537
+ <div class="content">
538
+ ${test.testTags.length > 0 ? `
539
+ <div class="control mb-4">
540
+ <div class="tags is-rounded">
541
+ ${test.testTags.map(tag => `<span class="tag is-primary is-medium">${tag}</span>`).join('')}
542
+ </div>
543
+ </div>` : ""}
544
+ ${test.annotations
545
+ .filter(annotation => annotation !== null && annotation !== undefined)
546
+ .map(annotation => `
547
+ <div class="mb-4">
548
+ ${annotation?.type ? `<strong class="has-text-link">Type: </strong><span>${annotation.type}</span>` : ''}
549
+ <br>
550
+ ${annotation?.description ? `<strong class="has-text-link">Description: </strong><span>${annotation.description}</span>` : ''}
551
+ </div>
552
+ `).join('')}
553
+ </div>
554
+ </div>
555
+ </div>
556
+ `;
557
+ }
558
+
559
+ /**
560
+ * Render tabs
561
+ * @param {Object} test - Test data
562
+ * @returns {string} HTML content
563
+ */
564
+ renderTabs(test) {
565
+ return `
566
+ <div class="card mt-5">
567
+ <div class="card-content">
568
+ <div class="tabs is-boxed is-fullwidth">
569
+ <ul>
570
+ <li class="is-active"><a data-tab="steps">Steps</a></li>
571
+ <li><a data-tab="errors">Errors</a></li>
572
+ <li><a data-tab="logs">Logs</a></li>
573
+ </ul>
574
+ </div>
575
+ <div id="tabContent">
576
+ <div id="stepsTab" class="tab-content">
577
+ ${this.renderSteps(test)}
578
+ </div>
579
+ <div id="errorsTab" class="tab-content" style="display: none;">
580
+ ${this.renderErrors(test)}
581
+ </div>
582
+ <div id="logsTab" class="tab-content" style="display: none;">
583
+ ${this.renderLogs(test)}
584
+ </div>
585
+ </div>
586
+ </div>
587
+ </div>
588
+ `;
589
+ }
590
+
591
+ /**
592
+ * Render steps
593
+ * @param {Object} test - Test data
594
+ * @returns {string} HTML content
595
+ */
596
+ renderSteps(test) {
597
+ if (test.steps.length === 0) return '<p>No steps available.</p>';
598
+ return `
599
+ <div class="content">
600
+ <span id="stepDetails" class="content"></span>
601
+ </div>
602
+ `;
603
+ }
604
+
605
+ /**
606
+ * Render errors
607
+ * @param {Object} test - Test data
608
+ * @returns {string} HTML content
609
+ */
610
+ renderErrors(test) {
611
+ if (!test.errors.length) return '<p>No errors reported.</p>';
612
+ return `
613
+ <div class="content">
614
+ <pre><code class="data-lang=js">${test.errors.join('\n')}</code></pre>
615
+ </div>
616
+ `;
617
+ }
618
+
619
+ /**
620
+ * Render logs
621
+ * @param {Object} test - Test data
622
+ * @returns {string} HTML content
623
+ */
624
+ renderLogs(test) {
625
+ if (!test.logs) return '<p>No logs available.</p>';
626
+ return `
627
+ <div class="content">
628
+ <pre>${test.logs}</pre>
629
+ </div>
630
+ `;
631
+ }
632
+
633
+ /**
634
+ * Attach screenshot listeners
635
+ * @param {Object} test - Test data
636
+ */
637
+ attachScreenshotListeners(test) {
638
+ if (test.screenshots && test.screenshots.length > 0) {
639
+ const changeScreenshot = (direction) => {
640
+ this.currentScreenshotIndex = (this.currentScreenshotIndex + direction + test.screenshots.length) % test.screenshots.length;
641
+ this.updateScreenshot(test.screenshots, this.currentScreenshotIndex);
642
+ };
643
+ const gotoScreenshot = (index) => {
644
+ this.currentScreenshotIndex = index;
645
+ this.updateScreenshot(test.screenshots, this.currentScreenshotIndex);
646
+ };
647
+ document.querySelector('.pagination-previous').addEventListener('click', () => changeScreenshot(-1));
648
+ document.querySelector('.pagination-next').addEventListener('click', () => changeScreenshot(1));
649
+ document.querySelectorAll('.pagination-link').forEach((link, index) => {
650
+ link.addEventListener('click', () => gotoScreenshot(index));
651
+ });
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Update screenshot
657
+ * @param {Array} screenshots - Screenshot paths
658
+ * @param {number} index - Screenshot index
659
+ */
660
+ updateScreenshot(screenshots, index) {
661
+ document.getElementById('screenshot-main-img').src = screenshots[index];
662
+ document.getElementById('screenshot-modal-img').src = screenshots[index];
663
+ document.querySelectorAll('.pagination-link').forEach((link, i) => {
664
+ link.classList.toggle('is-current', i === index);
665
+ });
666
+ }
667
+
668
+ /**
669
+ * Attach steps
670
+ * @param {Object} test - Test data
671
+ */
672
+ attachSteps(test) {
673
+ const stepDetailsDiv = document.getElementById('stepDetails');
674
+ if (stepDetailsDiv) {
675
+ const stepsList = document.createElement("ul");
676
+ stepsList.setAttribute("id", "steps");
677
+ test.steps.forEach(step => {
678
+ const li = document.createElement('li');
679
+ li.innerHTML = `<strong class="${step.snippet ? 'has-text-danger' : ''}">${step.title}</strong>`;
680
+ if (step.snippet) {
681
+ const pre = document.createElement('pre');
682
+ const code = document.createElement('code');
683
+ const locationText = step.location ? `\n\nat: ${step.location}` : '';
684
+ code.innerHTML = `${step.snippet}${locationText}`;
685
+ code.setAttribute('data-lang', 'js');
686
+ pre.appendChild(code);
687
+ li.appendChild(pre);
688
+ }
689
+ stepsList.appendChild(li);
690
+ });
691
+ stepDetailsDiv.appendChild(stepsList);
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Attach tab listeners
697
+ */
698
+ attachTabListeners() {
699
+ const tabLinks = document.querySelectorAll('div.tabs a');
700
+ tabLinks.forEach(link => {
701
+ link.addEventListener('click', (e) => {
702
+ e.preventDefault();
703
+ const tabId = link.getAttribute('data-tab');
704
+
705
+ tabLinks.forEach(l => l.parentElement.classList.remove('is-active'));
706
+ document.querySelectorAll('div.tab-content').forEach(c => c.style.display = 'none');
707
+
708
+ link.parentElement.classList.add('is-active');
709
+ document.getElementById(`${tabId}Tab`).style.display = 'block';
710
+ });
711
+ });
712
+ }
713
+
714
+ /**
715
+ * Attach event listeners
716
+ */
717
+ attachEventListeners() {
718
+ const testItems = document.querySelectorAll('[data-test-id]');
719
+ testItems.forEach(item => {
720
+ item.addEventListener('click', () => {
721
+ testItems.forEach(i => i.classList.remove('listselected'));
722
+ item.classList.add('listselected');
723
+ const testId = item.getAttribute('data-test-id');
724
+ const testHistoryId = item.getAttribute('data-test-history-id');
725
+ const test = this.testData[testId];
726
+ const historyEntry = this.testHistory.find(entry => entry.testId === testHistoryId);
727
+ this.testHistoriesMap = historyEntry ? historyEntry.history : null;
728
+ this.testHistoryTitle = historyEntry && historyEntry.testId ? historyEntry.testId.split(":")[2] : '';
729
+ this.show(test);
730
+ });
731
+ });
732
+ }
733
+ }
734
+
735
+ /**
736
+ * ====================================
737
+ * FILTER MANAGER
738
+ * ====================================
739
+ */
740
+ class FilterManager {
741
+ /**
742
+ * Initialize the filter manager
743
+ * @param {Object} elements - DOM elements
744
+ */
745
+ constructor(elements) {
746
+ this.elements = elements;
747
+ this.init();
748
+ }
749
+
750
+ /**
751
+ * Initialize filter manager
752
+ */
753
+ init() {
754
+ this.attachEventListeners();
755
+ }
756
+
757
+ /**
758
+ * Attach event listeners
759
+ */
760
+ attachEventListeners() {
761
+ const checkboxes = document.querySelectorAll('#select-filter input[type="checkbox"]');
762
+ checkboxes.forEach(checkbox => {
763
+ checkbox.addEventListener('change', () => this.applyFilters());
764
+ });
765
+
766
+ const filters = document.querySelectorAll('.filter');
767
+ filters.forEach(filter => {
768
+ filter.addEventListener('click', (event) => {
769
+ filters.forEach(f => {
770
+ f.classList.remove('active');
771
+ f.style.borderRight = ''; // Reset previous border-right
772
+ });
773
+ const clickedFilter = event.currentTarget;
774
+ clickedFilter.classList.add('active');
775
+ const borderLeftColor = window.getComputedStyle(clickedFilter).borderLeftColor;
776
+ clickedFilter.style.setProperty('border-right', `5px solid ${borderLeftColor}`, 'important');
777
+ this.applyFilters();
778
+ });
779
+ });
780
+ }
781
+
782
+ /**
783
+ * Apply filters
784
+ */
785
+ applyFilters() {
786
+ const selectedProjects = this.getSelectedValues('project');
787
+ const selectedTags = this.getSelectedValues('test-tags');
788
+ const selectedStatus = document.querySelector('.filter.active')?.getAttribute('data-status') || 'all';
789
+
790
+ this.elements.detailsElements.forEach(details => {
791
+ const items = details.querySelectorAll('div[data-test-id]');
792
+ let shouldShowDetails = false;
793
+
794
+ items.forEach(item => {
795
+ const isVisible = this.shouldShowItem(item, selectedProjects, selectedTags, selectedStatus);
796
+ item.classList.toggle('is-hidden', !isVisible);
797
+ shouldShowDetails = shouldShowDetails || isVisible;
798
+ });
799
+
800
+ details.open = shouldShowDetails;
801
+ details.classList.toggle('is-hidden', !shouldShowDetails);
802
+ });
803
+
804
+ this.updateSelectedFiltersDisplay(selectedProjects, selectedTags, selectedStatus);
805
+ }
806
+
807
+ /**
808
+ * Get selected values
809
+ * @param {string} type - Filter type
810
+ * @returns {Array} Selected values
811
+ */
812
+ getSelectedValues(type) {
813
+ return Array.from(document.querySelectorAll(`#select-filter input[type="checkbox"][data-filter-type="${type}"]:checked`))
814
+ .map(checkbox => checkbox.value.trim());
815
+ }
816
+
817
+ /**
818
+ * Check if item should be shown
819
+ * @param {HTMLElement} item - Test item
820
+ * @param {Array} projects - Selected projects
821
+ * @param {Array} tags - Selected tags
822
+ * @param {string} status - Selected status
823
+ * @returns {boolean} Whether item should be shown
824
+ */
825
+ shouldShowItem(item, projects, tags, status) {
826
+ const testTags = item.getAttribute('data-test-tags').trim().split(' ').filter(Boolean);
827
+ const projectName = item.getAttribute('data-project-name').trim();
828
+ const testStatus = item.getAttribute('data-test-status').trim();
829
+
830
+ const matchesProject = projects.length === 0 || projects.includes(projectName);
831
+ const matchesTags = tags.length === 0 || tags.every(tag => testTags.includes(tag));
832
+ const matchesStatus = this.matchesStatus(testStatus, status);
833
+
834
+ return matchesProject && matchesTags && matchesStatus;
835
+ }
836
+
837
+ /**
838
+ * Check if test status matches selected status
839
+ * @param {string} testStatus - Test status
840
+ * @param {string} selectedStatus - Selected status
841
+ * @returns {boolean} Whether status matches
842
+ */
843
+ matchesStatus(testStatus, selectedStatus) {
844
+ if (selectedStatus === 'all') return testStatus !== 'skipped';
845
+ if (selectedStatus === 'failed') return testStatus === 'failed' || testStatus === 'timedOut';
846
+ if (selectedStatus === 'retry') return testStatus.includes('retry');
847
+ if (selectedStatus === 'flaky') return testStatus.includes('flaky');
848
+ return testStatus === selectedStatus;
849
+ }
850
+
851
+ /**
852
+ * Update selected filters display
853
+ * @param {Array} projects - Selected projects
854
+ * @param {Array} tags - Selected tags
855
+ * @param {string} status - Selected status
856
+ */
857
+ updateSelectedFiltersDisplay(projects, tags, status) {
858
+ let displayText = [];
859
+ if (projects.length > 0) displayText.push(`Projects: ${projects.join(', ')}`);
860
+ if (tags.length > 0) displayText.push(`Tags: ${tags.join(', ')}`);
861
+ if (status !== 'all') displayText.push(`Status: ${status}`);
862
+ this.elements.filtersDisplay.innerHTML = displayText.length > 0 ? displayText.join(' | ') : 'All Tests';
863
+ }
864
+ }
865
+
866
+ /**
867
+ * ====================================
868
+ * SEARCH MANAGER
869
+ * ====================================
870
+ */
871
+ class SearchManager {
872
+ /**
873
+ * Initialize the search manager
874
+ * @param {Object} elements - DOM elements
875
+ */
876
+ constructor(elements) {
877
+ this.elements = elements;
878
+ this.init();
879
+ }
880
+
881
+ /**
882
+ * Initialize search manager
883
+ */
884
+ init() {
885
+ this.elements.searchInput.addEventListener('input', Utils.debounce(this.filterTests.bind(this), 300));
886
+ }
887
+
888
+ /**
889
+ * Filter tests based on search term
890
+ * @param {Event} event - Input event
891
+ */
892
+ filterTests(event) {
893
+ const searchTerm = event.target.value.toLowerCase();
894
+ const testItems = document.querySelectorAll('[data-test-id]');
895
+
896
+ this.elements.detailsElements.forEach(detail => detail.open = !!searchTerm);
897
+
898
+ testItems.forEach(item => {
899
+ const isVisible = item.textContent.toLowerCase().includes(searchTerm);
900
+ item.style.display = isVisible ? 'flex' : 'none';
901
+ if (isVisible && searchTerm) this.openParentDetails(item);
902
+ });
903
+ }
904
+
905
+ /**
906
+ * Open parent details elements
907
+ * @param {HTMLElement} item - Test item
908
+ */
909
+ openParentDetails(item) {
910
+ let parent = item.parentElement;
911
+ while (parent && parent.tagName !== 'ASIDE') {
912
+ if (parent.tagName === 'DETAILS') parent.open = true;
913
+ parent = parent.parentElement;
914
+ }
915
+ }
916
+ }
917
+
918
+ /**
919
+ * ====================================
920
+ * HISTORY MANAGER
921
+ * ====================================
922
+ */
923
+ class HistoryManager {
924
+ /**
925
+ * Initialize the history manager
926
+ * @param {Array} testHistory - Test history data
927
+ */
928
+ constructor(testHistory) {
929
+ this.testHistory = testHistory;
930
+ this.testHistoriesMap = null;
931
+ this.testHistoryTitle = '';
932
+ }
933
+
934
+ /**
935
+ * Set current test history
936
+ * @param {Array} historyMap - History map
937
+ * @param {string} title - Test title
938
+ */
939
+ setCurrentHistory(historyMap, title) {
940
+ this.testHistoriesMap = historyMap;
941
+ this.testHistoryTitle = title;
942
+ }
943
+
944
+ /**
945
+ * Open history modal
946
+ */
947
+ openHistory() {
948
+ const historyElement = document.getElementById("historyModal");
949
+ historyElement.classList.add('is-active');
950
+
951
+ let historyContent = '';
952
+ if (this.testHistoriesMap && this.testHistoriesMap.length > 0) {
953
+ historyContent = this.testHistoriesMap.map((h, index) => `
954
+ <tr>
955
+ <td>${h.run_date}</td>
956
+ <td>${h.status}</td>
957
+ <td>${h.duration}</td>
958
+ ${h.error_message ? `<td><div class="modal" id="${index}">
959
+ <div class="modal-background"></div>
960
+ <div class="modal-content">
961
+ <pre><code>${h.error_message}</code></pre>
962
+ </div>
963
+ <button class="button is-primary" onclick="closeErrorModal(${index})">Close</button>
964
+ </div><a class="button is-link" onclick="showHistoryErrorMessage(${index})">Show error</a></td>` : '<td>No Error</td>'}
965
+ </tr>
966
+ `).join('');
967
+ } else {
968
+ historyContent = '<p class="title">No history available</p>';
969
+ }
970
+
971
+ historyElement.innerHTML = `
972
+ <div class="modal-background"></div>
973
+ <div class="modal-card">
974
+ <header class="modal-card-head">
975
+ <p class="modal-card-title">${this.testHistoryTitle}</p>
976
+ <button class="button is-primary" onclick="closeHistoryModal()">Close</button>
977
+ </header>
978
+ <section class="modal-card-body">
979
+ <table class="table is-hoverable is-fullwidth">
980
+ <thead>
981
+ <tr>
982
+ <th title="Run Date">Run Date</th>
983
+ <th title="Status">Status</th>
984
+ <th title="Duration">Duration</th>
985
+ <th title="Reason">Reason</th>
986
+ </tr>
987
+ </thead>
988
+ <tbody>
989
+ ${historyContent}
990
+ </tbody>
991
+ </table>
992
+ </section>
993
+ </div>
994
+ `;
995
+ }
996
+
997
+ /**
998
+ * Close history modal
999
+ */
1000
+ closeHistoryModal() {
1001
+ document.getElementById("historyModal").classList.remove('is-active');
1002
+ }
1003
+
1004
+ /**
1005
+ * Show history error message
1006
+ * @param {string} modalId - Modal ID
1007
+ */
1008
+ showHistoryErrorMessage(modalId) {
1009
+ document.getElementById(modalId)?.classList.add('is-active');
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * ====================================
1015
+ * MOBILE RESPONSIVE MANAGER
1016
+ * ====================================
1017
+ */
1018
+ class MobileManager {
1019
+ /**
1020
+ * Initialize the mobile manager
1021
+ */
1022
+ constructor() {
1023
+ this.sidebar = document.getElementById('sidebar');
1024
+ this.init();
1025
+ }
1026
+
1027
+ /**
1028
+ * Initialize mobile manager
1029
+ */
1030
+ init() {
1031
+ // Add sidebar toggle button to navbar for mobile
1032
+ const navbar = document.querySelector('.navbar-brand');
1033
+ if (navbar) {
1034
+ const toggleButton = document.createElement('a');
1035
+ toggleButton.className = 'navbar-item sidebar-toggle is-hidden-desktop';
1036
+ toggleButton.innerHTML = '<i class="fa fa-bars"></i>';
1037
+ toggleButton.addEventListener('click', () => this.toggleSidebar());
1038
+ navbar.appendChild(toggleButton);
1039
+ }
1040
+
1041
+ // Close sidebar when clicking on a link on mobile
1042
+ const sidebarLinks = document.querySelectorAll('.sidebar-menu-link');
1043
+ sidebarLinks.forEach(link => {
1044
+ link.addEventListener('click', () => {
1045
+ if (window.innerWidth < 769) {
1046
+ this.sidebar.classList.remove('is-active');
1047
+ }
1048
+ });
1049
+ });
1050
+ }
1051
+
1052
+ /**
1053
+ * Toggle sidebar on mobile
1054
+ */
1055
+ toggleSidebar() {
1056
+ this.sidebar.classList.toggle('is-active');
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * ====================================
1062
+ * INITIALIZATION
1063
+ * ====================================
1064
+ */
1065
+ // Initialize data
1066
+ const testData = {{{ json results }}};
1067
+ const testHistory = {{{ json testHistories }}};
1068
+
1069
+ // Initialize DOM elements
1070
+ const elements = {
1071
+ testDetails: document.getElementById('testDetails'),
1072
+ summary: document.getElementById('dashboard-section'),
1073
+ themeButton: document.getElementById("toggle-theme"),
1074
+ themeIcon: document.getElementById("theme-icon"),
1075
+ htmlElement: document.documentElement,
1076
+ searchInput: document.querySelector('input[name="search"]'),
1077
+ detailsElements: document.querySelectorAll('details'),
1078
+ filtersDisplay: document.getElementById('selected-filters'),
1079
+ };
1080
+
1081
+ // Initialize managers
1082
+ const sidebarManager = new SidebarManager();
1083
+ const themeManager = new ThemeManager(elements);
1084
+ const testDetailsManager = new TestDetailsManager(elements, testData, testHistory, sidebarManager);
1085
+ const filterManager = new FilterManager(elements);
1086
+ const searchManager = new SearchManager(elements);
1087
+ const historyManager = new HistoryManager(testHistory);
1088
+ const mobileManager = new MobileManager();
1089
+ const sidebarCollapseManager = new SidebarCollapseManager();
1090
+
1091
+ // Attach event listeners
1092
+ testDetailsManager.attachEventListeners();
1093
+
1094
+ // Expose necessary functions to the global scope
1095
+ window.showSummary = () => testDetailsManager.hide();
1096
+ window.openModal = () => document.querySelector("#testImage").classList.add("is-active");
1097
+ window.closeModal = () => document.querySelector("#testImage").classList.remove("is-active");
1098
+ window.closeErrorModal = (modalId) => document.getElementById(modalId).classList.remove("is-active");
1099
+ window.openTraceViewer = (button) => {
1100
+ const tracePath = button.getAttribute("data-trace");
1101
+ if (tracePath) {
1102
+ const normalizedTracePath = tracePath.replace(/\\/g, '/');
1103
+ const baseUrl = Utils.getAdjustedBaseUrl();
1104
+ window.open(`${baseUrl}/trace/index.html?trace=${baseUrl}/${normalizedTracePath}`, "_blank");
1105
+ }
1106
+ };
1107
+ window.getAdjustedBaseUrl = Utils.getAdjustedBaseUrl;
1108
+ window.openHistory = () => {
1109
+ historyManager.setCurrentHistory(testDetailsManager.testHistoriesMap, testDetailsManager.testHistoryTitle);
1110
+ historyManager.openHistory();
1111
+ };
1112
+ window.closeHistoryModal = () => historyManager.closeHistoryModal();
1113
+ window.showHistoryErrorMessage = (modalId) => historyManager.showHistoryErrorMessage(modalId);
1114
+ });
1115
+ </script>
1116
+ </body>
577
1117
  </html>