ortoni-report 3.0.5 → 4.0.2-beta.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.
Files changed (45) hide show
  1. package/changelog.md +30 -0
  2. package/dist/chunk-45EJSEX2.mjs +632 -0
  3. package/dist/chunk-75EAJL2U.mjs +632 -0
  4. package/dist/chunk-FGIYOFIC.mjs +632 -0
  5. package/dist/chunk-FHKWBHU6.mjs +633 -0
  6. package/dist/chunk-GLICR3VS.mjs +637 -0
  7. package/dist/chunk-HFO6XSKC.mjs +633 -0
  8. package/dist/chunk-HOZD6YIV.mjs +634 -0
  9. package/dist/chunk-IJO2YIFE.mjs +637 -0
  10. package/dist/chunk-INS3E7E6.mjs +638 -0
  11. package/dist/chunk-JEIWNUQY.mjs +632 -0
  12. package/dist/chunk-JPLAGYR7.mjs +632 -0
  13. package/dist/chunk-NM6ULN2O.mjs +632 -0
  14. package/dist/chunk-OZS6QIJS.mjs +638 -0
  15. package/dist/chunk-P57227VN.mjs +633 -0
  16. package/dist/chunk-QMTRYN5N.js +635 -0
  17. package/dist/chunk-TI33PMMQ.mjs +639 -0
  18. package/dist/chunk-Z5NBP5TS.mjs +635 -0
  19. package/dist/cli/cli.cjs +678 -0
  20. package/dist/cli/cli.d.cts +1 -0
  21. package/dist/cli/cli.js +580 -11
  22. package/dist/cli/cli.mjs +62 -5
  23. package/dist/index.html +21 -0
  24. package/dist/ortoni-report.cjs +2134 -0
  25. package/dist/ortoni-report.d.cts +111 -0
  26. package/dist/ortoni-report.d.mts +3 -12
  27. package/dist/ortoni-report.d.ts +3 -12
  28. package/dist/ortoni-report.js +182 -314
  29. package/dist/ortoni-report.mjs +64 -740
  30. package/package.json +4 -5
  31. package/readme.md +26 -33
  32. package/dist/chunk-AY2PKDHU.mjs +0 -69
  33. package/dist/chunk-OOALU4XG.mjs +0 -72
  34. package/dist/chunk-ZSIRUQUA.mjs +0 -68
  35. package/dist/style/main.css +0 -80
  36. package/dist/views/analytics.hbs +0 -103
  37. package/dist/views/head.hbs +0 -11
  38. package/dist/views/main.hbs +0 -1295
  39. package/dist/views/project.hbs +0 -238
  40. package/dist/views/sidebar.hbs +0 -244
  41. package/dist/views/summaryCard.hbs +0 -15
  42. package/dist/views/testIcons.hbs +0 -13
  43. package/dist/views/testPanel.hbs +0 -45
  44. package/dist/views/testStatus.hbs +0 -9
  45. package/dist/views/userInfo.hbs +0 -260
@@ -1,1295 +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
- class="button is-primary is-fullwidth mt-3"
552
- data-markdown="${test.markdownPath}"
553
- onclick="openMarkdown(this)">
554
- <span class="icon"><i class="fa-solid fa-file-lines"></i></span>
555
- <span class="has-text-white pl-2">Open Markdown</span>
556
- </button>
557
- </div>
558
- `;
559
- }
560
- content += '</div></div></div>';
561
- return content;
562
- }
563
-
564
- /**
565
- * Render additional information
566
- * @param {Object} test - Test data
567
- * @returns {string} HTML content
568
- */
569
- renderAdditionalInfo(test) {
570
- if (!(test.annotations.length || test.testTags.length > 0)) return '';
571
- return `
572
- <div class="card mb-5">
573
- <header class="card-header">
574
- <p class="card-header-title">Additional Information</p>
575
- </header>
576
- <div class="card-content">
577
- <div class="content">
578
- ${test.testTags.length > 0 ? `
579
- <div class="control mb-4">
580
- <div class="tags is-rounded">
581
- ${test.testTags.map(tag => `<span class="tag is-primary is-medium">${tag}</span>`).join('')}
582
- </div>
583
- </div>` : ""}
584
- ${test.annotations
585
- .filter(annotation => annotation !== null && annotation !== undefined)
586
- .map(annotation => `
587
- <div class="mb-4">
588
- ${annotation?.type ? `<strong class="has-text-link">Type: </strong><span>${annotation.type}</span>` : ''}
589
- <br>
590
- ${annotation?.description ? `<strong class="has-text-link">Description: </strong><span>${annotation.description}</span>` : ''}
591
- </div>
592
- `).join('')}
593
- </div>
594
- </div>
595
- </div>
596
- `;
597
- }
598
-
599
- /**
600
- * Render tabs
601
- * @param {Object} test - Test data
602
- * @returns {string} HTML content
603
- */
604
- renderTabs(test) {
605
- return `
606
- <div class="card mt-5">
607
- <div class="card-content">
608
- <div class="tabs is-boxed is-fullwidth">
609
- <ul>
610
- <li class="is-active"><a data-tab="steps">Steps</a></li>
611
- <li><a data-tab="errors">Errors</a></li>
612
- <li><a data-tab="logs">Logs</a></li>
613
- </ul>
614
- </div>
615
- <div id="tabContent">
616
- <div id="stepsTab" class="tab-content">
617
- ${this.renderSteps(test)}
618
- </div>
619
- <div id="errorsTab" class="tab-content" style="display: none;">
620
- ${this.renderErrors(test)}
621
- </div>
622
- <div id="logsTab" class="tab-content" style="display: none;">
623
- ${this.renderLogs(test)}
624
- </div>
625
- </div>
626
- </div>
627
- </div>
628
- `;
629
- }
630
-
631
- /**
632
- * Render steps
633
- * @param {Object} test - Test data
634
- * @returns {string} HTML content
635
- */
636
- renderSteps(test) {
637
- if (test.steps.length === 0) return '<p>No steps available.</p>';
638
- return `
639
- <div class="content">
640
- <span id="stepDetails" class="content"></span>
641
- </div>
642
- `;
643
- }
644
-
645
- /**
646
- * Render errors
647
- * @param {Object} test - Test data
648
- * @returns {string} HTML content
649
- */
650
- renderErrors(test) {
651
- if (!test.errors.length) return '<p>No errors reported.</p>';
652
- return `
653
- <div class="content">
654
- <pre><code class="data-lang=js">${test.errors.join('\n')}</code></pre>
655
- </div>
656
- `;
657
- }
658
-
659
- /**
660
- * Render logs
661
- * @param {Object} test - Test data
662
- * @returns {string} HTML content
663
- */
664
- renderLogs(test) {
665
- if (!test.logs) return '<p>No logs available.</p>';
666
- return `
667
- <div class="content">
668
- <pre>${test.logs}</pre>
669
- </div>
670
- `;
671
- }
672
-
673
- /**
674
- * Attach screenshot listeners
675
- * @param {Object} test - Test data
676
- */
677
- attachScreenshotListeners(test) {
678
- if (test.screenshots && test.screenshots.length > 0) {
679
- const changeScreenshot = (direction) => {
680
- this.currentScreenshotIndex = (this.currentScreenshotIndex + direction + test.screenshots.length) % test.screenshots.length;
681
- this.updateScreenshot(test.screenshots, this.currentScreenshotIndex);
682
- };
683
- const gotoScreenshot = (index) => {
684
- this.currentScreenshotIndex = index;
685
- this.updateScreenshot(test.screenshots, this.currentScreenshotIndex);
686
- };
687
- document.querySelector('.pagination-previous').addEventListener('click', () => changeScreenshot(-1));
688
- document.querySelector('.pagination-next').addEventListener('click', () => changeScreenshot(1));
689
- document.querySelectorAll('.pagination-link').forEach((link, index) => {
690
- link.addEventListener('click', () => gotoScreenshot(index));
691
- });
692
- }
693
- }
694
-
695
- /**
696
- * Update screenshot
697
- * @param {Array} screenshots - Screenshot paths
698
- * @param {number} index - Screenshot index
699
- */
700
- updateScreenshot(screenshots, index) {
701
- document.getElementById('screenshot-main-img').src = screenshots[index];
702
- document.getElementById('screenshot-modal-img').src = screenshots[index];
703
- document.querySelectorAll('.pagination-link').forEach((link, i) => {
704
- link.classList.toggle('is-current', i === index);
705
- });
706
- }
707
-
708
- /**
709
- * Attach steps
710
- * @param {Object} test - Test data
711
- */
712
- attachSteps(test) {
713
- const stepDetailsDiv = document.getElementById('stepDetails');
714
- if (stepDetailsDiv) {
715
- const stepsList = document.createElement("ul");
716
- stepsList.setAttribute("id", "steps");
717
- test.steps.forEach(step => {
718
- const li = document.createElement('li');
719
- li.innerHTML = `<strong class="${step.snippet ? 'has-text-danger' : ''}">${step.title}</strong>`;
720
- if (step.snippet) {
721
- const pre = document.createElement('pre');
722
- const code = document.createElement('code');
723
- const locationText = step.location ? `\n\nat: ${step.location}` : '';
724
- code.innerHTML = `${step.snippet}${locationText}`;
725
- code.setAttribute('data-lang', 'js');
726
- pre.appendChild(code);
727
- li.appendChild(pre);
728
- }
729
- stepsList.appendChild(li);
730
- });
731
- stepDetailsDiv.appendChild(stepsList);
732
- }
733
- }
734
-
735
- /**
736
- * Attach tab listeners
737
- */
738
- attachTabListeners() {
739
- const tabLinks = document.querySelectorAll('div.tabs a');
740
- tabLinks.forEach(link => {
741
- link.addEventListener('click', (e) => {
742
- e.preventDefault();
743
- const tabId = link.getAttribute('data-tab');
744
-
745
- tabLinks.forEach(l => l.parentElement.classList.remove('is-active'));
746
- document.querySelectorAll('div.tab-content').forEach(c => c.style.display = 'none');
747
-
748
- link.parentElement.classList.add('is-active');
749
- document.getElementById(`${tabId}Tab`).style.display = 'block';
750
- });
751
- });
752
- }
753
-
754
- /**
755
- * Attach event listeners
756
- */
757
- attachEventListeners() {
758
- const testItems = document.querySelectorAll('[data-test-id]');
759
- testItems.forEach(item => {
760
- item.addEventListener('click', () => {
761
- testItems.forEach(i => i.classList.remove('listselected'));
762
- item.classList.add('listselected');
763
- const testId = item.getAttribute('data-test-id');
764
- const testHistoryId = item.getAttribute('data-test-history-id');
765
- const test = this.testData[testId];
766
- const historyEntry = this.testHistory.find(entry => entry.testId === testHistoryId);
767
- this.testHistoriesMap = historyEntry ? historyEntry.history : null;
768
- this.testHistoryTitle = historyEntry && historyEntry.testId ? historyEntry.testId.split(":")[2] : '';
769
- this.show(test);
770
- });
771
- });
772
- }
773
- }
774
-
775
- /**
776
- * ====================================
777
- * FILTER MANAGER
778
- * ====================================
779
- */
780
- class FilterManager {
781
- /**
782
- * Initialize the filter manager
783
- * @param {Object} elements - DOM elements
784
- */
785
- constructor(elements) {
786
- this.elements = elements;
787
- this.init();
788
- }
789
-
790
- /**
791
- * Initialize filter manager
792
- */
793
- init() {
794
- this.attachEventListeners();
795
- }
796
-
797
- /**
798
- * Attach event listeners
799
- */
800
- attachEventListeners() {
801
- const checkboxes = document.querySelectorAll('#select-filter input[type="checkbox"]');
802
- checkboxes.forEach(checkbox => {
803
- checkbox.addEventListener('change', () => this.applyFilters());
804
- });
805
-
806
- const filters = document.querySelectorAll('.filter');
807
- filters.forEach(filter => {
808
- filter.addEventListener('click', (event) => {
809
- filters.forEach(f => {
810
- f.classList.remove('active');
811
- f.style.borderRight = ''; // Reset previous border-right
812
- });
813
- const clickedFilter = event.currentTarget;
814
- clickedFilter.classList.add('active');
815
- const borderLeftColor = window.getComputedStyle(clickedFilter).borderLeftColor;
816
- clickedFilter.style.setProperty('border-right', `5px solid ${borderLeftColor}`, 'important');
817
- this.applyFilters();
818
- });
819
- });
820
- }
821
-
822
- /**
823
- * Apply filters
824
- */
825
- applyFilters() {
826
- const selectedProjects = this.getSelectedValues('project');
827
- const selectedTags = this.getSelectedValues('test-tags');
828
- const selectedStatus = document.querySelector('.filter.active')?.getAttribute('data-status') || 'all';
829
-
830
- this.elements.detailsElements.forEach(details => {
831
- const items = details.querySelectorAll('div[data-test-id]');
832
- let shouldShowDetails = false;
833
-
834
- items.forEach(item => {
835
- const isVisible = this.shouldShowItem(item, selectedProjects, selectedTags, selectedStatus);
836
- item.classList.toggle('is-hidden', !isVisible);
837
- shouldShowDetails = shouldShowDetails || isVisible;
838
- });
839
-
840
- details.open = shouldShowDetails;
841
- details.classList.toggle('is-hidden', !shouldShowDetails);
842
- });
843
-
844
- this.updateSelectedFiltersDisplay(selectedProjects, selectedTags, selectedStatus);
845
- }
846
-
847
- /**
848
- * Get selected values
849
- * @param {string} type - Filter type
850
- * @returns {Array} Selected values
851
- */
852
- getSelectedValues(type) {
853
- return Array.from(document.querySelectorAll(`#select-filter input[type="checkbox"][data-filter-type="${type}"]:checked`))
854
- .map(checkbox => checkbox.value.trim());
855
- }
856
-
857
- /**
858
- * Check if item should be shown
859
- * @param {HTMLElement} item - Test item
860
- * @param {Array} projects - Selected projects
861
- * @param {Array} tags - Selected tags
862
- * @param {string} status - Selected status
863
- * @returns {boolean} Whether item should be shown
864
- */
865
- shouldShowItem(item, projects, tags, status) {
866
- const testTags = item.getAttribute('data-test-tags').trim().split(' ').filter(Boolean);
867
- const projectName = item.getAttribute('data-project-name').trim();
868
- const testStatus = item.getAttribute('data-test-status').trim();
869
-
870
- const matchesProject = projects.length === 0 || projects.includes(projectName);
871
- const matchesTags = tags.length === 0 || tags.every(tag => testTags.includes(tag));
872
- const matchesStatus = this.matchesStatus(testStatus, status);
873
-
874
- return matchesProject && matchesTags && matchesStatus;
875
- }
876
-
877
- /**
878
- * Check if test status matches selected status
879
- * @param {string} testStatus - Test status
880
- * @param {string} selectedStatus - Selected status
881
- * @returns {boolean} Whether status matches
882
- */
883
- matchesStatus(testStatus, selectedStatus) {
884
- if (selectedStatus === 'all') return testStatus !== 'skipped';
885
- if (selectedStatus === 'failed') return testStatus === 'failed' || testStatus === 'timedOut';
886
- if (selectedStatus === 'retry') return testStatus.includes('retry');
887
- if (selectedStatus === 'flaky') return testStatus.includes('flaky');
888
- return testStatus === selectedStatus;
889
- }
890
-
891
- /**
892
- * Update selected filters display
893
- * @param {Array} projects - Selected projects
894
- * @param {Array} tags - Selected tags
895
- * @param {string} status - Selected status
896
- */
897
- updateSelectedFiltersDisplay(projects, tags, status) {
898
- let displayText = [];
899
- if (projects.length > 0) displayText.push(`Projects: ${projects.join(', ')}`);
900
- if (tags.length > 0) displayText.push(`Tags: ${tags.join(', ')}`);
901
- if (status !== 'all') displayText.push(`Status: ${status}`);
902
- this.elements.filtersDisplay.innerHTML = displayText.length > 0 ? displayText.join(' | ') : 'All Tests';
903
- }
904
- }
905
-
906
- /**
907
- * ====================================
908
- * SEARCH MANAGER
909
- * ====================================
910
- */
911
- class SearchManager {
912
- /**
913
- * Initialize the search manager
914
- * @param {Object} elements - DOM elements
915
- */
916
- constructor(elements) {
917
- this.elements = elements;
918
- this.init();
919
- }
920
-
921
- /**
922
- * Initialize search manager
923
- */
924
- init() {
925
- this.elements.searchInput.addEventListener('input', Utils.debounce(this.filterTests.bind(this), 300));
926
- }
927
-
928
- /**
929
- * Filter tests based on search term
930
- * @param {Event} event - Input event
931
- */
932
- filterTests(event) {
933
- const searchTerm = event.target.value.toLowerCase();
934
- const testItems = document.querySelectorAll('[data-test-id]');
935
-
936
- this.elements.detailsElements.forEach(detail => detail.open = !!searchTerm);
937
-
938
- testItems.forEach(item => {
939
- const isVisible = item.textContent.toLowerCase().includes(searchTerm);
940
- item.style.display = isVisible ? 'flex' : 'none';
941
- if (isVisible && searchTerm) this.openParentDetails(item);
942
- });
943
- }
944
-
945
- /**
946
- * Open parent details elements
947
- * @param {HTMLElement} item - Test item
948
- */
949
- openParentDetails(item) {
950
- let parent = item.parentElement;
951
- while (parent && parent.tagName !== 'ASIDE') {
952
- if (parent.tagName === 'DETAILS') parent.open = true;
953
- parent = parent.parentElement;
954
- }
955
- }
956
- }
957
-
958
- /**
959
- * ====================================
960
- * HISTORY MANAGER
961
- * ====================================
962
- */
963
- class HistoryManager {
964
- /**
965
- * Initialize the history manager
966
- * @param {Array} testHistory - Test history data
967
- */
968
- constructor(testHistory) {
969
- this.testHistory = testHistory
970
- this.testHistoriesMap = null
971
- this.testHistoryTitle = ""
972
- this.activeErrorModal = null
973
- }
974
-
975
- /**
976
- * Set current test history
977
- * @param {Array} historyMap - History map
978
- * @param {string} title - Test title
979
- */
980
- setCurrentHistory(historyMap, title) {
981
- this.testHistoriesMap = historyMap
982
- this.testHistoryTitle = title
983
- }
984
-
985
- /**
986
- * Open history modal
987
- */
988
- openHistory() {
989
- const historyElement = document.getElementById("historyModal")
990
- historyElement.classList.add("is-active")
991
-
992
- let historyContent = ""
993
- if (this.testHistoriesMap && this.testHistoriesMap.length > 0) {
994
- historyContent = this.testHistoriesMap
995
- .map(
996
- (h, index) => `
997
- <tr>
998
- <td>${h.run_date}</td>
999
- <td>
1000
- <span class="tag is-${this.getStatusClass(h.status)}">
1001
- ${h.status}
1002
- </span>
1003
- </td>
1004
- <td>${h.duration}</td>
1005
- <td>
1006
- ${h.error_message
1007
- ? `<div class="modal" id="error-${index}">
1008
- <div class="modal-background"></div>
1009
- <div class="modal-card">
1010
- <header class="modal-card-head">
1011
- <p class="modal-card-title">
1012
- <span class="icon-text">
1013
- <span class="icon">
1014
- <i class="fa-solid fa-exclamation-triangle"></i>
1015
- </span>
1016
- <span>Error Details</span>
1017
- </span>
1018
- </p>
1019
- <button class="delete" aria-label="close" onclick="closeErrorModal('error-${index}')"></button>
1020
- </header>
1021
- <section class="modal-card-body">
1022
- <div class="notification is-danger is-light">
1023
- <pre><code>${h.error_message}</code></pre>
1024
- </div>
1025
- </section>
1026
- <footer class="modal-card-foot">
1027
- <button class="button is-primary" onclick="closeErrorModal('error-${index}')">Close</button>
1028
- </footer>
1029
- </div>
1030
- </div>
1031
- <button class="button is-small is-link" onclick="showHistoryErrorMessage('error-${index}')">
1032
- <span class="icon">
1033
- <i class="fa-solid fa-exclamation-triangle"></i>
1034
- </span>
1035
- <span>View Error</span>
1036
- </button>`
1037
- : '<span class="tag is-success is-light">No Error</span>'
1038
- }
1039
- </td>
1040
- </tr>
1041
- `,
1042
- )
1043
- .join("")
1044
- } else {
1045
- historyContent = `
1046
- <tr>
1047
- <td colspan="4">
1048
- <div class="notification is-info is-light">
1049
- <p class="has-text-centered">
1050
- <span class="icon">
1051
- <i class="fa-solid fa-info-circle"></i>
1052
- </span>
1053
- <span>No history available for this test.</span>
1054
- </p>
1055
- </div>
1056
- </td>
1057
- </tr>
1058
- `
1059
- }
1060
-
1061
- historyElement.innerHTML = `
1062
- <div class="modal-background"></div>
1063
- <div class="modal-card">
1064
- <header class="modal-card-head">
1065
- <p class="modal-card-title">
1066
- <span class="icon-text">
1067
- <span class="icon">
1068
- <i class="fa-solid fa-history"></i>
1069
- </span>
1070
- <span>Test History: ${this.testHistoryTitle}</span>
1071
- </span>
1072
- </p>
1073
- <button class="delete" aria-label="close" onclick="closeHistoryModal()"></button>
1074
- </header>
1075
- <section class="modal-card-body">
1076
- <div class="table-container">
1077
- <table class="table is-hoverable is-fullwidth">
1078
- <thead>
1079
- <tr>
1080
- <th title="Run Date">
1081
- <span class="icon-text">
1082
- <span class="icon">
1083
- <i class="fa-solid fa-calendar"></i>
1084
- </span>
1085
- <span>Run Date</span>
1086
- </span>
1087
- </th>
1088
- <th title="Status">
1089
- <span class="icon-text">
1090
- <span class="icon">
1091
- <i class="fa-solid fa-check-circle"></i>
1092
- </span>
1093
- <span>Status</span>
1094
- </span>
1095
- </th>
1096
- <th title="Duration">
1097
- <span class="icon-text">
1098
- <span class="icon">
1099
- <i class="fa-solid fa-clock"></i>
1100
- </span>
1101
- <span>Duration</span>
1102
- </span>
1103
- </th>
1104
- <th title="Details">
1105
- <span class="icon-text">
1106
- <span class="icon">
1107
- <i class="fa-solid fa-info-circle"></i>
1108
- </span>
1109
- <span>Details</span>
1110
- </span>
1111
- </th>
1112
- </tr>
1113
- </thead>
1114
- <tbody>
1115
- ${historyContent}
1116
- </tbody>
1117
- </table>
1118
- </div>
1119
- </section>
1120
- <footer class="modal-card-foot">
1121
- <button class="button is-primary" onclick="closeHistoryModal()">Close</button>
1122
- </footer>
1123
- </div>
1124
- `
1125
-
1126
- // Add keyboard event listener for ESC key
1127
- document.addEventListener("keydown", this.handleEscKeyPress)
1128
- }
1129
-
1130
- /**
1131
- * Get status class based on test status
1132
- * @param {string} status - Test status
1133
- * @returns {string} CSS class
1134
- */
1135
- getStatusClass(status) {
1136
- if (status && status.startsWith("passed")) return "success"
1137
- if (status === "flaky") return "warning"
1138
- if (status === "failed") return "danger"
1139
- if (status === "skipped") return "info"
1140
- if (status && status.includes("retry")) return "link"
1141
- return "dark"
1142
- }
1143
-
1144
- /**
1145
- * Handle ESC key press to close modal
1146
- * @param {KeyboardEvent} event - Keyboard event
1147
- */
1148
- handleEscKeyPress = (event) => {
1149
- if (event.key === "Escape") {
1150
- this.closeHistoryModal()
1151
- }
1152
- }
1153
-
1154
- /**
1155
- * Close history modal
1156
- */
1157
- closeHistoryModal() {
1158
- const historyElement = document.getElementById("historyModal")
1159
- historyElement.classList.remove("is-active")
1160
-
1161
- // Remove keyboard event listener
1162
- document.removeEventListener("keydown", this.handleEscKeyPress)
1163
- }
1164
-
1165
- /**
1166
- * Show history error message
1167
- * @param {string} modalId - Modal ID
1168
- */
1169
- showHistoryErrorMessage(modalId) {
1170
- document.getElementById(modalId)?.classList.add("is-active")
1171
- }
1172
-
1173
- /**
1174
- * Close error modal
1175
- * @param {string} modalId - Modal ID
1176
- */
1177
- closeErrorModal(modalId) {
1178
- document.getElementById(modalId)?.classList.remove("is-active")
1179
- }
1180
- }
1181
- /**
1182
- * ====================================
1183
- * MOBILE RESPONSIVE MANAGER
1184
- * ====================================
1185
- */
1186
- class MobileManager {
1187
- /**
1188
- * Initialize the mobile manager
1189
- */
1190
- constructor() {
1191
- this.sidebar = document.getElementById('sidebar');
1192
- this.init();
1193
- }
1194
-
1195
- /**
1196
- * Initialize mobile manager
1197
- */
1198
- init() {
1199
- // Add sidebar toggle button to navbar for mobile
1200
- const navbar = document.querySelector('.navbar-brand');
1201
- if (navbar) {
1202
- const toggleButton = document.createElement('a');
1203
- toggleButton.className = 'navbar-item sidebar-toggle is-hidden-desktop';
1204
- toggleButton.innerHTML = '<i class="fa fa-bars"></i>';
1205
- toggleButton.addEventListener('click', () => this.toggleSidebar());
1206
- navbar.appendChild(toggleButton);
1207
- }
1208
-
1209
- // Close sidebar when clicking on a link on mobile
1210
- const sidebarLinks = document.querySelectorAll('.sidebar-menu-link');
1211
- sidebarLinks.forEach(link => {
1212
- link.addEventListener('click', () => {
1213
- if (window.innerWidth < 769) {
1214
- this.sidebar.classList.remove('is-active');
1215
- }
1216
- });
1217
- });
1218
- }
1219
-
1220
- /**
1221
- * Toggle sidebar on mobile
1222
- */
1223
- toggleSidebar() {
1224
- this.sidebar.classList.toggle('is-active');
1225
- }
1226
- }
1227
-
1228
- /**
1229
- * ====================================
1230
- * INITIALIZATION
1231
- * ====================================
1232
- */
1233
- // Initialize data
1234
- const testData = {{{ json results }}};
1235
- const testHistory = {{{ json testHistories }}};
1236
-
1237
- // Initialize DOM elements
1238
- const elements = {
1239
- testDetails: document.getElementById('testDetails'),
1240
- summary: document.getElementById('dashboard-section'),
1241
- themeButton: document.getElementById("toggle-theme"),
1242
- themeIcon: document.getElementById("theme-icon"),
1243
- htmlElement: document.documentElement,
1244
- searchInput: document.querySelector('input[name="search"]'),
1245
- detailsElements: document.querySelectorAll('details'),
1246
- filtersDisplay: document.getElementById('selected-filters'),
1247
- };
1248
-
1249
- // Initialize managers
1250
- const sidebarManager = new SidebarManager();
1251
- const themeManager = new ThemeManager(elements);
1252
- const testDetailsManager = new TestDetailsManager(elements, testData, testHistory, sidebarManager);
1253
- const filterManager = new FilterManager(elements);
1254
- const searchManager = new SearchManager(elements);
1255
- const historyManager = new HistoryManager(testHistory);
1256
- const mobileManager = new MobileManager();
1257
- const sidebarCollapseManager = new SidebarCollapseManager();
1258
-
1259
- // Attach event listeners
1260
- testDetailsManager.attachEventListeners();
1261
-
1262
- // Expose necessary functions to the global scope
1263
- window.showSummary = () => testDetailsManager.hide();
1264
- window.openModal = () => document.querySelector("#testImage").classList.add("is-active");
1265
- window.closeModal = () => document.querySelector("#testImage").classList.remove("is-active");
1266
- window.closeErrorModal = (modalId) => document.getElementById(modalId).classList.remove("is-active");
1267
- window.openTraceViewer = (button) => {
1268
- const tracePath = button.getAttribute("data-trace");
1269
- if (tracePath) {
1270
- const normalizedTracePath = tracePath.replace(/\\/g, '/');
1271
- const baseUrl = Utils.getAdjustedBaseUrl();
1272
- window.open(`${baseUrl}/trace/index.html?trace=${baseUrl}/${normalizedTracePath}`, "_blank");
1273
- }
1274
- };
1275
- window.openMarkdown = (button) => {
1276
- const path = button.getAttribute('data-markdown');
1277
- if (path) {
1278
- const normalizedPath = path.replace(/\\/g, '/');
1279
- const baseUrl = Utils.getAdjustedBaseUrl();
1280
- const fullUrl = `${baseUrl.replace(/\/$/, '')}/${normalizedPath.replace(/^\//, '')}`;
1281
- window.open(fullUrl, '_blank');
1282
- }
1283
- };
1284
- window.getAdjustedBaseUrl = Utils.getAdjustedBaseUrl;
1285
- window.copyFilePath = Utils.copyFilePath
1286
- window.openHistory = () => {
1287
- historyManager.setCurrentHistory(testDetailsManager.testHistoriesMap, testDetailsManager.testHistoryTitle);
1288
- historyManager.openHistory();
1289
- };
1290
- window.closeHistoryModal = () => historyManager.closeHistoryModal();
1291
- window.showHistoryErrorMessage = (modalId) => historyManager.showHistoryErrorMessage(modalId);
1292
- });
1293
- </script>
1294
- </body>
1295
- </html>