@arghajit/dummy 0.1.0-beta-14 → 0.1.0-beta-15

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.1.0-beta-14",
4
+ "version": "0.1.0-beta-15",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "keywords": [
7
7
  "playwright",
@@ -67,7 +67,6 @@
67
67
  "@types/node": "^20",
68
68
  "@types/ua-parser-js": "^0.7.39",
69
69
  "eslint": "9.25.1",
70
- "pretty-ansi": "^3.0.0",
71
70
  "typescript": "^5"
72
71
  },
73
72
  "engines": {
@@ -177,9 +177,11 @@ function generateMinifiedHTML(reportData) {
177
177
  };
178
178
 
179
179
  const testsByBrowser = new Map();
180
+ const allBrowsers = new Set();
180
181
  if (results && results.length > 0) {
181
182
  results.forEach((test) => {
182
183
  const browser = test.browser || "unknown";
184
+ allBrowsers.add(browser);
183
185
  if (!testsByBrowser.has(browser)) {
184
186
  testsByBrowser.set(browser, []);
185
187
  }
@@ -195,7 +197,9 @@ function generateMinifiedHTML(reportData) {
195
197
  let html = "";
196
198
  testsByBrowser.forEach((tests, browser) => {
197
199
  html += `
198
- <div class="browser-section">
200
+ <div class="browser-section" data-browser-group="${sanitizeHTML(
201
+ browser.toLowerCase()
202
+ )}">
199
203
  <h2 class="browser-title">${sanitizeHTML(capitalize(browser))}</h2>
200
204
  <ul class="test-list">
201
205
  `;
@@ -204,7 +208,12 @@ function generateMinifiedHTML(reportData) {
204
208
  const testTitle =
205
209
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
206
210
  html += `
207
- <li class="test-item ${getStatusClass(test.status)}">
211
+ <li class="test-item ${getStatusClass(test.status)}"
212
+ data-test-name-min="${sanitizeHTML(testTitle.toLowerCase())}"
213
+ data-status-min="${sanitizeHTML(
214
+ String(test.status).toLowerCase()
215
+ )}"
216
+ data-browser-min="${sanitizeHTML(browser.toLowerCase())}">
208
217
  <span class="test-status-icon">${getStatusIcon(
209
218
  test.status
210
219
  )}</span>
@@ -343,6 +352,43 @@ function generateMinifiedHTML(reportData) {
343
352
  padding-bottom: 10px;
344
353
  border-bottom: 2px solid var(--secondary-color);
345
354
  }
355
+
356
+ /* Filters Section */
357
+ .filters-section {
358
+ display: flex;
359
+ flex-wrap: wrap;
360
+ gap: 15px;
361
+ margin-bottom: 20px;
362
+ padding: 15px;
363
+ background-color: var(--light-gray-color);
364
+ border-radius: var(--border-radius);
365
+ border: 1px solid var(--border-color);
366
+ }
367
+ .filters-section input[type="text"],
368
+ .filters-section select {
369
+ padding: 8px 12px;
370
+ border: 1px solid var(--medium-gray-color);
371
+ border-radius: 4px;
372
+ font-size: 0.95em;
373
+ flex-grow: 1;
374
+ }
375
+ .filters-section select {
376
+ min-width: 150px;
377
+ }
378
+ .filters-section button {
379
+ padding: 8px 15px;
380
+ font-size: 0.95em;
381
+ background-color: var(--secondary-color);
382
+ color: white;
383
+ border: none;
384
+ border-radius: 4px;
385
+ cursor: pointer;
386
+ transition: background-color 0.2s ease;
387
+ }
388
+ .filters-section button:hover {
389
+ background-color: var(--primary-color);
390
+ }
391
+
346
392
  .browser-section {
347
393
  margin-bottom: 25px;
348
394
  }
@@ -365,7 +411,7 @@ function generateMinifiedHTML(reportData) {
365
411
  border: 1px solid var(--border-color);
366
412
  border-radius: var(--border-radius);
367
413
  background-color: #fff;
368
- transition: background-color 0.2s ease;
414
+ transition: background-color 0.2s ease, display 0.3s ease-out;
369
415
  }
370
416
  .test-item:hover {
371
417
  background-color: var(--light-gray-color);
@@ -424,10 +470,11 @@ function generateMinifiedHTML(reportData) {
424
470
  .report-header { flex-direction: column; align-items: flex-start; gap: 10px; }
425
471
  .report-header h1 { font-size: 1.5em; }
426
472
  .run-info { text-align: left; }
427
- .summary-stats { grid-template-columns: 1fr 1fr; } /* Two cards per row on smaller screens */
473
+ .summary-stats { grid-template-columns: 1fr 1fr; }
474
+ .filters-section { flex-direction: column; }
428
475
  }
429
476
  @media (max-width: 480px) {
430
- .summary-stats { grid-template-columns: 1fr; } /* One card per row on very small screens */
477
+ .summary-stats { grid-template-columns: 1fr; }
431
478
  }
432
479
  </style>
433
480
  </head>
@@ -471,6 +518,30 @@ function generateMinifiedHTML(reportData) {
471
518
 
472
519
  <section class="test-results-section">
473
520
  <h1 class="section-title">Test Case Summary</h1>
521
+
522
+ <div class="filters-section">
523
+ <input type="text" id="filter-min-name" placeholder="Search by test name...">
524
+ <select id="filter-min-status">
525
+ <option value="">All Statuses</option>
526
+ <option value="passed">Passed</option>
527
+ <option value="failed">Failed</option>
528
+ <option value="skipped">Skipped</option>
529
+ <option value="unknown">Unknown</option>
530
+ </select>
531
+ <select id="filter-min-browser">
532
+ <option value="">All Browsers</option>
533
+ ${Array.from(allBrowsers)
534
+ .map(
535
+ (browser) =>
536
+ `<option value="${sanitizeHTML(
537
+ browser.toLowerCase()
538
+ )}">${sanitizeHTML(capitalize(browser))}</option>`
539
+ )
540
+ .join("")}
541
+ </select>
542
+ <button id="clear-min-filters">Clear Filters</button>
543
+ </div>
544
+
474
545
  ${generateTestListHTML()}
475
546
  </section>
476
547
 
@@ -485,16 +556,83 @@ function generateMinifiedHTML(reportData) {
485
556
  </footer>
486
557
  </div>
487
558
  <script>
488
- // Global helper functions needed by the template (if any complex ones were used)
489
- // For this minified version, formatDuration and formatDate are primarily used during HTML generation server-side.
490
- // No client-side interactivity scripts are needed for this simple report.
559
+ document.addEventListener('DOMContentLoaded', function() {
560
+ const nameFilterMin = document.getElementById('filter-min-name');
561
+ const statusFilterMin = document.getElementById('filter-min-status');
562
+ const browserFilterMin = document.getElementById('filter-min-browser');
563
+ const clearMinFiltersBtn = document.getElementById('clear-min-filters');
564
+ const testItemsMin = document.querySelectorAll('.test-results-section .test-item');
565
+ const browserSections = document.querySelectorAll('.test-results-section .browser-section');
566
+
567
+ function filterMinifiedTests() {
568
+ const nameValue = nameFilterMin.value.toLowerCase();
569
+ const statusValue = statusFilterMin.value;
570
+ const browserValue = browserFilterMin.value;
571
+ let anyBrowserSectionVisible = false;
572
+
573
+ browserSections.forEach(section => {
574
+ let sectionHasVisibleTests = false;
575
+ const testsInThisSection = section.querySelectorAll('.test-item');
576
+
577
+ testsInThisSection.forEach(testItem => {
578
+ const testName = testItem.getAttribute('data-test-name-min');
579
+ const testStatus = testItem.getAttribute('data-status-min');
580
+ const testBrowser = testItem.getAttribute('data-browser-min');
581
+
582
+ const nameMatch = testName.includes(nameValue);
583
+ const statusMatch = !statusValue || testStatus === statusValue;
584
+ const browserMatch = !browserValue || testBrowser === browserValue;
585
+
586
+ if (nameMatch && statusMatch && browserMatch) {
587
+ testItem.style.display = 'flex';
588
+ sectionHasVisibleTests = true;
589
+ anyBrowserSectionVisible = true;
590
+ } else {
591
+ testItem.style.display = 'none';
592
+ }
593
+ });
594
+ // Hide browser section if no tests match OR if a specific browser is selected and it's not this one
595
+ if (!sectionHasVisibleTests || (browserValue && section.getAttribute('data-browser-group') !== browserValue)) {
596
+ section.style.display = 'none';
597
+ } else {
598
+ section.style.display = '';
599
+ }
600
+ });
601
+
602
+ // Show "no tests" message if all sections are hidden
603
+ const noTestsMessage = document.querySelector('.test-results-section .no-tests');
604
+ if (noTestsMessage) {
605
+ noTestsMessage.style.display = anyBrowserSectionVisible ? 'none' : 'block';
606
+ }
607
+
608
+ }
609
+
610
+ if (nameFilterMin) nameFilterMin.addEventListener('input', filterMinifiedTests);
611
+ if (statusFilterMin) statusFilterMin.addEventListener('change', filterMinifiedTests);
612
+ if (browserFilterMin) browserFilterMin.addEventListener('change', filterMinifiedTests);
613
+
614
+ if (clearMinFiltersBtn) {
615
+ clearMinFiltersBtn.addEventListener('click', () => {
616
+ nameFilterMin.value = '';
617
+ statusFilterMin.value = '';
618
+ browserFilterMin.value = '';
619
+ filterMinifiedTests();
620
+ });
621
+ }
622
+ // Initial filter call in case of pre-filled values (though unlikely here)
623
+ if (testItemsMin.length > 0) { // Only filter if there are items
624
+ filterMinifiedTests();
625
+ }
626
+ });
627
+
628
+ // Fallback helper functions (though ideally not needed client-side for this minified report)
491
629
  if (typeof formatDuration === 'undefined') {
492
- function formatDuration(ms) { // Fallback, though should be pre-rendered
630
+ function formatDuration(ms) {
493
631
  if (ms === undefined || ms === null || ms < 0) return "0.0s";
494
632
  return (ms / 1000).toFixed(1) + "s";
495
633
  }
496
634
  }
497
- if (typeof formatDate === 'undefined') { // Fallback
635
+ if (typeof formatDate === 'undefined') {
498
636
  function formatDate(dateStrOrDate) {
499
637
  if (!dateStrOrDate) return "N/A";
500
638
  try {
@@ -5,7 +5,6 @@ import { readFileSync, existsSync as fsExistsSync } from "fs"; // ADD THIS LINE
5
5
  import path from "path";
6
6
  import { fork } from "child_process"; // Add this
7
7
  import { fileURLToPath } from "url"; // Add this for resolving path in ESM
8
- import prettyAnsi from "pretty-ansi";
9
8
 
10
9
  // Use dynamic import for chalk as it's ESM only
11
10
  let chalk;
@@ -27,6 +26,129 @@ const DEFAULT_OUTPUT_DIR = "pulse-report";
27
26
  const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
28
27
  const DEFAULT_HTML_FILE = "playwright-pulse-static-report.html";
29
28
  // Helper functions
29
+ export function ansiToHtml(text) {
30
+ if (!text) {
31
+ return "";
32
+ }
33
+
34
+ const codes = {
35
+ 0: "color:inherit;font-weight:normal;font-style:normal;text-decoration:none;opacity:1;background-color:inherit;",
36
+ 1: "font-weight:bold",
37
+ 2: "opacity:0.6",
38
+ 3: "font-style:italic",
39
+ 4: "text-decoration:underline",
40
+ 30: "color:#000", // black
41
+ 31: "color:#d00", // red
42
+ 32: "color:#0a0", // green
43
+ 33: "color:#aa0", // yellow
44
+ 34: "color:#00d", // blue
45
+ 35: "color:#a0a", // magenta
46
+ 36: "color:#0aa", // cyan
47
+ 37: "color:#aaa", // light grey
48
+ 39: "color:inherit", // default foreground color
49
+ 40: "background-color:#000", // black background
50
+ 41: "background-color:#d00", // red background
51
+ 42: "background-color:#0a0", // green background
52
+ 43: "background-color:#aa0", // yellow background
53
+ 44: "background-color:#00d", // blue background
54
+ 45: "background-color:#a0a", // magenta background
55
+ 46: "background-color:#0aa", // cyan background
56
+ 47: "background-color:#aaa", // light grey background
57
+ 49: "background-color:inherit", // default background color
58
+ 90: "color:#555", // dark grey
59
+ 91: "color:#f55", // light red
60
+ 92: "color:#5f5", // light green
61
+ 93: "color:#ff5", // light yellow
62
+ 94: "color:#55f", // light blue
63
+ 95: "color:#f5f", // light magenta
64
+ 96: "color:#5ff", // light cyan
65
+ 97: "color:#fff", // white
66
+ };
67
+
68
+ let currentStylesArray = [];
69
+ let html = "";
70
+ let openSpan = false;
71
+
72
+ const applyStyles = () => {
73
+ if (openSpan) {
74
+ html += "</span>";
75
+ openSpan = false;
76
+ }
77
+ if (currentStylesArray.length > 0) {
78
+ const styleString = currentStylesArray.filter((s) => s).join(";");
79
+ if (styleString) {
80
+ html += `<span style="${styleString}">`;
81
+ openSpan = true;
82
+ }
83
+ }
84
+ };
85
+
86
+ const resetAndApplyNewCodes = (newCodesStr) => {
87
+ const newCodes = newCodesStr.split(";");
88
+
89
+ if (newCodes.includes("0")) {
90
+ currentStylesArray = [];
91
+ if (codes["0"]) currentStylesArray.push(codes["0"]);
92
+ }
93
+
94
+ for (const code of newCodes) {
95
+ if (code === "0") continue;
96
+
97
+ if (codes[code]) {
98
+ if (code === "39") {
99
+ currentStylesArray = currentStylesArray.filter(
100
+ (s) => !s.startsWith("color:")
101
+ );
102
+ currentStylesArray.push("color:inherit");
103
+ } else if (code === "49") {
104
+ currentStylesArray = currentStylesArray.filter(
105
+ (s) => !s.startsWith("background-color:")
106
+ );
107
+ currentStylesArray.push("background-color:inherit");
108
+ } else {
109
+ currentStylesArray.push(codes[code]);
110
+ }
111
+ } else if (code.startsWith("38;2;") || code.startsWith("48;2;")) {
112
+ const parts = code.split(";");
113
+ const type = parts[0] === "38" ? "color" : "background-color";
114
+ if (parts.length === 5) {
115
+ currentStylesArray = currentStylesArray.filter(
116
+ (s) => !s.startsWith(type + ":")
117
+ );
118
+ currentStylesArray.push(
119
+ `${type}:rgb(${parts[2]},${parts[3]},${parts[4]})`
120
+ );
121
+ }
122
+ }
123
+ }
124
+ applyStyles();
125
+ };
126
+
127
+ const segments = text.split(/(\x1b\[[0-9;]*m)/g);
128
+
129
+ for (const segment of segments) {
130
+ if (!segment) continue;
131
+
132
+ if (segment.startsWith("\x1b[") && segment.endsWith("m")) {
133
+ const command = segment.slice(2, -1);
134
+ resetAndApplyNewCodes(command);
135
+ } else {
136
+ const escapedContent = segment
137
+ .replace(/&/g, "&amp;")
138
+ .replace(/</g, "&lt;")
139
+ .replace(/>/g, "&gt;")
140
+ .replace(/"/g, "&quot;")
141
+ .replace(/'/g, "&#039;");
142
+ html += escapedContent;
143
+ }
144
+ }
145
+
146
+ if (openSpan) {
147
+ html += "</span>";
148
+ }
149
+
150
+ return html;
151
+ }
30
152
  function sanitizeHTML(str) {
31
153
  if (str === null || str === undefined) return "";
32
154
  return String(str).replace(/[&<>"']/g, (match) => {
@@ -45,7 +167,7 @@ function capitalize(str) {
45
167
  return str[0].toUpperCase() + str.slice(1).toLowerCase();
46
168
  }
47
169
  function formatPlaywrightError(error) {
48
- const commandOutput = prettyAnsi(error || error.message);
170
+ const commandOutput = ansiToHtml(error || error.message);
49
171
  return convertPlaywrightErrorToHTML(commandOutput);
50
172
  }
51
173
  function convertPlaywrightErrorToHTML(str) {
@@ -585,6 +707,335 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
585
707
  </div>
586
708
  `;
587
709
  }
710
+ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
711
+ // Format memory for display
712
+ const formattedMemory = environment.memory.replace(/(\d+\.\d{2})GB/, "$1 GB");
713
+
714
+ // Generate a unique ID for the dashboard
715
+ const dashboardId = `envDashboard-${Date.now()}-${Math.random()
716
+ .toString(36)
717
+ .substring(2, 7)}`;
718
+
719
+ const cardHeight = Math.floor(dashboardHeight * 0.44);
720
+ const cardContentPadding = 16; // px
721
+
722
+ return `
723
+ <div class="environment-dashboard-wrapper" id="${dashboardId}">
724
+ <style>
725
+ .environment-dashboard-wrapper *,
726
+ .environment-dashboard-wrapper *::before,
727
+ .environment-dashboard-wrapper *::after {
728
+ box-sizing: border-box;
729
+ }
730
+
731
+ .environment-dashboard-wrapper {
732
+ --primary-color: #007bff;
733
+ --primary-light-color: #e6f2ff;
734
+ --secondary-color: #6c757d;
735
+ --success-color: #28a745;
736
+ --success-light-color: #eaf6ec;
737
+ --warning-color: #ffc107;
738
+ --warning-light-color: #fff9e6;
739
+ --danger-color: #dc3545;
740
+
741
+ --background-color: #ffffff;
742
+ --card-background-color: #ffffff;
743
+ --text-color: #212529;
744
+ --text-color-secondary: #6c757d;
745
+ --border-color: #dee2e6;
746
+ --border-light-color: #f1f3f5;
747
+ --icon-color: #495057;
748
+ --chip-background: #e9ecef;
749
+ --chip-text: #495057;
750
+ --shadow-color: rgba(0, 0, 0, 0.075);
751
+
752
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
753
+ background-color: var(--background-color);
754
+ border-radius: 12px;
755
+ box-shadow: 0 6px 12px var(--shadow-color);
756
+ padding: 24px;
757
+ color: var(--text-color);
758
+ display: grid;
759
+ grid-template-columns: 1fr 1fr;
760
+ grid-template-rows: auto 1fr;
761
+ gap: 20px;
762
+ font-size: 14px;
763
+ }
764
+
765
+ .env-dashboard-header {
766
+ grid-column: 1 / -1;
767
+ display: flex;
768
+ justify-content: space-between;
769
+ align-items: center;
770
+ border-bottom: 1px solid var(--border-color);
771
+ padding-bottom: 16px;
772
+ margin-bottom: 8px;
773
+ }
774
+
775
+ .env-dashboard-title {
776
+ font-size: 1.5rem;
777
+ font-weight: 600;
778
+ color: var(--text-color);
779
+ margin: 0;
780
+ }
781
+
782
+ .env-dashboard-subtitle {
783
+ font-size: 0.875rem;
784
+ color: var(--text-color-secondary);
785
+ margin-top: 4px;
786
+ }
787
+
788
+ .env-card {
789
+ background-color: var(--card-background-color);
790
+ border-radius: 8px;
791
+ padding: ${cardContentPadding}px;
792
+ box-shadow: 0 3px 6px var(--shadow-color);
793
+ height: ${cardHeight}px;
794
+ display: flex;
795
+ flex-direction: column;
796
+ overflow: hidden;
797
+ }
798
+
799
+ .env-card-header {
800
+ font-weight: 600;
801
+ font-size: 1rem;
802
+ margin-bottom: 12px;
803
+ color: var(--text-color);
804
+ display: flex;
805
+ align-items: center;
806
+ padding-bottom: 8px;
807
+ border-bottom: 1px solid var(--border-light-color);
808
+ }
809
+
810
+ .env-card-header svg {
811
+ margin-right: 10px;
812
+ width: 18px;
813
+ height: 18px;
814
+ fill: var(--icon-color);
815
+ }
816
+
817
+ .env-card-content {
818
+ flex-grow: 1;
819
+ overflow-y: auto;
820
+ padding-right: 5px;
821
+ }
822
+
823
+ .env-detail-row {
824
+ display: flex;
825
+ justify-content: space-between;
826
+ align-items: center;
827
+ padding: 10px 0;
828
+ border-bottom: 1px solid var(--border-light-color);
829
+ font-size: 0.875rem;
830
+ }
831
+
832
+ .env-detail-row:last-child {
833
+ border-bottom: none;
834
+ }
835
+
836
+ .env-detail-label {
837
+ color: var(--text-color-secondary);
838
+ font-weight: 500;
839
+ margin-right: 10px;
840
+ }
841
+
842
+ .env-detail-value {
843
+ color: var(--text-color);
844
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
845
+ text-align: right;
846
+ word-break: break-all;
847
+ }
848
+
849
+ .env-chip {
850
+ display: inline-block;
851
+ padding: 4px 10px;
852
+ border-radius: 16px;
853
+ font-size: 0.75rem;
854
+ font-weight: 500;
855
+ line-height: 1.2;
856
+ background-color: var(--chip-background);
857
+ color: var(--chip-text);
858
+ }
859
+
860
+ .env-chip-primary {
861
+ background-color: var(--primary-light-color);
862
+ color: var(--primary-color);
863
+ }
864
+
865
+ .env-chip-success {
866
+ background-color: var(--success-light-color);
867
+ color: var(--success-color);
868
+ }
869
+
870
+ .env-chip-warning {
871
+ background-color: var(--warning-light-color);
872
+ color: var(--warning-color);
873
+ }
874
+
875
+ .env-cpu-cores {
876
+ display: flex;
877
+ align-items: center;
878
+ gap: 6px;
879
+ }
880
+
881
+ .env-core-indicator {
882
+ width: 12px;
883
+ height: 12px;
884
+ border-radius: 50%;
885
+ background-color: var(--success-color);
886
+ border: 1px solid rgba(0,0,0,0.1);
887
+ }
888
+
889
+ .env-core-indicator.inactive {
890
+ background-color: var(--border-light-color);
891
+ opacity: 0.7;
892
+ border-color: var(--border-color);
893
+ }
894
+ </style>
895
+
896
+ <div class="env-dashboard-header">
897
+ <div>
898
+ <h3 class="env-dashboard-title">System Environment</h3>
899
+ <p class="env-dashboard-subtitle">Snapshot of the execution environment</p>
900
+ </div>
901
+ <span class="env-chip env-chip-primary">${environment.host}</span>
902
+ </div>
903
+
904
+ <div class="env-card">
905
+ <div class="env-card-header">
906
+ <svg viewBox="0 0 24 24"><path d="M4 6h16V4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h-2v10H4V6zm18-2h-4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2H6a2 2 0 0 0-2 2v2h20V6a2 2 0 0 0-2-2zM8 12h8v2H8v-2zm0 4h8v2H8v-2z"/></svg>
907
+ Hardware
908
+ </div>
909
+ <div class="env-card-content">
910
+ <div class="env-detail-row">
911
+ <span class="env-detail-label">CPU Model</span>
912
+ <span class="env-detail-value">${environment.cpu.model}</span>
913
+ </div>
914
+ <div class="env-detail-row">
915
+ <span class="env-detail-label">CPU Cores</span>
916
+ <span class="env-detail-value">
917
+ <div class="env-cpu-cores">
918
+ ${Array.from(
919
+ { length: Math.max(0, environment.cpu.cores || 0) },
920
+ (_, i) =>
921
+ `<div class="env-core-indicator ${
922
+ i >=
923
+ (environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
924
+ ? "inactive"
925
+ : ""
926
+ }" title="Core ${i + 1}"></div>`
927
+ ).join("")}
928
+ <span>${environment.cpu.cores || "N/A"} cores</span>
929
+ </div>
930
+ </span>
931
+ </div>
932
+ <div class="env-detail-row">
933
+ <span class="env-detail-label">Memory</span>
934
+ <span class="env-detail-value">${formattedMemory}</span>
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <div class="env-card">
940
+ <div class="env-card-header">
941
+ <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-0.01 18c-2.76 0-5.26-1.12-7.07-2.93A7.973 7.973 0 0 1 4 12c0-2.21.9-4.21 2.36-5.64A7.994 7.994 0 0 1 11.99 4c4.41 0 8 3.59 8 8 0 2.76-1.12 5.26-2.93 7.07A7.973 7.973 0 0 1 11.99 20zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/></svg>
942
+ Operating System
943
+ </div>
944
+ <div class="env-card-content">
945
+ <div class="env-detail-row">
946
+ <span class="env-detail-label">OS Type</span>
947
+ <span class="env-detail-value">${
948
+ environment.os.split(" ")[0] === "darwin"
949
+ ? "macOS"
950
+ : environment.os.split(" ")[0] || "Unknown"
951
+ }</span>
952
+ </div>
953
+ <div class="env-detail-row">
954
+ <span class="env-detail-label">OS Version</span>
955
+ <span class="env-detail-value">${
956
+ environment.os.split(" ")[1] || "N/A"
957
+ }</span>
958
+ </div>
959
+ <div class="env-detail-row">
960
+ <span class="env-detail-label">Hostname</span>
961
+ <span class="env-detail-value" title="${environment.host}">${
962
+ environment.host
963
+ }</span>
964
+ </div>
965
+ </div>
966
+ </div>
967
+
968
+ <div class="env-card">
969
+ <div class="env-card-header">
970
+ <svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
971
+ Node.js Runtime
972
+ </div>
973
+ <div class="env-card-content">
974
+ <div class="env-detail-row">
975
+ <span class="env-detail-label">Node Version</span>
976
+ <span class="env-detail-value">${environment.node}</span>
977
+ </div>
978
+ <div class="env-detail-row">
979
+ <span class="env-detail-label">V8 Engine</span>
980
+ <span class="env-detail-value">${environment.v8}</span>
981
+ </div>
982
+ <div class="env-detail-row">
983
+ <span class="env-detail-label">Working Dir</span>
984
+ <span class="env-detail-value" title="${environment.cwd}">${
985
+ environment.cwd.length > 25
986
+ ? "..." + environment.cwd.slice(-22)
987
+ : environment.cwd
988
+ }</span>
989
+ </div>
990
+ </div>
991
+ </div>
992
+
993
+ <div class="env-card">
994
+ <div class="env-card-header">
995
+ <svg viewBox="0 0 24 24"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 8.69 9.48 7 12 7c2.76 0 5 2.24 5 5v1h2c1.66 0 3 1.34 3 3s-1.34 3-3 3z"/></svg>
996
+ System Summary
997
+ </div>
998
+ <div class="env-card-content">
999
+ <div class="env-detail-row">
1000
+ <span class="env-detail-label">Platform Arch</span>
1001
+ <span class="env-detail-value">
1002
+ <span class="env-chip ${
1003
+ environment.os.includes("darwin") &&
1004
+ environment.cpu.model.toLowerCase().includes("apple")
1005
+ ? "env-chip-success"
1006
+ : "env-chip-warning"
1007
+ }">
1008
+ ${
1009
+ environment.os.includes("darwin") &&
1010
+ environment.cpu.model.toLowerCase().includes("apple")
1011
+ ? "Apple Silicon"
1012
+ : environment.cpu.model.toLowerCase().includes("arm") ||
1013
+ environment.cpu.model.toLowerCase().includes("aarch64")
1014
+ ? "ARM-based"
1015
+ : "x86/Other"
1016
+ }
1017
+ </span>
1018
+ </span>
1019
+ </div>
1020
+ <div class="env-detail-row">
1021
+ <span class="env-detail-label">Memory per Core</span>
1022
+ <span class="env-detail-value">${
1023
+ environment.cpu.cores > 0
1024
+ ? (
1025
+ parseFloat(environment.memory) / environment.cpu.cores
1026
+ ).toFixed(2) + " GB"
1027
+ : "N/A"
1028
+ }</span>
1029
+ </div>
1030
+ <div class="env-detail-row">
1031
+ <span class="env-detail-label">Run Context</span>
1032
+ <span class="env-detail-value">CI/Local Test</span>
1033
+ </div>
1034
+ </div>
1035
+ </div>
1036
+ </div>
1037
+ `;
1038
+ }
588
1039
  function generateTestHistoryContent(trendData) {
589
1040
  if (
590
1041
  !trendData ||
@@ -1197,7 +1648,7 @@ function generateHTML(reportData, trendData = null) {
1197
1648
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1198
1649
  <link rel="icon" type="image/png" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1199
1650
  <link rel="apple-touch-icon" href="https://i.postimg.cc/XqVn1NhF/pulse.png">
1200
- <script src="https://code.highcharts.com/highcharts.js"></script>
1651
+ <script src="https://code.highcharts.com/highcharts.js" defer></script>
1201
1652
  <title>Playwright Pulse Report</title>
1202
1653
  <style>
1203
1654
  :root {
@@ -1397,6 +1848,7 @@ function generateHTML(reportData, trendData = null) {
1397
1848
  )}</div></div>
1398
1849
  </div>
1399
1850
  <div class="dashboard-bottom-row">
1851
+ <div style="display: grid; gap: 20px">
1400
1852
  ${generatePieChart(
1401
1853
  [
1402
1854
  { label: "Passed", value: runSummary.passed },
@@ -1406,6 +1858,13 @@ function generateHTML(reportData, trendData = null) {
1406
1858
  400,
1407
1859
  390
1408
1860
  )}
1861
+ ${
1862
+ runSummary.environment &&
1863
+ Object.keys(runSummary.environment).length > 0
1864
+ ? generateEnvironmentDashboard(runSummary.environment)
1865
+ : '<div class="no-data">Environment data not available.</div>'
1866
+ }
1867
+ </div>
1409
1868
  ${generateSuitesWidget(suitesData)}
1410
1869
  </div>
1411
1870
  </div>
@@ -39,6 +39,7 @@ function mergeReports(files) {
39
39
  combinedRun.failed += run.failed || 0;
40
40
  combinedRun.skipped += run.skipped || 0;
41
41
  combinedRun.duration += run.duration || 0;
42
+ combinedRun.environment = run.environment;
42
43
 
43
44
  if (json.results) {
44
45
  combinedResults.push(...json.results);