ortoni-report 2.0.8 → 2.0.9

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,7 +1,7 @@
1
1
  <head>
2
2
  <meta charset="UTF-8">
3
3
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
4
- <meta name="description" content="Playwright HTML report by LetCode Koushik - V2.0.8">
4
+ <meta name="description" content="Playwright HTML report by LetCode Koushik - V2.0.9">
5
5
  <title>{{title}}</title>
6
6
  <link rel="icon" href="https://raw.githubusercontent.com/ortoniKC/ortoni-report/refs/heads/main/favicon.png"
7
7
  type="image/x-icon">
@@ -4,6 +4,7 @@
4
4
  <style>
5
5
  {{{inlineCss}}}
6
6
  </style>
7
+
7
8
  <body>
8
9
  {{> navbar }}
9
10
  <section class="section mt-6">
@@ -47,7 +48,7 @@
47
48
  htmlElement: document.documentElement,
48
49
  searchInput: document.querySelector('input[name="search"]'),
49
50
  detailsElements: document.querySelectorAll('details'),
50
- filtersDisplay: document.getElementById('selected-filters')
51
+ filtersDisplay: document.getElementById('selected-filters'),
51
52
  };
52
53
 
53
54
  const themeManager = {
@@ -141,7 +142,6 @@
141
142
  ${this.renderTestContent(test)}
142
143
  </div>
143
144
  `;
144
-
145
145
  this.attachScreenshotListeners(test);
146
146
  this.attachSteps(test);
147
147
  },
@@ -168,9 +168,7 @@
168
168
  content += this.renderScreenshotsAndVideo(test);
169
169
  }
170
170
  content += this.renderAdditionalInfo(test);
171
- content += this.renderSteps(test);
172
- content += this.renderErrors(test);
173
- content += this.renderLogs(test);
171
+ content += this.renderTabs(test);
174
172
  return content;
175
173
  },
176
174
  renderScreenshotsAndVideo(test) {
@@ -242,7 +240,7 @@
242
240
  ` : ''}
243
241
  </div>
244
242
  `;
245
-
243
+
246
244
  content += '</div></div>';
247
245
  return content;
248
246
  },
@@ -262,8 +260,8 @@
262
260
  </div>
263
261
  </div>` : ""}
264
262
  ${test.annotations
265
- .filter(annotation => annotation !== null && annotation !== undefined)
266
- .map(annotation => `
263
+ .filter(annotation => annotation !== null && annotation !== undefined)
264
+ .map(annotation => `
267
265
  <div class="mb-4">
268
266
  ${annotation?.type ? `<strong class="has-text-link">Type: </strong><span>${annotation.type}</span>` : ''}
269
267
  <br>
@@ -275,48 +273,57 @@
275
273
  </div>
276
274
  `;
277
275
  },
278
- renderSteps(test) {
279
- if (test.steps.length === 0) return '';
276
+
277
+ renderTabs(test) {
280
278
  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>
279
+ <div class="card mt-5">
280
+ <div class="card-content">
281
+ <div class="tabs is-boxed is-fullwidth">
282
+ <ul>
283
+ <li class="is-active"><a data-tab="steps">Steps</a></li>
284
+ <li><a data-tab="errors">Errors</a></li>
285
+ <li><a data-tab="logs">Logs</a></li>
286
+ </ul>
287
+ </div>
288
+ <div id="tabContent">
289
+ <div id="stepsTab" class="tab-content">
290
+ ${this.renderSteps(test)}
291
+ </div>
292
+ <div id="errorsTab" class="tab-content" style="display: none;">
293
+ ${this.renderErrors(test)}
294
+ </div>
295
+ <div id="logsTab" class="tab-content" style="display: none;">
296
+ ${this.renderLogs(test)}
288
297
  </div>
289
298
  </div>
290
299
  </div>
300
+ </div>
301
+ `;
302
+ },
303
+
304
+ renderSteps(test) {
305
+ if (test.steps.length === 0) return '<p>No steps available.</p>';
306
+ return `
307
+ <div class="content">
308
+ <span id="stepDetails" class="content"></span>
309
+ </div>
291
310
  `;
292
311
  },
312
+
293
313
  renderErrors(test) {
294
- if (!test.errors.length) return '';
314
+ if (!test.errors.length) return '<p>No errors reported.</p>';
295
315
  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>
316
+ <div class="content">
317
+ <pre><code class="data-lang=js">${test.errors.join('\n')}</code></pre>
305
318
  </div>
306
319
  `;
307
320
  },
321
+
308
322
  renderLogs(test) {
309
- if (!test.logs) return '';
323
+ if (!test.logs) return '<p>No logs available.</p>';
310
324
  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>
325
+ <div class="content">
326
+ <pre>${test.logs}</pre>
320
327
  </div>
321
328
  `;
322
329
  },
@@ -378,151 +385,166 @@
378
385
  const test = testData[testId];
379
386
  const historyEntry = testHistory.find(entry => entry.testId === testHistoryId);
380
387
  testHistoriesMap = historyEntry ? historyEntry.history : null;
381
- testHistoryTitle = historyEntry.testId ? historyEntry.testId.split(":")[2] : '';
388
+ testHistoryTitle = historyEntry.testId ? historyEntry.testId.split(":")[2] : '';
382
389
  this.show(test);
383
390
  });
384
391
  });
385
- }
386
- };
392
+ document.addEventListener('click', (e) => {
393
+ const tabLink = e.target.closest('div.tabs a');
394
+ if (tabLink) {
395
+ e.preventDefault();
396
+ const tabId = e.target.getAttribute('data-tab');
397
+ const tabLinks = document.querySelectorAll('div.tabs a');
398
+ const tabContents = document.querySelectorAll('div.tab-content');
387
399
 
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
- });
400
+ tabLinks.forEach(l => l.parentElement.classList.remove('is-active'));
401
+ tabContents.forEach(c => c.style.display = 'none');
397
402
 
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
- });
403
+ e.target.parentElement.classList.add('is-active');
404
+ document.getElementById(`${tabId}Tab`).style.display = 'block';
405
+ }
405
406
  });
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';
407
+ }
408
+ };
411
409
 
412
- elements.detailsElements.forEach(details => {
413
- const items = details.querySelectorAll('div[data-test-id]');
414
- let shouldShowDetails = false;
410
+ const filterManager = {
411
+ init() {
412
+ this.attachEventListeners();
413
+ },
414
+ attachEventListeners() {
415
+ const checkboxes = document.querySelectorAll('#select-filter input[type="checkbox"]');
416
+ checkboxes.forEach(checkbox => {
417
+ checkbox.addEventListener('change', () => this.applyFilters());
418
+ });
415
419
 
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
+ const filters = document.querySelectorAll('.filter');
421
+ filters.forEach(filter => {
422
+ filter.addEventListener('click', () => {
423
+ filters.forEach(f => f.classList.remove('active'));
424
+ filter.classList.add('active');
425
+ this.applyFilters();
426
+ });
420
427
  });
428
+ },
429
+ applyFilters() {
430
+ const selectedProjects = this.getSelectedValues('project');
431
+ const selectedTags = this.getSelectedValues('test-tags');
432
+ const selectedStatus = document.querySelector('.filter.active')?.getAttribute('data-status') || 'all';
421
433
 
422
- details.open = shouldShowDetails;
423
- details.classList.toggle('is-hidden', !shouldShowDetails);
424
- });
434
+ elements.detailsElements.forEach(details => {
435
+ const items = details.querySelectorAll('div[data-test-id]');
436
+ let shouldShowDetails = false;
425
437
 
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();
438
+ items.forEach(item => {
439
+ const isVisible = this.shouldShowItem(item, selectedProjects, selectedTags, selectedStatus);
440
+ item.classList.toggle('is-hidden', !isVisible);
441
+ shouldShowDetails = shouldShowDetails || isVisible;
442
+ });
436
443
 
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);
444
+ details.open = shouldShowDetails;
445
+ details.classList.toggle('is-hidden', !shouldShowDetails);
446
+ });
440
447
 
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
- };
448
+ this.updateSelectedFiltersDisplay(selectedProjects, selectedTags, selectedStatus);
449
+ },
450
+ getSelectedValues(type) {
451
+ return Array.from(document.querySelectorAll(`#select-filter input[type="checkbox"][data-filter-type="${type}"]:checked`))
452
+ .map(checkbox => checkbox.value.trim());
453
+ },
454
+ shouldShowItem(item, projects, tags, status) {
455
+ const testTags = item.getAttribute('data-test-tags').trim().split(' ').filter(Boolean);
456
+ const projectName = item.getAttribute('data-project-name').trim();
457
+ const testStatus = item.getAttribute('data-test-status').trim();
458
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]');
459
+ const matchesProject = projects.length === 0 || projects.includes(projectName);
460
+ const matchesTags = tags.length === 0 || tags.every(tag => testTags.includes(tag));
461
+ const matchesStatus = this.matchesStatus(testStatus, status);
466
462
 
467
- elements.detailsElements.forEach(detail => detail.open = !!searchTerm);
463
+ return matchesProject && matchesTags && matchesStatus;
464
+ },
465
+ matchesStatus(testStatus, selectedStatus) {
466
+ if (selectedStatus === 'all') return testStatus !== 'skipped';
467
+ if (selectedStatus === 'failed') return testStatus === 'failed' || testStatus === 'timedOut';
468
+ if (selectedStatus === 'retry') return testStatus.includes('retry');
469
+ if (selectedStatus === 'flaky') return testStatus.includes('flaky');
470
+ return testStatus === selectedStatus;
471
+ },
472
+ updateSelectedFiltersDisplay(projects, tags, status) {
473
+ let displayText = [];
474
+ if (projects.length > 0) displayText.push(`Projects: ${projects.join(', ')}`);
475
+ if (tags.length > 0) displayText.push(`Tags: ${tags.join(', ')}`);
476
+ if (status !== 'all') displayText.push(`Status: ${status}`);
477
+ elements.filtersDisplay.innerHTML = displayText.length > 0 ? displayText.join(' | ') : 'All Tests';
478
+ }
479
+ };
468
480
 
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;
481
+ const searchManager = {
482
+ init() {
483
+ elements.searchInput.addEventListener('input', this.debounce(this.filterTests, 300));
484
+ },
485
+ filterTests(event) {
486
+ const searchTerm = event.target.value.toLowerCase();
487
+ const testItems = document.querySelectorAll('[data-test-id]');
488
+
489
+ elements.detailsElements.forEach(detail => detail.open = !!searchTerm);
490
+
491
+ testItems.forEach(item => {
492
+ const isVisible = item.textContent.toLowerCase().includes(searchTerm);
493
+ item.style.display = isVisible ? 'flex' : 'none';
494
+ if (isVisible && searchTerm) searchManager.openParentDetails(item);
495
+ });
496
+ },
497
+ openParentDetails(item) {
498
+ let parent = item.parentElement;
499
+ while (parent && parent.tagName !== 'ASIDE') {
500
+ if (parent.tagName === 'DETAILS') parent.open = true;
501
+ parent = parent.parentElement;
502
+ }
503
+ },
504
+ debounce(func, wait) {
505
+ let timeout;
506
+ return function (...args) {
507
+ clearTimeout(timeout);
508
+ timeout = setTimeout(() => func.apply(this, args), wait);
509
+ };
480
510
  }
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
- };
511
+ };
490
512
 
491
- // Initialize all managers
492
- themeManager.init();
493
- testDetailsManager.attachEventListeners();
494
- filterManager.init();
495
- searchManager.init();
513
+ // Initialize all managers
514
+ themeManager.init();
515
+ testDetailsManager.attachEventListeners();
516
+ filterManager.init();
517
+ searchManager.init();
496
518
 
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}`;
519
+ // Expose necessary functions to the global scope
520
+ window.showSummary = testDetailsManager.hide;
521
+ window.openModal = () => document.querySelector("#testImage").classList.add("is-active");
522
+ window.closeModal = () => document.querySelector("#testImage").classList.remove("is-active");
523
+ window.closeErrorModal = (modalId) => document.getElementById(modalId).classList.remove("is-active");
524
+ window.openTraceViewer = (button) => {
525
+ const tracePath = button.getAttribute("data-trace");
526
+ if (tracePath) {
527
+ const normalizedTracePath = tracePath.replace(/\\/g, '/');
528
+ const baseUrl = getAdjustedBaseUrl();
529
+ window.open(`${baseUrl}/trace/index.html?trace=${baseUrl}/${normalizedTracePath}`, "_blank");
530
+ }
531
+ };
532
+ window.getAdjustedBaseUrl = () => {
533
+ const origin = window.location.origin;
534
+ const pathname = window.location.pathname;
535
+ if (pathname.endsWith('.html')) {
536
+ const directoryPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
537
+ return `${origin}${directoryPath}`;
538
+ }
539
+ return origin;
516
540
  }
517
- return origin;
518
- }
519
- window.openHistory = () => {
520
- const historyElement = document.getElementById("historyModal");
521
- historyElement.classList.add('is-active');
541
+ window.openHistory = () => {
542
+ const historyElement = document.getElementById("historyModal");
543
+ historyElement.classList.add('is-active');
522
544
 
523
- let historyContent = '';
524
- if (testHistoriesMap && testHistoriesMap.length > 0) {
525
- historyContent = testHistoriesMap.map((h, index) => `
545
+ let historyContent = '';
546
+ if (testHistoriesMap && testHistoriesMap.length > 0) {
547
+ historyContent = testHistoriesMap.map((h, index) => `
526
548
  <tr>
527
549
  <td>${h.run_date}</td>
528
550
  <td>${h.status}</td>
@@ -536,11 +558,11 @@
536
558
  </div><a class="button is-link" onclick="showHistoryErrorMessage(${index})">Show error</a></td>` : '<td>No Error</td>'}
537
559
  </tr>
538
560
  `).join('');
539
- } else {
540
- historyContent = '<p class="title">No history available</p>';
541
- }
561
+ } else {
562
+ historyContent = '<p class="title">No history available</p>';
563
+ }
542
564
 
543
- historyElement.innerHTML = `
565
+ historyElement.innerHTML = `
544
566
  <div class="modal-background"></div>
545
567
  <div class="modal-card">
546
568
  <header class="modal-card-head">
@@ -564,14 +586,15 @@
564
586
  </section>
565
587
  </div>
566
588
  `;
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
- });
589
+ };
590
+ window.closeHistoryModal = () => {
591
+ document.getElementById("historyModal").classList.remove('is-active');
592
+ };
593
+ window.showHistoryErrorMessage = (modalId) => {
594
+ document.getElementById(modalId)?.classList.add('is-active');
595
+ };
596
+ });
575
597
  </script>
576
598
  </body>
599
+
577
600
  </html>
@@ -1,8 +1,8 @@
1
1
  {{#if (gr statusCount) }}
2
2
  <div class="column is-one-sixth">
3
3
  <div class="box has-background-{{bg}} filter" data-status="{{status}}">
4
- <p class="has-text-white is-size-6">{{statusHeader}}</p>
5
- <p class="has-text-white is-size-4">{{statusCount}}</p>
4
+ <p class="title is-size-3 has-text-white mb-0">{{statusCount}}</p>
5
+ <p class="subtitle is-size-7 has-text-white-bis">{{statusHeader}}</p>
6
6
  </div>
7
7
  </div>
8
8
  {{/if}}
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "ortoni-report",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Playwright Report By LetCode with Koushik",
5
5
  "scripts": {
6
6
  "tsc": "tsc",
7
7
  "build": "tsup",
8
8
  "release": "npm publish"
9
9
  },
10
+ "bin": {
11
+ "ortoni-report": "./dist/cli/cli.js"
12
+ },
10
13
  "files": [
11
14
  "dist",
12
15
  "README.md",
@@ -31,23 +34,24 @@
31
34
  },
32
35
  "homepage": "https://github.com/ortoniKC/ortoni-report#readme",
33
36
  "devDependencies": {
34
- "@playwright/test": "^1.44.1",
37
+ "@playwright/test": "^1.49.0",
35
38
  "@types/express": "^5.0.0",
36
39
  "@types/node": "^22.0.2",
37
40
  "@types/sqlite3": "^3.1.11",
38
41
  "@types/ws": "^8.5.12",
39
42
  "ansi-to-html": "^0.7.2",
43
+ "commander": "^12.1.0",
40
44
  "express": "^4.21.1",
41
45
  "handlebars": "^4.7.8",
42
46
  "tsup": "^6.5.0",
43
47
  "typescript": "^4.9.4"
44
48
  },
45
49
  "peerDependencies": {
46
- "ws": "^8.18.0",
47
50
  "sqlite": "^5.1.1",
48
- "sqlite3": "^5.1.7"
51
+ "sqlite3": "^5.1.7",
52
+ "ws": "^8.18.0"
49
53
  },
50
54
  "main": "dist/ortoni-report.js",
51
55
  "module": "dist/ortoni-report.mjs",
52
56
  "types": "dist/ortoni-report.d.ts"
53
- }
57
+ }