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