@arghajit/playwright-pulse-report 0.3.3 → 0.3.4
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/README.md +95 -85
- package/dist/reporter/playwright-pulse-reporter.d.ts +1 -0
- package/dist/reporter/playwright-pulse-reporter.js +85 -25
- package/dist/types/index.d.ts +8 -2
- package/dist/utils/compression-utils.d.ts +19 -0
- package/dist/utils/compression-utils.js +112 -0
- package/package.json +10 -3
- package/scripts/generate-email-report.mjs +41 -11
- package/scripts/generate-report.mjs +1060 -509
- package/scripts/generate-static-report.mjs +1139 -495
- package/scripts/merge-pulse-report.js +11 -2
- package/scripts/sendReport.mjs +3 -0
- package/scripts/terminal-logo.mjs +51 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -26
- package/dist/playwright-pulse-reporter.d.ts +0 -26
- package/dist/playwright-pulse-reporter.js +0 -304
- package/dist/reporter/lib/report-types.d.ts +0 -8
- package/dist/reporter/lib/report-types.js +0 -2
- package/dist/reporter/reporter/playwright-pulse-reporter.d.ts +0 -1
- package/dist/reporter/reporter/playwright-pulse-reporter.js +0 -398
- package/dist/reporter/types/index.d.ts +0 -52
- package/dist/reporter/types/index.js +0 -2
|
@@ -6,6 +6,7 @@ import path from "path";
|
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { getOutputDir } from "./config-reader.mjs";
|
|
9
|
+
import { animate } from "./terminal-logo.mjs";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Dynamically imports the 'chalk' library for terminal string styling.
|
|
@@ -338,6 +339,12 @@ function generateTestTrendsChart(trendData) {
|
|
|
338
339
|
color: "var(--warning-color)",
|
|
339
340
|
marker: { symbol: "circle" },
|
|
340
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: "Flaky",
|
|
344
|
+
data: runs.map((r) => r.flaky || 0),
|
|
345
|
+
color: "#00ccd3",
|
|
346
|
+
marker: { symbol: "circle" },
|
|
347
|
+
},
|
|
341
348
|
];
|
|
342
349
|
const runsForTooltip = runs.map((r) => ({
|
|
343
350
|
runId: r.runId,
|
|
@@ -541,6 +548,9 @@ function generateTestHistoryChart(history) {
|
|
|
541
548
|
case "skipped":
|
|
542
549
|
color = "var(--warning-color)";
|
|
543
550
|
break;
|
|
551
|
+
case "flaky":
|
|
552
|
+
color = "var(--neutral-500)";
|
|
553
|
+
break;
|
|
544
554
|
default:
|
|
545
555
|
color = "var(--dark-gray-color)";
|
|
546
556
|
}
|
|
@@ -657,6 +667,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
657
667
|
case "Failed":
|
|
658
668
|
color = "var(--danger-color)";
|
|
659
669
|
break;
|
|
670
|
+
case "Flaky":
|
|
671
|
+
color = "#00ccd3";
|
|
672
|
+
break;
|
|
660
673
|
case "Skipped":
|
|
661
674
|
color = "var(--warning-color)";
|
|
662
675
|
break;
|
|
@@ -764,62 +777,264 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
764
777
|
* @param {number} [dashboardHeight=600] The height of the dashboard.
|
|
765
778
|
* @returns {string} The HTML string for the environment dashboard.
|
|
766
779
|
*/
|
|
767
|
-
function
|
|
768
|
-
|
|
769
|
-
|
|
780
|
+
function generateEnvironmentSection(environmentData) {
|
|
781
|
+
if (!environmentData) {
|
|
782
|
+
return '<div class="no-data">Environment data not available.</div>';
|
|
783
|
+
}
|
|
770
784
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
785
|
+
if (Array.isArray(environmentData)) {
|
|
786
|
+
return `
|
|
787
|
+
<div class="sharded-env-section">
|
|
788
|
+
<div class="sharded-env-header">
|
|
789
|
+
<div class="sharded-env-title-row">
|
|
790
|
+
<div>
|
|
791
|
+
<div class="sharded-env-title">
|
|
792
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
793
|
+
<rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
|
|
794
|
+
<rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
|
|
795
|
+
<line x1="6" x2="6.01" y1="6" y2="6"></line>
|
|
796
|
+
<line x1="6" x2="6.01" y1="18" y2="18"></line>
|
|
797
|
+
</svg>
|
|
798
|
+
System Information
|
|
799
|
+
</div>
|
|
800
|
+
<div class="sharded-env-subtitle">Test execution environment details - ${environmentData.length} shard${environmentData.length > 1 ? "s" : ""}</div>
|
|
801
|
+
</div>
|
|
802
|
+
<div class="env-icon-badge">
|
|
803
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
804
|
+
<path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
|
|
805
|
+
</svg>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="sharded-environments-container">
|
|
810
|
+
<div class="sharded-environments-wrapper">
|
|
811
|
+
${environmentData
|
|
812
|
+
.map(
|
|
813
|
+
(env, index) => `
|
|
814
|
+
<div class="env-card-wrapper">
|
|
815
|
+
<div class="env-card-badge">Shard ${index + 1}</div>
|
|
816
|
+
${generateEnvironmentDashboard(env, true)}
|
|
817
|
+
</div>
|
|
818
|
+
`,
|
|
819
|
+
)
|
|
820
|
+
.join("")}
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
<style>
|
|
825
|
+
.sharded-env-section {
|
|
826
|
+
border: 1px solid var(--border-light);
|
|
827
|
+
border-radius: 12px;
|
|
828
|
+
background: var(--bg-secondary);
|
|
829
|
+
overflow: hidden;
|
|
830
|
+
}
|
|
831
|
+
.sharded-env-header {
|
|
832
|
+
position: sticky;
|
|
833
|
+
top: 0;
|
|
834
|
+
z-index: 20;
|
|
835
|
+
background: linear-gradient(to bottom right, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
836
|
+
border-bottom: 1px solid var(--border-light);
|
|
837
|
+
padding: 24px 24px 16px;
|
|
838
|
+
}
|
|
839
|
+
.sharded-env-title-row {
|
|
840
|
+
display: flex;
|
|
841
|
+
justify-content: space-between;
|
|
842
|
+
align-items: center;
|
|
843
|
+
}
|
|
844
|
+
.sharded-env-title {
|
|
845
|
+
display: flex;
|
|
846
|
+
align-items: center;
|
|
847
|
+
font-size: 18px;
|
|
848
|
+
font-weight: 600;
|
|
849
|
+
color: var(--text-primary);
|
|
850
|
+
}
|
|
851
|
+
.sharded-env-title svg {
|
|
852
|
+
width: 18px;
|
|
853
|
+
height: 18px;
|
|
854
|
+
margin-right: 8px;
|
|
855
|
+
stroke: currentColor;
|
|
856
|
+
fill: none;
|
|
857
|
+
}
|
|
858
|
+
.sharded-env-subtitle {
|
|
859
|
+
font-size: 13px;
|
|
860
|
+
color: var(--text-secondary);
|
|
861
|
+
margin-top: 4px;
|
|
862
|
+
}
|
|
863
|
+
.sharded-environments-container {
|
|
864
|
+
max-height: 520px;
|
|
865
|
+
overflow-y: auto;
|
|
866
|
+
overflow-x: hidden;
|
|
867
|
+
padding: 16px;
|
|
868
|
+
}
|
|
869
|
+
.sharded-environments-container::-webkit-scrollbar {
|
|
870
|
+
width: 8px;
|
|
871
|
+
}
|
|
872
|
+
.sharded-environments-container::-webkit-scrollbar-track {
|
|
873
|
+
background: var(--bg-tertiary);
|
|
874
|
+
border-radius: 4px;
|
|
875
|
+
}
|
|
876
|
+
.sharded-environments-container::-webkit-scrollbar-thumb {
|
|
877
|
+
background: var(--border-medium);
|
|
878
|
+
border-radius: 4px;
|
|
879
|
+
}
|
|
880
|
+
.sharded-environments-container::-webkit-scrollbar-thumb:hover {
|
|
881
|
+
background: var(--border-color);
|
|
882
|
+
}
|
|
883
|
+
.sharded-environments-wrapper {
|
|
884
|
+
display: grid;
|
|
885
|
+
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
|
|
886
|
+
gap: 24px;
|
|
887
|
+
}
|
|
888
|
+
@media (max-width: 768px) {
|
|
889
|
+
.sharded-environments-wrapper {
|
|
890
|
+
grid-template-columns: 1fr;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
.env-card-wrapper {
|
|
894
|
+
position: relative;
|
|
895
|
+
}
|
|
896
|
+
.env-card-badge {
|
|
897
|
+
position: absolute;
|
|
898
|
+
top: -10px;
|
|
899
|
+
right: 16px;
|
|
900
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
901
|
+
color: white;
|
|
902
|
+
padding: 6px 14px;
|
|
903
|
+
border-radius: 20px;
|
|
904
|
+
font-size: 0.75em;
|
|
905
|
+
font-weight: 700;
|
|
906
|
+
text-transform: uppercase;
|
|
907
|
+
letter-spacing: 0.5px;
|
|
908
|
+
z-index: 10;
|
|
909
|
+
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.3);
|
|
910
|
+
}
|
|
911
|
+
</style>
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
775
914
|
|
|
776
|
-
|
|
777
|
-
|
|
915
|
+
return generateEnvironmentDashboard(environmentData);
|
|
916
|
+
}
|
|
778
917
|
|
|
779
|
-
|
|
780
|
-
const
|
|
918
|
+
function generateEnvironmentDashboard(environment, hideHeader = false) {
|
|
919
|
+
const cpuModel = environment.cpu && environment.cpu.model ? environment.cpu.model : "N/A";
|
|
920
|
+
const cpuCores = environment.cpu && environment.cpu.cores ? environment.cpu.cores : "N/A";
|
|
921
|
+
const cpuInfo = `model: ${cpuModel}, cores: ${cpuCores}`;
|
|
922
|
+
const cwdInfo = environment.cwd || "N/A";
|
|
781
923
|
|
|
782
924
|
return `
|
|
783
|
-
<div class="
|
|
925
|
+
<div class="env-modern-card${hideHeader ? " no-header" : ""}">
|
|
784
926
|
<style>
|
|
785
|
-
.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
--primary-color: var(--primary-dark, #6366f1);
|
|
793
|
-
--success-color: var(--success-dark, #10b981);
|
|
794
|
-
--warning-color: var(--warning-dark, #f59e0b);
|
|
795
|
-
|
|
796
|
-
background-color: var(--bg-tertiary);
|
|
797
|
-
padding: 48px;
|
|
798
|
-
border-bottom: 1px solid var(--border-light);
|
|
927
|
+
.env-modern-card {
|
|
928
|
+
background: linear-gradient(to bottom right, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
929
|
+
border: 0;
|
|
930
|
+
border-radius: 12px;
|
|
931
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
932
|
+
margin-top: 24px;
|
|
933
|
+
transition: all 0.3s ease;
|
|
799
934
|
font-family: var(--font-family);
|
|
935
|
+
overflow: hidden;
|
|
936
|
+
}
|
|
937
|
+
.env-modern-card:hover {
|
|
938
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
939
|
+
}
|
|
940
|
+
.env-modern-card {
|
|
941
|
+
margin-bottom: 0;
|
|
942
|
+
}
|
|
943
|
+
.env-card-header {
|
|
944
|
+
display: flex;
|
|
945
|
+
flex-direction: column;
|
|
946
|
+
padding: 24px 24px 12px;
|
|
947
|
+
}
|
|
948
|
+
.env-modern-card.no-header .env-card-header {
|
|
949
|
+
display: none;
|
|
950
|
+
}
|
|
951
|
+
.env-modern-card.no-header {
|
|
952
|
+
margin-top: 0;
|
|
953
|
+
}
|
|
954
|
+
.env-modern-card.no-header .env-card-content {
|
|
955
|
+
padding-top: 24px;
|
|
956
|
+
}
|
|
957
|
+
.env-card-title-row {
|
|
958
|
+
display: flex;
|
|
959
|
+
justify-content: space-between;
|
|
960
|
+
align-items: center;
|
|
961
|
+
}
|
|
962
|
+
.env-card-title {
|
|
963
|
+
display: flex;
|
|
964
|
+
align-items: center;
|
|
965
|
+
font-size: 16px;
|
|
966
|
+
font-weight: 600;
|
|
800
967
|
color: var(--text-primary);
|
|
968
|
+
transition: color 0.3s;
|
|
969
|
+
}
|
|
970
|
+
.env-modern-card:hover .env-card-title {
|
|
971
|
+
color: var(--primary-dark, #6366f1);
|
|
972
|
+
}
|
|
973
|
+
.env-card-title svg {
|
|
974
|
+
width: 16px;
|
|
975
|
+
height: 16px;
|
|
976
|
+
margin-right: 8px;
|
|
977
|
+
stroke: currentColor;
|
|
978
|
+
fill: none;
|
|
979
|
+
}
|
|
980
|
+
.env-card-subtitle {
|
|
981
|
+
font-size: 12px;
|
|
982
|
+
color: var(--text-secondary);
|
|
983
|
+
margin-top: 4px;
|
|
984
|
+
}
|
|
985
|
+
.env-card-content {
|
|
986
|
+
padding: 0 24px 24px;
|
|
987
|
+
}
|
|
988
|
+
.env-items-grid {
|
|
801
989
|
display: grid;
|
|
802
990
|
grid-template-columns: repeat(2, 1fr);
|
|
803
|
-
gap:
|
|
804
|
-
font-size: 15px;
|
|
805
|
-
transform: translateZ(0);
|
|
991
|
+
gap: 10px;
|
|
806
992
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
padding: 32px 24px;
|
|
812
|
-
}
|
|
993
|
+
@media (min-width: 768px) {
|
|
994
|
+
.env-items-grid {
|
|
995
|
+
grid-template-columns: repeat(4, 1fr);
|
|
996
|
+
}
|
|
813
997
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
998
|
+
.env-item {
|
|
999
|
+
display: flex;
|
|
1000
|
+
align-items: flex-start;
|
|
1001
|
+
gap: 8px;
|
|
1002
|
+
padding: 8px;
|
|
1003
|
+
border-radius: 8px;
|
|
1004
|
+
transition: background-color 0.2s;
|
|
1005
|
+
min-height: 48px;
|
|
818
1006
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1007
|
+
.env-item:hover {
|
|
1008
|
+
background-color: var(--bg-hover);
|
|
1009
|
+
}
|
|
1010
|
+
.env-item-icon {
|
|
1011
|
+
flex-shrink: 0;
|
|
1012
|
+
}
|
|
1013
|
+
.env-item-icon svg {
|
|
1014
|
+
width: 16px;
|
|
1015
|
+
height: 16px;
|
|
1016
|
+
stroke: var(--primary-dark, #6366f1);
|
|
1017
|
+
fill: none;
|
|
1018
|
+
}
|
|
1019
|
+
.env-item-content {
|
|
1020
|
+
flex-grow: 1;
|
|
1021
|
+
min-width: 0;
|
|
1022
|
+
}
|
|
1023
|
+
.env-item-label {
|
|
1024
|
+
font-size: 12px;
|
|
1025
|
+
font-weight: 500;
|
|
1026
|
+
color: var(--text-secondary);
|
|
1027
|
+
white-space: nowrap;
|
|
1028
|
+
overflow: hidden;
|
|
1029
|
+
text-overflow: ellipsis;
|
|
1030
|
+
}
|
|
1031
|
+
.env-item-value {
|
|
1032
|
+
font-size: 12px;
|
|
1033
|
+
font-weight: 600;
|
|
1034
|
+
color: var(--text-primary);
|
|
1035
|
+
word-wrap: break-word;
|
|
1036
|
+
overflow-wrap: break-word;
|
|
1037
|
+
line-height: 1.4;
|
|
823
1038
|
}
|
|
824
1039
|
|
|
825
1040
|
.env-dashboard-title {
|
|
@@ -951,133 +1166,151 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
951
1166
|
}
|
|
952
1167
|
</style>
|
|
953
1168
|
|
|
954
|
-
<div class="env-
|
|
955
|
-
<div>
|
|
956
|
-
<
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
<div class="env-card-content">
|
|
968
|
-
<div class="env-detail-row">
|
|
969
|
-
<span class="env-detail-label">CPU Model</span>
|
|
970
|
-
<span class="env-detail-value">${environment.cpu.model}</span>
|
|
971
|
-
</div>
|
|
972
|
-
<div class="env-detail-row">
|
|
973
|
-
<span class="env-detail-label">CPU Cores</span>
|
|
974
|
-
<span class="env-detail-value">
|
|
975
|
-
<div class="env-cpu-cores">
|
|
976
|
-
<span>${environment.cpu.cores || "N/A"} core${environment.cpu.cores !== 1 ? "s" : ""}</span>
|
|
977
|
-
</div>
|
|
978
|
-
</span>
|
|
979
|
-
</div>
|
|
980
|
-
<div class="env-detail-row">
|
|
981
|
-
<span class="env-detail-label">Memory</span>
|
|
982
|
-
<span class="env-detail-value">${formattedMemory}</span>
|
|
1169
|
+
<div class="env-card-header">
|
|
1170
|
+
<div class="env-card-title-row">
|
|
1171
|
+
<div>
|
|
1172
|
+
<div class="env-card-title">
|
|
1173
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1174
|
+
<rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
|
|
1175
|
+
<rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
|
|
1176
|
+
<line x1="6" x2="6.01" y1="6" y2="6"></line>
|
|
1177
|
+
<line x1="6" x2="6.01" y1="18" y2="18"></line>
|
|
1178
|
+
</svg>
|
|
1179
|
+
System Information
|
|
1180
|
+
</div>
|
|
1181
|
+
<div class="env-card-subtitle">Test execution environment details</div>
|
|
983
1182
|
</div>
|
|
984
1183
|
</div>
|
|
985
1184
|
</div>
|
|
986
1185
|
|
|
987
|
-
<div class="env-card">
|
|
988
|
-
<div class="env-
|
|
989
|
-
<
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
<
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}</span>
|
|
1000
|
-
</div>
|
|
1001
|
-
<div class="env-detail-row">
|
|
1002
|
-
<span class="env-detail-label">OS Version</span>
|
|
1003
|
-
<span class="env-detail-value">${
|
|
1004
|
-
environment.os.split(" ")[1] || "N/A"
|
|
1005
|
-
}</span>
|
|
1006
|
-
</div>
|
|
1007
|
-
<div class="env-detail-row">
|
|
1008
|
-
<span class="env-detail-label">Hostname</span>
|
|
1009
|
-
<span class="env-detail-value" title="${environment.host}">${
|
|
1010
|
-
environment.host
|
|
1011
|
-
}</span>
|
|
1186
|
+
<div class="env-card-content">
|
|
1187
|
+
<div class="env-items-grid">
|
|
1188
|
+
<div class="env-item">
|
|
1189
|
+
<div class="env-item-icon">
|
|
1190
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1191
|
+
<path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
|
|
1192
|
+
</svg>
|
|
1193
|
+
</div>
|
|
1194
|
+
<div class="env-item-content">
|
|
1195
|
+
<p class="env-item-label">Host</p>
|
|
1196
|
+
<div class="env-item-value" title="${environment.host}">${environment.host}</div>
|
|
1197
|
+
</div>
|
|
1012
1198
|
</div>
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
<span class="env-detail-value">${environment.node}</span>
|
|
1199
|
+
|
|
1200
|
+
<div class="env-item">
|
|
1201
|
+
<div class="env-item-icon">
|
|
1202
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1203
|
+
<path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"></path>
|
|
1204
|
+
</svg>
|
|
1205
|
+
</div>
|
|
1206
|
+
<div class="env-item-content">
|
|
1207
|
+
<p class="env-item-label">Os</p>
|
|
1208
|
+
<div class="env-item-value" title="${environment.os}">${environment.os}</div>
|
|
1209
|
+
</div>
|
|
1025
1210
|
</div>
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
<
|
|
1211
|
+
|
|
1212
|
+
<div class="env-item">
|
|
1213
|
+
<div class="env-item-icon">
|
|
1214
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1215
|
+
<rect width="16" height="16" x="4" y="4" rx="2"></rect>
|
|
1216
|
+
<rect width="6" height="6" x="9" y="9" rx="1"></rect>
|
|
1217
|
+
<path d="M15 2v2"></path>
|
|
1218
|
+
<path d="M15 20v2"></path>
|
|
1219
|
+
<path d="M2 15h2"></path>
|
|
1220
|
+
<path d="M2 9h2"></path>
|
|
1221
|
+
<path d="M20 15h2"></path>
|
|
1222
|
+
<path d="M20 9h2"></path>
|
|
1223
|
+
<path d="M9 2v2"></path>
|
|
1224
|
+
<path d="M9 20v2"></path>
|
|
1225
|
+
</svg>
|
|
1226
|
+
</div>
|
|
1227
|
+
<div class="env-item-content">
|
|
1228
|
+
<p class="env-item-label">Cpu</p>
|
|
1229
|
+
<div class="env-item-value" title='${JSON.stringify(environment.cpu)}'>${cpuInfo}</div>
|
|
1230
|
+
</div>
|
|
1029
1231
|
</div>
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
<
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1232
|
+
|
|
1233
|
+
<div class="env-item">
|
|
1234
|
+
<div class="env-item-icon">
|
|
1235
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1236
|
+
<path d="M6 19v-3"></path>
|
|
1237
|
+
<path d="M10 19v-3"></path>
|
|
1238
|
+
<path d="M14 19v-3"></path>
|
|
1239
|
+
<path d="M18 19v-3"></path>
|
|
1240
|
+
<path d="M8 11V9"></path>
|
|
1241
|
+
<path d="M16 11V9"></path>
|
|
1242
|
+
<path d="M12 11V9"></path>
|
|
1243
|
+
<path d="M2 15h20"></path>
|
|
1244
|
+
<path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z"></path>
|
|
1245
|
+
</svg>
|
|
1246
|
+
</div>
|
|
1247
|
+
<div class="env-item-content">
|
|
1248
|
+
<p class="env-item-label">Memory</p>
|
|
1249
|
+
<div class="env-item-value" title="${environment.memory}">${environment.memory}</div>
|
|
1250
|
+
</div>
|
|
1037
1251
|
</div>
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
? "ARM-based"
|
|
1063
|
-
: "x86/Other"
|
|
1064
|
-
}
|
|
1065
|
-
</span>
|
|
1066
|
-
</span>
|
|
1252
|
+
|
|
1253
|
+
<div class="env-item">
|
|
1254
|
+
<div class="env-item-icon">
|
|
1255
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1256
|
+
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
|
|
1257
|
+
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
|
|
1258
|
+
<path d="M12 2v2"></path>
|
|
1259
|
+
<path d="M12 22v-2"></path>
|
|
1260
|
+
<path d="m17 20.66-1-1.73"></path>
|
|
1261
|
+
<path d="M11 10.27 7 3.34"></path>
|
|
1262
|
+
<path d="m20.66 17-1.73-1"></path>
|
|
1263
|
+
<path d="m3.34 7 1.73 1"></path>
|
|
1264
|
+
<path d="M14 12h8"></path>
|
|
1265
|
+
<path d="M2 12h2"></path>
|
|
1266
|
+
<path d="m20.66 7-1.73 1"></path>
|
|
1267
|
+
<path d="m3.34 17 1.73-1"></path>
|
|
1268
|
+
<path d="m17 3.34-1 1.73"></path>
|
|
1269
|
+
<path d="m11 13.73-4 6.93"></path>
|
|
1270
|
+
</svg>
|
|
1271
|
+
</div>
|
|
1272
|
+
<div class="env-item-content">
|
|
1273
|
+
<p class="env-item-label">Node</p>
|
|
1274
|
+
<div class="env-item-value" title="${environment.node}">${environment.node}</div>
|
|
1275
|
+
</div>
|
|
1067
1276
|
</div>
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
<
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1277
|
+
|
|
1278
|
+
<div class="env-item">
|
|
1279
|
+
<div class="env-item-icon">
|
|
1280
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1281
|
+
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"></path>
|
|
1282
|
+
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path>
|
|
1283
|
+
<path d="M12 2v2"></path>
|
|
1284
|
+
<path d="M12 22v-2"></path>
|
|
1285
|
+
<path d="m17 20.66-1-1.73"></path>
|
|
1286
|
+
<path d="M11 10.27 7 3.34"></path>
|
|
1287
|
+
<path d="m20.66 17-1.73-1"></path>
|
|
1288
|
+
<path d="m3.34 7 1.73 1"></path>
|
|
1289
|
+
<path d="M14 12h8"></path>
|
|
1290
|
+
<path d="M2 12h2"></path>
|
|
1291
|
+
<path d="m20.66 7-1.73 1"></path>
|
|
1292
|
+
<path d="m3.34 17 1.73-1"></path>
|
|
1293
|
+
<path d="m17 3.34-1 1.73"></path>
|
|
1294
|
+
<path d="m11 13.73-4 6.93"></path>
|
|
1295
|
+
</svg>
|
|
1296
|
+
</div>
|
|
1297
|
+
<div class="env-item-content">
|
|
1298
|
+
<p class="env-item-label">V8</p>
|
|
1299
|
+
<div class="env-item-value" title="${environment.v8}">${environment.v8}</div>
|
|
1300
|
+
</div>
|
|
1077
1301
|
</div>
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
<
|
|
1302
|
+
|
|
1303
|
+
<div class="env-item">
|
|
1304
|
+
<div class="env-item-icon">
|
|
1305
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1306
|
+
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
1307
|
+
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
1308
|
+
</svg>
|
|
1309
|
+
</div>
|
|
1310
|
+
<div class="env-item-content">
|
|
1311
|
+
<p class="env-item-label">Working Dir</p>
|
|
1312
|
+
<div class="env-item-value" title="${cwdInfo}">${cwdInfo.length > 30 ? "..." + cwdInfo.slice(-27) : cwdInfo}</div>
|
|
1313
|
+
</div>
|
|
1081
1314
|
</div>
|
|
1082
1315
|
</div>
|
|
1083
1316
|
</div>
|
|
@@ -1105,11 +1338,11 @@ function generateWorkerDistributionChart(results) {
|
|
|
1105
1338
|
const workerId =
|
|
1106
1339
|
typeof test.workerId !== "undefined" ? test.workerId : "N/A";
|
|
1107
1340
|
if (!acc[workerId]) {
|
|
1108
|
-
acc[workerId] = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
1341
|
+
acc[workerId] = { passed: 0, failed: 0, skipped: 0, flaky: 0, tests: [] };
|
|
1109
1342
|
}
|
|
1110
1343
|
|
|
1111
1344
|
const status = String(test.status).toLowerCase();
|
|
1112
|
-
if (status === "passed" || status === "failed" || status === "skipped") {
|
|
1345
|
+
if (status === "passed" || status === "failed" || status === "skipped" || status === "flaky") {
|
|
1113
1346
|
acc[workerId][status]++;
|
|
1114
1347
|
}
|
|
1115
1348
|
|
|
@@ -1154,6 +1387,7 @@ function generateWorkerDistributionChart(results) {
|
|
|
1154
1387
|
const passedData = workerIds.map((id) => workerData[id].passed);
|
|
1155
1388
|
const failedData = workerIds.map((id) => workerData[id].failed);
|
|
1156
1389
|
const skippedData = workerIds.map((id) => workerData[id].skipped);
|
|
1390
|
+
const flakyData = workerIds.map((id) => workerData[id].flaky);
|
|
1157
1391
|
|
|
1158
1392
|
const categoriesString = JSON.stringify(categories);
|
|
1159
1393
|
const fullDataString = JSON.stringify(fullWorkerData);
|
|
@@ -1161,6 +1395,7 @@ function generateWorkerDistributionChart(results) {
|
|
|
1161
1395
|
{ name: "Passed", data: passedData, color: "var(--success-color)" },
|
|
1162
1396
|
{ name: "Failed", data: failedData, color: "var(--danger-color)" },
|
|
1163
1397
|
{ name: "Skipped", data: skippedData, color: "var(--warning-color)" },
|
|
1398
|
+
{ name: "Flaky", data: flakyData, color: "#00ccd3" },
|
|
1164
1399
|
]);
|
|
1165
1400
|
|
|
1166
1401
|
// The HTML now includes the chart container, the modal, and styles for the modal
|
|
@@ -1427,6 +1662,7 @@ function generateTestHistoryContent(trendData) {
|
|
|
1427
1662
|
<option value="">All Statuses</option>
|
|
1428
1663
|
<option value="passed">Passed</option>
|
|
1429
1664
|
<option value="failed">Failed</option>
|
|
1665
|
+
<option value="flaky">Flaky</option>
|
|
1430
1666
|
<option value="skipped">Skipped</option>
|
|
1431
1667
|
</select>
|
|
1432
1668
|
<button id="clear-history-filters" class="clear-filters-btn">Clear Filters</button>
|
|
@@ -1499,6 +1735,8 @@ function getStatusClass(status) {
|
|
|
1499
1735
|
return "status-failed";
|
|
1500
1736
|
case "skipped":
|
|
1501
1737
|
return "status-skipped";
|
|
1738
|
+
case "flaky":
|
|
1739
|
+
return "status-flaky";
|
|
1502
1740
|
default:
|
|
1503
1741
|
return "status-unknown";
|
|
1504
1742
|
}
|
|
@@ -1516,6 +1754,8 @@ function getStatusIcon(status) {
|
|
|
1516
1754
|
return "❌";
|
|
1517
1755
|
case "skipped":
|
|
1518
1756
|
return "⏭️";
|
|
1757
|
+
case "flaky":
|
|
1758
|
+
return "⚠️";
|
|
1519
1759
|
default:
|
|
1520
1760
|
return "❓";
|
|
1521
1761
|
}
|
|
@@ -1556,6 +1796,7 @@ function getSuitesData(results) {
|
|
|
1556
1796
|
browser: browser,
|
|
1557
1797
|
passed: 0,
|
|
1558
1798
|
failed: 0,
|
|
1799
|
+
flaky: 0,
|
|
1559
1800
|
skipped: 0,
|
|
1560
1801
|
count: 0,
|
|
1561
1802
|
statusOverall: "passed",
|
|
@@ -1563,12 +1804,15 @@ function getSuitesData(results) {
|
|
|
1563
1804
|
}
|
|
1564
1805
|
const suite = suitesMap.get(key);
|
|
1565
1806
|
suite.count++;
|
|
1566
|
-
|
|
1807
|
+
let currentStatus = String(test.status).toLowerCase();
|
|
1808
|
+
if (test.outcome === 'flaky') currentStatus = 'flaky';
|
|
1567
1809
|
if (currentStatus && suite[currentStatus] !== undefined) {
|
|
1568
1810
|
suite[currentStatus]++;
|
|
1569
1811
|
}
|
|
1570
1812
|
if (currentStatus === "failed") suite.statusOverall = "failed";
|
|
1571
|
-
else if (currentStatus === "
|
|
1813
|
+
else if (currentStatus === "flaky" && suite.statusOverall !== "failed")
|
|
1814
|
+
suite.statusOverall = "flaky";
|
|
1815
|
+
else if (currentStatus === "skipped" && suite.statusOverall !== "failed" && suite.statusOverall !== "flaky")
|
|
1572
1816
|
suite.statusOverall = "skipped";
|
|
1573
1817
|
});
|
|
1574
1818
|
return Array.from(suitesMap.values());
|
|
@@ -1614,8 +1858,8 @@ function generateSuitesWidget(suitesData) {
|
|
|
1614
1858
|
|
|
1615
1859
|
// Added inline styles for height consistency with Pie Chart (approx 450px) and scrolling
|
|
1616
1860
|
return `
|
|
1617
|
-
<div class="suites-widget
|
|
1618
|
-
<div class="suites-header"
|
|
1861
|
+
<div class="suites-widget fixed-height-widget">
|
|
1862
|
+
<div class="suites-header">
|
|
1619
1863
|
<h2>Test Suites</h2>
|
|
1620
1864
|
<span class="summary-badge">${
|
|
1621
1865
|
suitesData.length
|
|
@@ -1625,40 +1869,40 @@ function generateSuitesWidget(suitesData) {
|
|
|
1625
1869
|
)} tests</span>
|
|
1626
1870
|
</div>
|
|
1627
1871
|
|
|
1628
|
-
<div class="suites-grid-container"
|
|
1872
|
+
<div class="suites-grid-container">
|
|
1629
1873
|
<div class="suites-grid">
|
|
1630
1874
|
${suitesData
|
|
1631
1875
|
.map(
|
|
1632
1876
|
(suite) => `
|
|
1633
1877
|
<div class="suite-card status-${suite.statusOverall}">
|
|
1634
1878
|
<div class="suite-card-header">
|
|
1635
|
-
<h3 class="suite-name" title="${sanitizeHTML(
|
|
1636
|
-
|
|
1637
|
-
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1879
|
+
<h3 class="suite-name" title="${sanitizeHTML(suite.name)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1880
|
+
<div class="status-indicator-dot status-${suite.statusOverall}" title="${suite.statusOverall.charAt(0).toUpperCase() + suite.statusOverall.slice(1)}"></div>
|
|
1638
1881
|
</div>
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1882
|
+
|
|
1883
|
+
<div class="browser-tag" title="🌐Browser: ${sanitizeHTML(suite.browser)}">
|
|
1884
|
+
<span style="font-size: 1.1em;">🌐</span> ${sanitizeHTML(suite.browser)}
|
|
1885
|
+
</div>
|
|
1886
|
+
|
|
1642
1887
|
<div class="suite-card-body">
|
|
1643
|
-
<span class="test-count">${suite.count}
|
|
1644
|
-
suite.count !== 1 ? "s" : ""
|
|
1645
|
-
}</span>
|
|
1888
|
+
<span class="test-count-label">${suite.count} Test${suite.count !== 1 ? "s" : ""}</span>
|
|
1646
1889
|
<div class="suite-stats">
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1890
|
+
<span class="stat-pill passed" title="Passed">
|
|
1891
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg>
|
|
1892
|
+
${suite.passed}
|
|
1893
|
+
</span>
|
|
1894
|
+
<span class="stat-pill failed" title="Failed">
|
|
1895
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg>
|
|
1896
|
+
${suite.failed}
|
|
1897
|
+
</span>
|
|
1898
|
+
<span class="stat-pill flaky" title="Flaky">
|
|
1899
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg>
|
|
1900
|
+
${suite.flaky || 0}
|
|
1901
|
+
</span>
|
|
1902
|
+
<span class="stat-pill skipped" title="Skipped">
|
|
1903
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg>
|
|
1904
|
+
${suite.skipped}
|
|
1905
|
+
</span>
|
|
1662
1906
|
</div>
|
|
1663
1907
|
</div>
|
|
1664
1908
|
</div>`,
|
|
@@ -2037,6 +2281,7 @@ function generateSeverityDistributionChart(results) {
|
|
|
2037
2281
|
const data = {
|
|
2038
2282
|
passed: [0, 0, 0, 0, 0],
|
|
2039
2283
|
failed: [0, 0, 0, 0, 0],
|
|
2284
|
+
flaky: [0, 0, 0, 0, 0],
|
|
2040
2285
|
skipped: [0, 0, 0, 0, 0],
|
|
2041
2286
|
};
|
|
2042
2287
|
|
|
@@ -2055,6 +2300,8 @@ function generateSeverityDistributionChart(results) {
|
|
|
2055
2300
|
status === "interrupted"
|
|
2056
2301
|
) {
|
|
2057
2302
|
data.failed[index]++;
|
|
2303
|
+
} else if (status === "flaky") {
|
|
2304
|
+
data.flaky[index]++;
|
|
2058
2305
|
} else {
|
|
2059
2306
|
data.skipped[index]++;
|
|
2060
2307
|
}
|
|
@@ -2068,6 +2315,7 @@ function generateSeverityDistributionChart(results) {
|
|
|
2068
2315
|
const seriesData = [
|
|
2069
2316
|
{ name: "Passed", data: data.passed, color: "var(--success-color)" },
|
|
2070
2317
|
{ name: "Failed", data: data.failed, color: "var(--danger-color)" },
|
|
2318
|
+
{ name: "Flaky", data: data.flaky, color: "#00ccd3" },
|
|
2071
2319
|
{ name: "Skipped", data: data.skipped, color: "var(--warning-color)" },
|
|
2072
2320
|
];
|
|
2073
2321
|
|
|
@@ -2211,32 +2459,85 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2211
2459
|
duration: 0,
|
|
2212
2460
|
timestamp: new Date().toISOString(),
|
|
2213
2461
|
};
|
|
2214
|
-
|
|
2215
|
-
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
2216
|
-
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
2217
|
-
const skipPercentage = Math.round(
|
|
2218
|
-
((runSummary.skipped || 0) / totalTestsOr1) * 100,
|
|
2219
|
-
);
|
|
2462
|
+
|
|
2220
2463
|
const avgTestDuration =
|
|
2221
2464
|
runSummary.totalTests > 0
|
|
2222
2465
|
? formatDuration(runSummary.duration / runSummary.totalTests)
|
|
2223
2466
|
: "0.0s";
|
|
2224
2467
|
|
|
2468
|
+
const flakyCount = (results || []).filter(r => r.outcome === 'flaky').length;
|
|
2469
|
+
|
|
2225
2470
|
// Calculate retry statistics
|
|
2471
|
+
let retriedTestsCount = 0;
|
|
2226
2472
|
const totalRetried = (results || []).reduce((acc, test) => {
|
|
2227
|
-
if (test.
|
|
2228
|
-
|
|
2473
|
+
if (test.retryHistory && test.retryHistory.length > 0) {
|
|
2474
|
+
// Filter out any "passed" or "skipped" entries in the history
|
|
2475
|
+
// We only count attempts that actually failed or timed out, triggering a retry.
|
|
2476
|
+
const unsuccessfulRetries = test.retryHistory.filter(attempt =>
|
|
2477
|
+
attempt.status === 'failed' || attempt.status === 'timedout' || attempt.status === 'flaky'
|
|
2478
|
+
);
|
|
2479
|
+
if (unsuccessfulRetries.length > 0) {
|
|
2480
|
+
retriedTestsCount++;
|
|
2481
|
+
}
|
|
2482
|
+
return acc + unsuccessfulRetries.length;
|
|
2229
2483
|
}
|
|
2230
2484
|
return acc;
|
|
2231
2485
|
}, 0);
|
|
2232
2486
|
|
|
2487
|
+
// --- RECALCULATE KPI METRICS BASED ON FINAL_STATUS ---
|
|
2488
|
+
let calculatedPassed = 0;
|
|
2489
|
+
let calculatedFailed = 0;
|
|
2490
|
+
let calculatedSkipped = 0;
|
|
2491
|
+
let calculatedFlaky = 0;
|
|
2492
|
+
let calculatedTotal = 0;
|
|
2493
|
+
|
|
2494
|
+
(results || []).forEach(test => {
|
|
2495
|
+
calculatedTotal++;
|
|
2496
|
+
// New Logic: If outcome is 'flaky', it overrides everything.
|
|
2497
|
+
let statusToUse = test.status;
|
|
2498
|
+
if (test.outcome === 'flaky') {
|
|
2499
|
+
statusToUse = 'flaky';
|
|
2500
|
+
} else if (test.status === 'flaky') {
|
|
2501
|
+
// Just in case outcome wasn't set but status was (unlikely with new reporter)
|
|
2502
|
+
statusToUse = 'flaky';
|
|
2503
|
+
} else if (test.retryHistory && test.retryHistory.length > 0 && test.final_status) {
|
|
2504
|
+
statusToUse = test.final_status;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// Update test status in place for charts
|
|
2508
|
+
test.status = statusToUse;
|
|
2509
|
+
|
|
2510
|
+
const s = String(statusToUse).toLowerCase();
|
|
2511
|
+
if (s === 'passed') calculatedPassed++;
|
|
2512
|
+
else if (s === 'skipped') calculatedSkipped++;
|
|
2513
|
+
else if (s === 'flaky') calculatedFlaky++;
|
|
2514
|
+
else calculatedFailed++; // failed, timedout, interrupted
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
// Override runSummary counts with our calculated ones if results exist
|
|
2518
|
+
if (results && results.length > 0) {
|
|
2519
|
+
runSummary.passed = calculatedPassed;
|
|
2520
|
+
runSummary.failed = calculatedFailed;
|
|
2521
|
+
runSummary.skipped = calculatedSkipped;
|
|
2522
|
+
runSummary.flaky = calculatedFlaky;
|
|
2523
|
+
runSummary.totalTests = calculatedTotal;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
2527
|
+
const passPercentage = Math.round((runSummary.passed / totalTestsOr1) * 100);
|
|
2528
|
+
const failPercentage = Math.round((runSummary.failed / totalTestsOr1) * 100);
|
|
2529
|
+
const skipPercentage = Math.round(
|
|
2530
|
+
((runSummary.skipped || 0) / totalTestsOr1) * 100,
|
|
2531
|
+
);
|
|
2532
|
+
const flakyPercentage = Math.round(((runSummary.flaky || 0) / totalTestsOr1) * 100);
|
|
2533
|
+
|
|
2534
|
+
|
|
2233
2535
|
// Calculate browser distribution
|
|
2234
2536
|
const browserStats = (results || []).reduce((acc, test) => {
|
|
2235
2537
|
let browserName = "unknown";
|
|
2236
2538
|
if (test.browser) {
|
|
2237
|
-
//
|
|
2238
|
-
|
|
2239
|
-
browserName = match ? match[1] : test.browser;
|
|
2539
|
+
// Use full browser name
|
|
2540
|
+
browserName = test.browser;
|
|
2240
2541
|
}
|
|
2241
2542
|
acc[browserName] = (acc[browserName] || 0) + 1;
|
|
2242
2543
|
return acc;
|
|
@@ -2277,6 +2578,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2277
2578
|
const severity = test.severity || "Medium";
|
|
2278
2579
|
const severityBadge = `<span class="severity-badge" data-severity="${severity.toLowerCase()}">${severity}</span>`;
|
|
2279
2580
|
|
|
2581
|
+
// --- Retry Count Badge (only show if retries occurred) ---
|
|
2582
|
+
const retryCount = (test.retryHistory && test.retryHistory.length > 0) ? test.retryHistory.length : 0;
|
|
2583
|
+
const retryBadge = (test.retryHistory && test.retryHistory.length > 0) ? `<span class="retry-badge">Retry Count: ${retryCount}</span>` : '';
|
|
2584
|
+
|
|
2280
2585
|
// --- Step Generation ---
|
|
2281
2586
|
const generateStepsHTML = (steps, depth = 0) => {
|
|
2282
2587
|
if (!steps || steps.length === 0)
|
|
@@ -2285,17 +2590,20 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2285
2590
|
.map((step) => {
|
|
2286
2591
|
const hasNestedSteps = step.steps && step.steps.length > 0;
|
|
2287
2592
|
const isHook = step.hookType;
|
|
2593
|
+
const isFailedStep = step.isFailedStep === true;
|
|
2288
2594
|
const stepClass = isHook
|
|
2289
2595
|
? `step-hook step-hook-${step.hookType}`
|
|
2290
2596
|
: "";
|
|
2597
|
+
const failedStepClass = isFailedStep ? " failed-step-highlight" : "";
|
|
2291
2598
|
const hookIndicator = isHook ? ` (${step.hookType} hook)` : "";
|
|
2599
|
+
const failedStepIndicator = isFailedStep ? ` <span class="failed-step-marker">⚠️ Failed at this step</span>` : "";
|
|
2292
2600
|
return `
|
|
2293
|
-
<div class="step-item" style="--depth: ${depth};">
|
|
2601
|
+
<div class="step-item${failedStepClass}" style="--depth: ${depth};">
|
|
2294
2602
|
<div class="step-header ${stepClass}" role="button" aria-expanded="false">
|
|
2295
2603
|
<span class="step-icon">${getStatusIcon(step.status)}</span>
|
|
2296
2604
|
<span class="step-title">${sanitizeHTML(
|
|
2297
2605
|
step.title,
|
|
2298
|
-
)}${hookIndicator}</span>
|
|
2606
|
+
)}${hookIndicator}${failedStepIndicator}</span>
|
|
2299
2607
|
<span class="step-duration">${formatDuration(
|
|
2300
2608
|
step.duration,
|
|
2301
2609
|
)}</span>
|
|
@@ -2308,6 +2616,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2308
2616
|
)}</div>`
|
|
2309
2617
|
: ""
|
|
2310
2618
|
}
|
|
2619
|
+
${
|
|
2620
|
+
step.codeSnippet
|
|
2621
|
+
? `<div class="code-snippet-section"><pre class="code-snippet">${sanitizeHTML(
|
|
2622
|
+
step.codeSnippet,
|
|
2623
|
+
)}</pre></div>`
|
|
2624
|
+
: ""
|
|
2625
|
+
}
|
|
2311
2626
|
${
|
|
2312
2627
|
step.errorMessage
|
|
2313
2628
|
? `<div class="test-error-summary">
|
|
@@ -2318,7 +2633,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2318
2633
|
)}</div>`
|
|
2319
2634
|
: ""
|
|
2320
2635
|
}
|
|
2321
|
-
<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
|
|
2636
|
+
<button class="copy-error-btn" onclick="copyErrorToClipboard(this)" style="margin-top: 8px; padding: 6px 12px; background: rgba(248, 113, 113, 0.15); border: 2px solid var(--danger-color); border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--danger-color); font-weight: 600; transition: all 0.2s; align-self: flex-end; width: auto;" onmouseover="this.style.background='rgba(248, 113, 113, 0.25)'" onmouseout="this.style.background='rgba(248, 113, 113, 0.15)'">Copy Error Prompt</button>
|
|
2322
2637
|
</div>`
|
|
2323
2638
|
: ""
|
|
2324
2639
|
}
|
|
@@ -2336,43 +2651,36 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2336
2651
|
.join("");
|
|
2337
2652
|
};
|
|
2338
2653
|
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
test.status,
|
|
2365
|
-
).toUpperCase()}</span>
|
|
2366
|
-
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
2367
|
-
</div>
|
|
2368
|
-
</div>
|
|
2369
|
-
<div class="test-case-content" style="display: none;">
|
|
2370
|
-
<p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
|
|
2654
|
+
// Helper for Tab Badges
|
|
2655
|
+
const getSmallStatusBadge = (status) => {
|
|
2656
|
+
const s = String(status).toLowerCase();
|
|
2657
|
+
let colorVar = 'var(--text-tertiary)';
|
|
2658
|
+
if(s === 'passed') colorVar = 'var(--success-color)';
|
|
2659
|
+
else if(s === 'failed') colorVar = 'var(--danger-color)';
|
|
2660
|
+
else if(s === 'skipped') colorVar = 'var(--warning-color)';
|
|
2661
|
+
else if(s === 'flaky') colorVar = '#00ccd3';
|
|
2662
|
+
|
|
2663
|
+
return `<span style="
|
|
2664
|
+
display: inline-block;
|
|
2665
|
+
width: 8px;
|
|
2666
|
+
height: 8px;
|
|
2667
|
+
border-radius: 50%;
|
|
2668
|
+
background-color: ${colorVar};
|
|
2669
|
+
margin-left: 6px;
|
|
2670
|
+
vertical-align: middle;
|
|
2671
|
+
" title="${s}"></span>`;
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
// Function to generate test content HTML (used for base run and retry tabs)
|
|
2675
|
+
const getTestContentHTML = (testData, runSuffix) => {
|
|
2676
|
+
const logId = `stdout-log-${test.id || testIndex}-${runSuffix}`;
|
|
2677
|
+
return `
|
|
2678
|
+
<p><strong>Full Path:</strong> ${sanitizeHTML(testData.name)}</p>
|
|
2371
2679
|
${
|
|
2372
|
-
|
|
2373
|
-
? `<div class="annotations-section">
|
|
2374
|
-
<h4 style="margin-top: 0; margin-bottom: 10px; font-size: 1.1em;">📌 Annotations</h4>
|
|
2375
|
-
${
|
|
2680
|
+
testData.annotations && testData.annotations.length > 0
|
|
2681
|
+
? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
|
|
2682
|
+
<h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
|
|
2683
|
+
${testData.annotations
|
|
2376
2684
|
.map((annotation, idx) => {
|
|
2377
2685
|
const isIssueOrBug =
|
|
2378
2686
|
annotation.type === "issue" ||
|
|
@@ -2380,176 +2688,312 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2380
2688
|
const descriptionText = annotation.description || "";
|
|
2381
2689
|
const typeLabel = sanitizeHTML(annotation.type);
|
|
2382
2690
|
const descriptionHtml =
|
|
2383
|
-
isIssueOrBug && descriptionText.match(/^[A-Z]
|
|
2691
|
+
isIssueOrBug && descriptionText.match(/^[A-Z]+-\\d+$/)
|
|
2384
2692
|
? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
|
|
2385
2693
|
descriptionText,
|
|
2386
|
-
)}" style="color:
|
|
2694
|
+
)}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
|
|
2387
2695
|
descriptionText,
|
|
2388
2696
|
)}</a>`
|
|
2389
2697
|
: sanitizeHTML(descriptionText);
|
|
2390
2698
|
const locationText = annotation.location
|
|
2391
|
-
? `<div style="font-size: 0.85em; color:
|
|
2699
|
+
? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
|
|
2392
2700
|
annotation.location.file,
|
|
2393
2701
|
)}:${annotation.location.line}:${
|
|
2394
2702
|
annotation.location.column
|
|
2395
2703
|
}</div>`
|
|
2396
2704
|
: "";
|
|
2397
2705
|
return `<div style="margin-bottom: ${
|
|
2398
|
-
idx <
|
|
2399
|
-
};"
|
|
2706
|
+
idx < testData.annotations.length - 1 ? "10px" : "0"
|
|
2707
|
+
};">
|
|
2708
|
+
<strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
|
|
2709
|
+
${
|
|
2400
2710
|
descriptionText
|
|
2401
|
-
? `<br><strong>Description:</strong> ${descriptionHtml}`
|
|
2711
|
+
? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
|
|
2402
2712
|
: ""
|
|
2403
|
-
}
|
|
2713
|
+
}
|
|
2714
|
+
${locationText}
|
|
2715
|
+
</div>`;
|
|
2404
2716
|
})
|
|
2405
2717
|
.join("")}
|
|
2406
2718
|
</div>`
|
|
2407
2719
|
: ""
|
|
2408
2720
|
}
|
|
2409
2721
|
<p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
|
|
2410
|
-
|
|
2722
|
+
testData.workerId,
|
|
2411
2723
|
)} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
|
|
2412
|
-
|
|
2724
|
+
testData.totalWorkers,
|
|
2413
2725
|
)}]</p>
|
|
2414
2726
|
${
|
|
2415
|
-
|
|
2416
|
-
? `<div class="test-error-summary">${formatPlaywrightError(
|
|
2417
|
-
|
|
2418
|
-
)}
|
|
2727
|
+
testData.errorMessage
|
|
2728
|
+
? `<div class="test-error-summary"><div class="stack-trace">${formatPlaywrightError(
|
|
2729
|
+
testData.errorMessage,
|
|
2730
|
+
)}</div>
|
|
2731
|
+
<button
|
|
2732
|
+
class="copy-error-btn"
|
|
2733
|
+
onclick="copyErrorToClipboard(this)"
|
|
2734
|
+
style="
|
|
2735
|
+
margin-top: 8px;
|
|
2736
|
+
padding: 6px 12px;
|
|
2737
|
+
background: rgba(248, 113, 113, 0.15);
|
|
2738
|
+
border: 2px solid var(--danger-color);
|
|
2739
|
+
border-radius: 6px;
|
|
2740
|
+
cursor: pointer;
|
|
2741
|
+
font-size: 12px;
|
|
2742
|
+
color: var(--danger-color);
|
|
2743
|
+
font-weight: 600;
|
|
2744
|
+
transition: 0.2s;
|
|
2745
|
+
align-self: flex-end;
|
|
2746
|
+
width: auto;
|
|
2747
|
+
"
|
|
2748
|
+
onmouseover="this.style.background='#e0e0e0'"
|
|
2749
|
+
onmouseout="this.style.background='#f0f0f0'"
|
|
2750
|
+
>
|
|
2751
|
+
Copy Error Prompt
|
|
2752
|
+
</button>
|
|
2753
|
+
</div>`
|
|
2419
2754
|
: ""
|
|
2420
2755
|
}
|
|
2421
2756
|
${
|
|
2422
|
-
|
|
2757
|
+
testData.snippet
|
|
2423
2758
|
? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
|
|
2424
|
-
|
|
2759
|
+
testData.snippet,
|
|
2425
2760
|
)}</code></pre></div>`
|
|
2426
2761
|
: ""
|
|
2427
2762
|
}
|
|
2428
2763
|
<h4>Steps</h4>
|
|
2429
|
-
<div class="steps-list">${generateStepsHTML(
|
|
2430
|
-
|
|
2431
|
-
${(() => {
|
|
2432
|
-
if (!test.stdout || test.stdout.length === 0) return "";
|
|
2433
|
-
// FIXED: Now using 'testIndex' which is guaranteed to be defined
|
|
2434
|
-
const logId = `stdout-log-${test.id || testIndex}`;
|
|
2435
|
-
return `<div class="console-output-section"><h4>Console Output (stdout) <button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</button></h4><div class="log-wrapper"><pre id="${logId}" class="console-log stdout-log" style="background-color: var(--bg-tertiary); color: #f3e8c3; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
|
|
2436
|
-
test.stdout.map((line) => sanitizeHTML(line)).join("\n"),
|
|
2437
|
-
)}</pre></div></div>`;
|
|
2438
|
-
})()}
|
|
2439
|
-
|
|
2440
|
-
${(() => {
|
|
2441
|
-
if (!test.stderr || test.stderr.length === 0) return "";
|
|
2442
|
-
// FIXED: Using 'testIndex'
|
|
2443
|
-
const logId = `stderr-log-${test.id || testIndex}`;
|
|
2444
|
-
return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: var(--bg-tertiary); color: #f87171; padding: 1.25em; border-radius: 0.85em; line-height: 1.2; border: 1px solid var(--border-light);">${formatPlaywrightError(
|
|
2445
|
-
test.stderr.map((line) => sanitizeHTML(line)).join("\n"),
|
|
2446
|
-
)}</pre></div>`;
|
|
2447
|
-
})()}
|
|
2448
|
-
|
|
2449
|
-
${(() => {
|
|
2450
|
-
if (!test.screenshots || test.screenshots.length === 0) return "";
|
|
2451
|
-
const screenshotsHTML = test.screenshots
|
|
2452
|
-
.map((screenshotPath, sIndex) => {
|
|
2453
|
-
try {
|
|
2454
|
-
const imagePath = path.resolve(
|
|
2455
|
-
DEFAULT_OUTPUT_DIR,
|
|
2456
|
-
screenshotPath,
|
|
2457
|
-
);
|
|
2458
|
-
if (!fsExistsSync(imagePath))
|
|
2459
|
-
return `<div class="attachment-item error">Screenshot not found</div>`;
|
|
2460
|
-
const base64ImageData =
|
|
2461
|
-
readFileSync(imagePath).toString("base64");
|
|
2462
|
-
// LAZY LOAD: Using helper with unique ID
|
|
2463
|
-
return createLazyMedia(
|
|
2464
|
-
base64ImageData,
|
|
2465
|
-
"image/png",
|
|
2466
|
-
"image",
|
|
2467
|
-
sIndex + 1,
|
|
2468
|
-
`screenshot-${testIndex}-${sIndex}.png`,
|
|
2469
|
-
);
|
|
2470
|
-
} catch (e) {
|
|
2471
|
-
return `<div class="attachment-item error">Error loading screenshot</div>`;
|
|
2472
|
-
}
|
|
2473
|
-
})
|
|
2474
|
-
.join("");
|
|
2475
|
-
return `<div class="attachments-section"><h4>Screenshots</h4><div class="attachments-grid">${screenshotsHTML}</div></div>`;
|
|
2476
|
-
})()}
|
|
2477
|
-
|
|
2764
|
+
<div class="steps-list">${generateStepsHTML(testData.steps)}</div>
|
|
2478
2765
|
${(() => {
|
|
2479
|
-
if (!
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
const mime =
|
|
2493
|
-
{ mp4: "video/mp4", webm: "video/webm" }[ext] ||
|
|
2494
|
-
"video/mp4";
|
|
2495
|
-
// LAZY LOAD: Using helper with unique ID
|
|
2496
|
-
return createLazyMedia(
|
|
2497
|
-
videoBase64,
|
|
2498
|
-
mime,
|
|
2499
|
-
"video",
|
|
2500
|
-
vIndex + 1,
|
|
2501
|
-
`video-${testIndex}-${vIndex}.${ext}`,
|
|
2502
|
-
);
|
|
2503
|
-
} catch (e) {
|
|
2504
|
-
return `<div class="attachment-item error">Error loading video</div>`;
|
|
2505
|
-
}
|
|
2506
|
-
})
|
|
2507
|
-
.join("");
|
|
2508
|
-
return `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${videosHTML}</div></div>`;
|
|
2766
|
+
if (!testData.stdout || testData.stdout.length === 0) return "";
|
|
2767
|
+
return `<div class="console-output-section">
|
|
2768
|
+
<h4>Console Output (stdout)
|
|
2769
|
+
<button class="copy-btn" onclick="copyLogContent('${logId}', this)">Copy</ button>
|
|
2770
|
+
</h4>
|
|
2771
|
+
<div class="log-wrapper">
|
|
2772
|
+
<pre id="${logId}" class="console-log stdout-log" style="background-color: #2d2d2d; color: wheat; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
2773
|
+
testData.stdout
|
|
2774
|
+
.map((line) => sanitizeHTML(line))
|
|
2775
|
+
.join("\\n"),
|
|
2776
|
+
)}</pre>
|
|
2777
|
+
</div>
|
|
2778
|
+
</div>`;
|
|
2509
2779
|
})()}
|
|
2510
|
-
|
|
2511
2780
|
${
|
|
2512
|
-
|
|
2513
|
-
? `<div class="
|
|
2514
|
-
|
|
2515
|
-
)}</
|
|
2516
|
-
test.tracePath,
|
|
2517
|
-
)}" target="_blank" download="${sanitizeHTML(
|
|
2518
|
-
path.basename(test.tracePath),
|
|
2519
|
-
)}" class="download-trace">Download Trace</a></div></div></div></div></div>`
|
|
2781
|
+
testData.stderr && testData.stderr.length > 0
|
|
2782
|
+
? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
|
|
2783
|
+
testData.stderr.map((line) => sanitizeHTML(line)).join("\\n"),
|
|
2784
|
+
)}</pre></div>`
|
|
2520
2785
|
: ""
|
|
2521
2786
|
}
|
|
2522
2787
|
${
|
|
2523
|
-
|
|
2524
|
-
?
|
|
2788
|
+
testData.screenshots && testData.screenshots.length > 0
|
|
2789
|
+
? `
|
|
2790
|
+
<div class="attachments-section">
|
|
2791
|
+
<h4>Screenshots</h4>
|
|
2792
|
+
<div class="attachments-grid">
|
|
2793
|
+
${testData.screenshots
|
|
2525
2794
|
.map(
|
|
2526
|
-
(
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2795
|
+
(screenshot, screenshotIndex) => `
|
|
2796
|
+
<div class="attachment-item">
|
|
2797
|
+
<img src="${sanitizeHTML(screenshot)}" alt="Screenshot ${
|
|
2798
|
+
screenshotIndex + 1
|
|
2799
|
+
}">
|
|
2800
|
+
<div class="attachment-info">
|
|
2801
|
+
<div class="trace-actions">
|
|
2802
|
+
<a href="${sanitizeHTML(
|
|
2803
|
+
screenshot,
|
|
2804
|
+
)}" target="_blank" class="view-full">View Full Image</a>
|
|
2805
|
+
<a href="${sanitizeHTML(
|
|
2806
|
+
screenshot,
|
|
2807
|
+
)}" target="_blank" download="screenshot-${Date.now()}-${screenshotIndex}.png">Download</a>
|
|
2808
|
+
</div>
|
|
2809
|
+
</div>
|
|
2810
|
+
</div>
|
|
2811
|
+
`,
|
|
2542
2812
|
)
|
|
2813
|
+
.join("")}
|
|
2814
|
+
</div>
|
|
2815
|
+
</div>
|
|
2816
|
+
`
|
|
2817
|
+
: ""
|
|
2818
|
+
}
|
|
2819
|
+
${
|
|
2820
|
+
testData.videoPath && testData.videoPath.length > 0
|
|
2821
|
+
? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${testData.videoPath
|
|
2822
|
+
.map((videoUrl, videoIndex) => {
|
|
2823
|
+
const fixedVideoUrl = sanitizeHTML(videoUrl);
|
|
2824
|
+
const fileExtension = String(fixedVideoUrl)
|
|
2825
|
+
.split(".")
|
|
2826
|
+
.pop()
|
|
2827
|
+
.toLowerCase();
|
|
2828
|
+
const mimeType =
|
|
2829
|
+
{
|
|
2830
|
+
mp4: "video/mp4",
|
|
2831
|
+
webm: "video/webm",
|
|
2832
|
+
ogg: "video/ogg",
|
|
2833
|
+
mov: "video/quicktime",
|
|
2834
|
+
avi: "video/x-msvideo",
|
|
2835
|
+
}[fileExtension] || "video/mp4";
|
|
2836
|
+
return `<div class="attachment-item video-item">
|
|
2837
|
+
<video controls width="100%" height="auto" title="Video ${
|
|
2838
|
+
videoIndex + 1
|
|
2839
|
+
}">
|
|
2840
|
+
<source src="${sanitizeHTML(
|
|
2841
|
+
fixedVideoUrl,
|
|
2842
|
+
)}" type="${mimeType}">
|
|
2843
|
+
Your browser does not support the video tag.
|
|
2844
|
+
</video>
|
|
2845
|
+
<div class="attachment-info">
|
|
2846
|
+
<div class="trace-actions">
|
|
2847
|
+
<a href="${sanitizeHTML(
|
|
2848
|
+
fixedVideoUrl,
|
|
2849
|
+
)}" target="_blank" download="video-${Date.now()}-${videoIndex}.${fileExtension}">Download</a>
|
|
2850
|
+
</div>
|
|
2851
|
+
</div>
|
|
2852
|
+
</div>`;
|
|
2853
|
+
})
|
|
2543
2854
|
.join("")}</div></div>`
|
|
2544
2855
|
: ""
|
|
2545
2856
|
}
|
|
2546
2857
|
${
|
|
2547
|
-
|
|
2548
|
-
?
|
|
2549
|
-
|
|
2550
|
-
|
|
2858
|
+
testData.tracePath
|
|
2859
|
+
? `
|
|
2860
|
+
<div class="attachments-section">
|
|
2861
|
+
<h4>Trace Files</h4>
|
|
2862
|
+
<div class="attachments-grid">
|
|
2863
|
+
<div class="attachment-item trace-item">
|
|
2864
|
+
<div class="trace-preview">
|
|
2865
|
+
<span class="trace-icon">📄</span>
|
|
2866
|
+
<span class="trace-name">${sanitizeHTML(
|
|
2867
|
+
path.basename(testData.tracePath),
|
|
2868
|
+
)}</span>
|
|
2869
|
+
</div>
|
|
2870
|
+
<div class="attachment-info">
|
|
2871
|
+
<div class="trace-actions">
|
|
2872
|
+
<a href="${sanitizeHTML(
|
|
2873
|
+
sanitizeHTML(testData.tracePath),
|
|
2874
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
2875
|
+
path.basename(testData.tracePath),
|
|
2876
|
+
)}" class="download-trace">Download Trace</a>
|
|
2877
|
+
</div>
|
|
2878
|
+
</div>
|
|
2879
|
+
</div>
|
|
2880
|
+
</div>
|
|
2881
|
+
</div>
|
|
2882
|
+
`
|
|
2883
|
+
: ""
|
|
2884
|
+
}
|
|
2885
|
+
${
|
|
2886
|
+
testData.attachments && testData.attachments.length > 0
|
|
2887
|
+
? `
|
|
2888
|
+
<div class="attachments-section">
|
|
2889
|
+
<h4>Other Attachments</h4>
|
|
2890
|
+
<div class="attachments-grid">
|
|
2891
|
+
${testData.attachments
|
|
2892
|
+
.map(
|
|
2893
|
+
(attachment) => `
|
|
2894
|
+
<div class="attachment-item generic-attachment">
|
|
2895
|
+
<div class="attachment-icon">${getAttachmentIcon(
|
|
2896
|
+
attachment.contentType,
|
|
2897
|
+
)}</div>
|
|
2898
|
+
<div class="attachment-caption">
|
|
2899
|
+
<span class="attachment-name" title="${sanitizeHTML(
|
|
2900
|
+
attachment.name,
|
|
2901
|
+
)}">${sanitizeHTML(attachment.name)}</span>
|
|
2902
|
+
<span class="attachment-type">${sanitizeHTML(
|
|
2903
|
+
attachment.contentType,
|
|
2904
|
+
)}</span>
|
|
2905
|
+
</div>
|
|
2906
|
+
<div class="attachment-info">
|
|
2907
|
+
<div class="trace-actions">
|
|
2908
|
+
<a href="${sanitizeHTML(
|
|
2909
|
+
sanitizeHTML(attachment.path),
|
|
2910
|
+
)}" target="_blank" class="view-full">View</a>
|
|
2911
|
+
<a href="${sanitizeHTML(
|
|
2912
|
+
sanitizeHTML(attachment.path),
|
|
2913
|
+
)}" target="_blank" download="${sanitizeHTML(
|
|
2914
|
+
attachment.name,
|
|
2915
|
+
)}" class="download-trace">Download</a>
|
|
2916
|
+
</div>
|
|
2917
|
+
</div>
|
|
2918
|
+
</div>
|
|
2919
|
+
`,
|
|
2920
|
+
)
|
|
2921
|
+
.join("")}
|
|
2922
|
+
</div>
|
|
2923
|
+
</div>
|
|
2924
|
+
`
|
|
2551
2925
|
: ""
|
|
2552
2926
|
}
|
|
2927
|
+
`;
|
|
2928
|
+
};
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
// Determine header status: use final_status if retried, else normal status
|
|
2932
|
+
const headerStatus = (test.retryHistory && test.retryHistory.length > 0 && test.final_status)
|
|
2933
|
+
? test.final_status
|
|
2934
|
+
: test.status;
|
|
2935
|
+
|
|
2936
|
+
const outcomeBadge = (test.outcome && test.outcome !== 'flaky')
|
|
2937
|
+
? `<span class="outcome-badge ${test.outcome}">${test.outcome}</span>`
|
|
2938
|
+
: '';
|
|
2939
|
+
|
|
2940
|
+
return `
|
|
2941
|
+
<div class="test-case" data-status="${
|
|
2942
|
+
headerStatus
|
|
2943
|
+
}" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
|
|
2944
|
+
.join(",")
|
|
2945
|
+
.toLowerCase()}">
|
|
2946
|
+
<div class="test-case-header" role="button" aria-expanded="false">
|
|
2947
|
+
<div class="test-case-summary">
|
|
2948
|
+
<span class="test-case-title" title="${sanitizeHTML(
|
|
2949
|
+
test.name,
|
|
2950
|
+
)}">${sanitizeHTML(testTitle)}</span>
|
|
2951
|
+
<span class="test-case-browser">(${sanitizeHTML(browser)})</span>
|
|
2952
|
+
</div>
|
|
2953
|
+
<div class="test-case-meta">
|
|
2954
|
+
${severityBadge}
|
|
2955
|
+
${retryBadge}
|
|
2956
|
+
${outcomeBadge}
|
|
2957
|
+
${
|
|
2958
|
+
test.tags && test.tags.length > 0
|
|
2959
|
+
? test.tags
|
|
2960
|
+
.map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
|
|
2961
|
+
.join(" ")
|
|
2962
|
+
: ""
|
|
2963
|
+
}
|
|
2964
|
+
</div>
|
|
2965
|
+
<div class="test-case-status-duration">
|
|
2966
|
+
<span class="status-badge ${getStatusClass(headerStatus)}">${String(
|
|
2967
|
+
headerStatus,
|
|
2968
|
+
).toUpperCase()}</span>
|
|
2969
|
+
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
2970
|
+
</div>
|
|
2971
|
+
</div>
|
|
2972
|
+
<div class="test-case-content" style="display: none;">
|
|
2973
|
+
${test.retryHistory && test.retryHistory.length > 0 ? `
|
|
2974
|
+
<div class="retry-tabs-container">
|
|
2975
|
+
<div class="retry-tabs-header">
|
|
2976
|
+
<button class="retry-tab active" onclick="switchRetryTab(event, 'base-run-${test.id}')">
|
|
2977
|
+
Base Run ${getSmallStatusBadge(test.final_status || test.status)}
|
|
2978
|
+
</button>
|
|
2979
|
+
${test.retryHistory.map((retry, idx) => `
|
|
2980
|
+
<button class="retry-tab" onclick="switchRetryTab(event, 'retry-${idx + 1}-${test.id}')">
|
|
2981
|
+
Retry ${idx + 1} ${getSmallStatusBadge(retry.final_status || retry.status)}
|
|
2982
|
+
</button>
|
|
2983
|
+
`).join('')}
|
|
2984
|
+
</div>
|
|
2985
|
+
|
|
2986
|
+
<div id="base-run-${test.id}" class="retry-tab-content active">
|
|
2987
|
+
${getTestContentHTML(test, 'base')}
|
|
2988
|
+
</div>
|
|
2989
|
+
|
|
2990
|
+
${test.retryHistory.map((retry, idx) => `
|
|
2991
|
+
<div id="retry-${idx + 1}-${test.id}" class="retry-tab-content" style="display: none;">
|
|
2992
|
+
${getTestContentHTML(retry, `retry-${idx + 1}`)}
|
|
2993
|
+
</div>
|
|
2994
|
+
`).join('')}
|
|
2995
|
+
</div>
|
|
2996
|
+
` : getTestContentHTML(test, 'single')}
|
|
2553
2997
|
</div>
|
|
2554
2998
|
</div>`;
|
|
2555
2999
|
})
|
|
@@ -2583,7 +3027,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2583
3027
|
--success-color: #34d399; --success-dark: #10b981; --success-light: #6ee7b7;
|
|
2584
3028
|
--danger-color: #f87171; --danger-dark: #ef4444; --danger-light: #fca5a5;
|
|
2585
3029
|
--warning-color: #fbbf24; --warning-dark: #f59e0b; --warning-light: #fcd34d;
|
|
2586
|
-
--info-color: #9ca3af;
|
|
3030
|
+
--info-color: #9ca3af;
|
|
3031
|
+
--flaky-color: #00ccd3;
|
|
2587
3032
|
--text-primary: #f9fafb; --text-secondary: #e5e7eb; --text-tertiary: #d1d5db;
|
|
2588
3033
|
--bg-primary: #000000; --bg-secondary: #0a0a0a; --bg-tertiary: #050505;
|
|
2589
3034
|
--bg-card: #0d0d0d; --bg-card-hover: #121212;
|
|
@@ -2591,6 +3036,8 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2591
3036
|
--light-gray-color: #262626; --medium-gray-color: #333333; --dark-gray-color: #a3a3a3;
|
|
2592
3037
|
--text-color: #f9fafb; --text-color-secondary: #e5e7eb; --border-color: #262626;
|
|
2593
3038
|
--card-background-color: #0d0d0d;
|
|
3039
|
+
--neutral-100: #171717; --neutral-200: #262626; --neutral-300: #404040;
|
|
3040
|
+
--bg-hover: #171717;
|
|
2594
3041
|
--font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
2595
3042
|
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-2xl: 24px;
|
|
2596
3043
|
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.8);
|
|
@@ -2682,7 +3129,6 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2682
3129
|
background: var(--gradient-card);
|
|
2683
3130
|
border-radius: 14px;
|
|
2684
3131
|
box-shadow: var(--shadow-md);
|
|
2685
|
-
border: 1px solid var(--border-light);
|
|
2686
3132
|
overflow: hidden;
|
|
2687
3133
|
}
|
|
2688
3134
|
.run-info-item {
|
|
@@ -2856,8 +3302,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2856
3302
|
color: #f9fafb;
|
|
2857
3303
|
text-transform: capitalize;
|
|
2858
3304
|
font-size: 1.05em;
|
|
3305
|
+
white-space: nowrap;
|
|
3306
|
+
overflow: hidden;
|
|
3307
|
+
text-overflow: ellipsis;
|
|
3308
|
+
flex: 1;
|
|
3309
|
+
min-width: 0;
|
|
3310
|
+
margin-right: 8px;
|
|
2859
3311
|
}
|
|
2860
3312
|
.browser-stats {
|
|
3313
|
+
color: #9ca3af;
|
|
3314
|
+
white-space: nowrap;
|
|
3315
|
+
flex-shrink: 0;
|
|
2861
3316
|
color: #9ca3af;
|
|
2862
3317
|
font-weight: 700;
|
|
2863
3318
|
font-size: 0.95em;
|
|
@@ -2893,57 +3348,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2893
3348
|
padding: 10px !important;
|
|
2894
3349
|
border-radius: 6px !important;
|
|
2895
3350
|
}
|
|
2896
|
-
|
|
2897
|
-
background: var(--gradient-card);
|
|
2898
|
-
border: 1px solid var(--border-light);
|
|
2899
|
-
border-radius: 12px;
|
|
2900
|
-
padding: 24px;
|
|
2901
|
-
margin-top: 20px;
|
|
2902
|
-
box-shadow: var(--shadow-md);
|
|
2903
|
-
}
|
|
2904
|
-
.env-dashboard-title {
|
|
2905
|
-
font-size: 1.2em;
|
|
2906
|
-
font-weight: 700;
|
|
2907
|
-
color: #f9fafb;
|
|
2908
|
-
margin-bottom: 8px;
|
|
2909
|
-
}
|
|
2910
|
-
.env-dashboard-subtitle {
|
|
2911
|
-
font-size: 0.9em;
|
|
2912
|
-
color: #9ca3af;
|
|
2913
|
-
margin-bottom: 16px;
|
|
2914
|
-
}
|
|
2915
|
-
.env-grid {
|
|
2916
|
-
display: grid;
|
|
2917
|
-
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
2918
|
-
gap: 12px;
|
|
2919
|
-
}
|
|
2920
|
-
.env-card {
|
|
2921
|
-
background: var(--bg-secondary);
|
|
2922
|
-
border: 1px solid var(--border-light);
|
|
2923
|
-
border-radius: 8px;
|
|
2924
|
-
padding: 14px;
|
|
2925
|
-
transition: all 0.3s ease;
|
|
2926
|
-
}
|
|
2927
|
-
.env-card:hover {
|
|
2928
|
-
transform: translateY(-3px);
|
|
2929
|
-
border-color: var(--primary-color);
|
|
2930
|
-
box-shadow: var(--shadow-lg), var(--glow-primary);
|
|
2931
|
-
background: var(--bg-card);
|
|
2932
|
-
}
|
|
2933
|
-
.env-card-header {
|
|
2934
|
-
font-size: 0.85em;
|
|
2935
|
-
color: #9ca3af;
|
|
2936
|
-
margin-bottom: 6px;
|
|
2937
|
-
text-transform: uppercase;
|
|
2938
|
-
letter-spacing: 0.5px;
|
|
2939
|
-
font-weight: 600;
|
|
2940
|
-
}
|
|
2941
|
-
.env-card-value {
|
|
2942
|
-
font-size: 1.1em;
|
|
2943
|
-
color: #f9fafb;
|
|
2944
|
-
font-weight: 700;
|
|
2945
|
-
word-break: break-word;
|
|
2946
|
-
}
|
|
3351
|
+
|
|
2947
3352
|
.suites-widget {
|
|
2948
3353
|
background: var(--bg-card);
|
|
2949
3354
|
border: 1px solid var(--border-light);
|
|
@@ -2961,17 +3366,9 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2961
3366
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
2962
3367
|
gap: 20px;
|
|
2963
3368
|
}
|
|
3369
|
+
/* Updated Suite Cards in Main Block */
|
|
2964
3370
|
.suite-card {
|
|
2965
|
-
|
|
2966
|
-
border: 1px solid var(--border-light);
|
|
2967
|
-
border-radius: 8px;
|
|
2968
|
-
padding: 20px;
|
|
2969
|
-
transition: all 0.2s ease;
|
|
2970
|
-
}
|
|
2971
|
-
.suite-card:hover {
|
|
2972
|
-
transform: translateY(-2px);
|
|
2973
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
2974
|
-
border-color: var(--primary-color);
|
|
3371
|
+
/* See line ~3455 for main definition */
|
|
2975
3372
|
}
|
|
2976
3373
|
.suite-name {
|
|
2977
3374
|
font-size: 1.1em;
|
|
@@ -3084,6 +3481,16 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3084
3481
|
.summary-card.status-skipped .value {
|
|
3085
3482
|
color: #f59e0b;
|
|
3086
3483
|
}
|
|
3484
|
+
.summary-card.flaky-status {
|
|
3485
|
+
background: rgba(0, 204, 211, 0.05);
|
|
3486
|
+
}
|
|
3487
|
+
.summary-card.flaky-status:hover {
|
|
3488
|
+
background: rgba(0, 204, 211, 0.15);
|
|
3489
|
+
box-shadow: 0 4px 12px rgba(0, 204, 211, 0.2);
|
|
3490
|
+
}
|
|
3491
|
+
.summary-card.flaky-status .value {
|
|
3492
|
+
color: #00ccd3;
|
|
3493
|
+
}
|
|
3087
3494
|
.summary-card:not([class*='status-']) .value {
|
|
3088
3495
|
color: #f9fafb;
|
|
3089
3496
|
}
|
|
@@ -3146,70 +3553,171 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3146
3553
|
.status-badge-small-tooltip.status-unknown {
|
|
3147
3554
|
background-color: #9ca3af;
|
|
3148
3555
|
}
|
|
3149
|
-
.suites-header {
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
.summary-badge {
|
|
3156
|
-
background-color: var(--border-light);
|
|
3157
|
-
color: var(--text-secondary);
|
|
3158
|
-
padding: 7px 14px;
|
|
3159
|
-
border-radius: 16px;
|
|
3160
|
-
font-size: 0.9em;
|
|
3556
|
+
.suites-header {
|
|
3557
|
+
flex-shrink: 0;
|
|
3558
|
+
display: flex;
|
|
3559
|
+
justify-content: space-between;
|
|
3560
|
+
align-items: center;
|
|
3561
|
+
margin-bottom: 20px;
|
|
3161
3562
|
}
|
|
3162
|
-
.
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3563
|
+
.summary-badge { background-color: var(--border-light); color: var(--text-secondary); padding: 7px 14px; border-radius: 16px; font-size: 0.9em; }
|
|
3564
|
+
.suites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
|
3565
|
+
.suites-widget {
|
|
3566
|
+
display: flex;
|
|
3567
|
+
flex-direction: column;
|
|
3166
3568
|
}
|
|
3167
|
-
.
|
|
3168
|
-
|
|
3169
|
-
border-left: 4px solid var(--border-light);
|
|
3170
|
-
padding: 24px;
|
|
3171
|
-
background-color: var(--bg-card);
|
|
3172
|
-
transition: all 0.15s ease;
|
|
3569
|
+
.fixed-height-widget {
|
|
3570
|
+
height: 450px;
|
|
3173
3571
|
}
|
|
3174
|
-
.
|
|
3175
|
-
|
|
3176
|
-
|
|
3572
|
+
.suites-grid-container {
|
|
3573
|
+
flex-grow: 1;
|
|
3574
|
+
overflow-y: auto;
|
|
3575
|
+
padding-right: 5px;
|
|
3177
3576
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3577
|
+
|
|
3578
|
+
@media (max-width: 768px) {
|
|
3579
|
+
.fixed-height-widget {
|
|
3580
|
+
height: auto;
|
|
3581
|
+
max-height: 600px;
|
|
3582
|
+
}
|
|
3180
3583
|
}
|
|
3181
|
-
.suite-card
|
|
3182
|
-
background:
|
|
3584
|
+
.suite-card {
|
|
3585
|
+
background: var(--bg-card); /* Changed from #ffffff */
|
|
3586
|
+
border: 1px solid var(--border-medium); /* Changed from border-light for better contrast */
|
|
3587
|
+
border-radius: 16px;
|
|
3588
|
+
padding: 24px;
|
|
3589
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); /* Darker shadow */
|
|
3590
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
3591
|
+
display: flex;
|
|
3592
|
+
flex-direction: column;
|
|
3593
|
+
height: 100%;
|
|
3594
|
+
position: relative;
|
|
3595
|
+
overflow: hidden;
|
|
3183
3596
|
}
|
|
3184
|
-
.suite-card
|
|
3185
|
-
|
|
3597
|
+
.suite-card:hover {
|
|
3598
|
+
transform: translateY(-4px);
|
|
3599
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
|
|
3600
|
+
background: var(--bg-card-hover);
|
|
3601
|
+
border-color: var(--primary-dark);
|
|
3186
3602
|
}
|
|
3187
|
-
.suite-card
|
|
3188
|
-
|
|
3603
|
+
.suite-card::before {
|
|
3604
|
+
content: '';
|
|
3605
|
+
position: absolute;
|
|
3606
|
+
top: 0;
|
|
3607
|
+
left: 0;
|
|
3608
|
+
width: 100%;
|
|
3609
|
+
height: 4px;
|
|
3610
|
+
background: var(--neutral-200);
|
|
3611
|
+
opacity: 0.8;
|
|
3612
|
+
transition: background 0.3s ease;
|
|
3613
|
+
}
|
|
3614
|
+
.suite-card.status-passed::before { background: var(--success-color); }
|
|
3615
|
+
.suite-card.status-failed::before { background: var(--danger-color); }
|
|
3616
|
+
.suite-card.status-flaky::before { background: #00ccd3; }
|
|
3617
|
+
.suite-card.status-skipped::before { background: var(--warning-color); }
|
|
3618
|
+
|
|
3619
|
+
.suite-card.status-skipped::before { background: var(--warning-color); }
|
|
3620
|
+
|
|
3621
|
+
/* Outcome Badge */
|
|
3622
|
+
.outcome-badge {
|
|
3623
|
+
background-color: var(--secondary-color);
|
|
3624
|
+
color: #000;
|
|
3625
|
+
padding: 2px 8px;
|
|
3626
|
+
border-radius: 4px;
|
|
3627
|
+
font-size: 0.75em;
|
|
3628
|
+
font-weight: 700;
|
|
3629
|
+
text-transform: uppercase;
|
|
3630
|
+
margin-right: 8px;
|
|
3631
|
+
letter-spacing: 0.5px;
|
|
3189
3632
|
}
|
|
3190
|
-
.
|
|
3191
|
-
|
|
3633
|
+
.outcome-badge.flaky {
|
|
3634
|
+
background-color: #00ccd3;
|
|
3635
|
+
color: #fff;
|
|
3192
3636
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3637
|
+
|
|
3638
|
+
.suite-card-header {
|
|
3639
|
+
display: flex;
|
|
3640
|
+
justify-content: space-between;
|
|
3641
|
+
align-items: flex-start;
|
|
3642
|
+
margin-bottom: 16px;
|
|
3195
3643
|
}
|
|
3196
|
-
.suite-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3644
|
+
.suite-name {
|
|
3645
|
+
font-size: 1.15em;
|
|
3646
|
+
font-weight: 700;
|
|
3647
|
+
color: var(--text-primary);
|
|
3648
|
+
line-height: 1.4;
|
|
3649
|
+
display: -webkit-box;
|
|
3650
|
+
-webkit-line-clamp: 2;
|
|
3651
|
+
-webkit-box-orient: vertical;
|
|
3652
|
+
overflow: hidden;
|
|
3653
|
+
margin-right: 12px;
|
|
3201
3654
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3655
|
+
.status-indicator-dot {
|
|
3656
|
+
width: 10px;
|
|
3657
|
+
height: 10px;
|
|
3658
|
+
border-radius: 50%;
|
|
3659
|
+
flex-shrink: 0;
|
|
3660
|
+
margin-top: 6px;
|
|
3208
3661
|
}
|
|
3662
|
+
.status-indicator-dot.status-passed { background-color: var(--success-color); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); }
|
|
3663
|
+
.status-indicator-dot.status-failed { background-color: var(--danger-color); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
|
|
3664
|
+
.status-indicator-dot.status-skipped { background-color: var(--warning-color); box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15); }
|
|
3665
|
+
|
|
3209
3666
|
.browser-tag {
|
|
3667
|
+
font-size: 0.8em;
|
|
3668
|
+
font-weight: 600;
|
|
3669
|
+
background: var(--bg-secondary);
|
|
3670
|
+
color: var(--text-secondary);
|
|
3671
|
+
padding: 4px 10px;
|
|
3672
|
+
border-radius: 20px;
|
|
3673
|
+
border: 1px solid var(--border-light);
|
|
3674
|
+
display: inline-flex;
|
|
3675
|
+
align-items: center;
|
|
3676
|
+
gap: 6px;
|
|
3677
|
+
margin-bottom: 20px;
|
|
3678
|
+
align-self: flex-start;
|
|
3679
|
+
box-shadow: none;
|
|
3680
|
+
text-shadow: none;
|
|
3681
|
+
}
|
|
3682
|
+
.browser-tag:hover {
|
|
3683
|
+
/* Remove hover effect from previous */
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
.suite-card-body {
|
|
3687
|
+
margin-top: auto;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
.test-count-label {
|
|
3210
3691
|
font-size: 0.85em;
|
|
3211
3692
|
font-weight: 600;
|
|
3212
|
-
|
|
3693
|
+
color: var(--text-tertiary);
|
|
3694
|
+
text-transform: uppercase;
|
|
3695
|
+
letter-spacing: 0.05em;
|
|
3696
|
+
margin-bottom: 8px;
|
|
3697
|
+
display: block;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
.suite-stats {
|
|
3701
|
+
display: flex;
|
|
3702
|
+
gap: 8px;
|
|
3703
|
+
background: var(--bg-secondary);
|
|
3704
|
+
padding: 10px 14px;
|
|
3705
|
+
border-radius: 10px;
|
|
3706
|
+
justify-content: space-between;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
.stat-pill {
|
|
3710
|
+
display: flex;
|
|
3711
|
+
align-items: center;
|
|
3712
|
+
gap: 6px;
|
|
3713
|
+
font-size: 0.9em;
|
|
3714
|
+
font-weight: 600;
|
|
3715
|
+
}
|
|
3716
|
+
.stat-pill svg { width: 14px; height: 14px; }
|
|
3717
|
+
.stat-pill.passed { color: var(--success-dark); }
|
|
3718
|
+
.stat-pill.failed { color: var(--danger-dark); }
|
|
3719
|
+
.stat-pill.flaky { color: #00ccd3; }
|
|
3720
|
+
.stat-pill.skipped { color: var(--warning-dark); }
|
|
3213
3721
|
color: #93c5fd;
|
|
3214
3722
|
padding: 6px 12px;
|
|
3215
3723
|
border-radius: var(--radius-sm);
|
|
@@ -3403,6 +3911,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3403
3911
|
.status-badge.status-skipped {
|
|
3404
3912
|
background: var(--warning-color);
|
|
3405
3913
|
}
|
|
3914
|
+
.status-badge.status-flaky {
|
|
3915
|
+
background-color: #00ccd3;
|
|
3916
|
+
color: #fff;
|
|
3917
|
+
}
|
|
3406
3918
|
.status-badge.status-unknown {
|
|
3407
3919
|
background: var(--dark-gray-color);
|
|
3408
3920
|
}
|
|
@@ -3458,6 +3970,65 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3458
3970
|
border-color: rgba(148, 163, 184, 0.25);
|
|
3459
3971
|
}
|
|
3460
3972
|
|
|
3973
|
+
/* --- RETRY COUNT BADGE --- */
|
|
3974
|
+
.retry-badge {
|
|
3975
|
+
display: inline-flex;
|
|
3976
|
+
align-items: center;
|
|
3977
|
+
padding: 5px 12px;
|
|
3978
|
+
border-radius: 12px;
|
|
3979
|
+
font-size: 0.75rem;
|
|
3980
|
+
font-weight: 600;
|
|
3981
|
+
background: rgba(147, 51, 234, 0.15);
|
|
3982
|
+
color: #a855f7;
|
|
3983
|
+
border: 1px solid rgba(147, 51, 234, 0.3);
|
|
3984
|
+
margin-left: 8px;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
/* --- RETRY TABS --- */
|
|
3988
|
+
.retry-tabs-container {
|
|
3989
|
+
margin-top: 16px;
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
.retry-tabs-header {
|
|
3993
|
+
display: flex;
|
|
3994
|
+
gap: 8px;
|
|
3995
|
+
border-bottom: 2px solid var(--border-medium);
|
|
3996
|
+
margin-bottom: 20px;
|
|
3997
|
+
flex-wrap: wrap;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
.retry-tab {
|
|
4001
|
+
padding: 10px 20px;
|
|
4002
|
+
background: transparent;
|
|
4003
|
+
border: none;
|
|
4004
|
+
border-bottom: 3px solid transparent;
|
|
4005
|
+
cursor: pointer;
|
|
4006
|
+
font-size: 0.95rem;
|
|
4007
|
+
font-weight: 600;
|
|
4008
|
+
color: var(--text-color-secondary);
|
|
4009
|
+
transition: all 0.2s ease;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
.retry-tab:hover {
|
|
4013
|
+
color: var(--primary-color);
|
|
4014
|
+
background: rgba(147, 51, 234, 0.05);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
.retry-tab.active {
|
|
4018
|
+
color: #a855f7;
|
|
4019
|
+
border-bottom-color: #a855f7;
|
|
4020
|
+
background: rgba(147, 51, 234, 0.1);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
.retry-tab-content {
|
|
4024
|
+
animation: fadeIn 0.3s ease-in;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
@keyframes fadeIn {
|
|
4028
|
+
from { opacity: 0; }
|
|
4029
|
+
to { opacity: 1; }
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3461
4032
|
.tag {
|
|
3462
4033
|
display: inline-flex;
|
|
3463
4034
|
align-items: center;
|
|
@@ -3507,7 +4078,9 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3507
4078
|
background-color: rgba(248,113,113,0.08);
|
|
3508
4079
|
border: 1px solid rgba(248,113,113,0.25);
|
|
3509
4080
|
border-left: 4px solid var(--danger-color);
|
|
3510
|
-
border-radius: 4px;
|
|
4081
|
+
border-radius: 4px;
|
|
4082
|
+
display: flex;
|
|
4083
|
+
flex-direction: column;
|
|
3511
4084
|
}
|
|
3512
4085
|
.test-error-summary h4 {
|
|
3513
4086
|
color: #ef4444;
|
|
@@ -3758,6 +4331,40 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3758
4331
|
font-size: 0.9em;
|
|
3759
4332
|
border: 1px solid var(--border-light);
|
|
3760
4333
|
}
|
|
4334
|
+
.failed-step-highlight {
|
|
4335
|
+
border-left: 4px solid var(--danger-color) !important;
|
|
4336
|
+
background-color: rgba(244,67,54,0.03);
|
|
4337
|
+
}
|
|
4338
|
+
.failed-step-highlight .step-header {
|
|
4339
|
+
background-color: rgba(244,67,54,0.05);
|
|
4340
|
+
border-color: rgba(244,67,54,0.3);
|
|
4341
|
+
}
|
|
4342
|
+
.failed-step-marker {
|
|
4343
|
+
display: inline-block;
|
|
4344
|
+
margin-left: 10px;
|
|
4345
|
+
padding: 2px 8px;
|
|
4346
|
+
background-color: var(--danger-color);
|
|
4347
|
+
color: white;
|
|
4348
|
+
border-radius: 4px;
|
|
4349
|
+
font-size: 0.85em;
|
|
4350
|
+
font-weight: 600;
|
|
4351
|
+
}
|
|
4352
|
+
.code-snippet-section {
|
|
4353
|
+
margin: 12px 0;
|
|
4354
|
+
}
|
|
4355
|
+
.code-snippet {
|
|
4356
|
+
background-color: #f8f9fa;
|
|
4357
|
+
border: 1px solid #e1e4e8;
|
|
4358
|
+
border-radius: 6px;
|
|
4359
|
+
padding: 12px;
|
|
4360
|
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
4361
|
+
font-size: 0.9em;
|
|
4362
|
+
line-height: 1.5;
|
|
4363
|
+
overflow-x: auto;
|
|
4364
|
+
color: #24292e;
|
|
4365
|
+
margin: 0;
|
|
4366
|
+
white-space: pre;
|
|
4367
|
+
}
|
|
3761
4368
|
.nested-steps {
|
|
3762
4369
|
margin-top: 12px;
|
|
3763
4370
|
}
|
|
@@ -3941,6 +4548,10 @@ function generateHTML(reportData, trendData = null) {
|
|
|
3941
4548
|
.status-badge-small.status-skipped {
|
|
3942
4549
|
background-color: #f59e0b;
|
|
3943
4550
|
}
|
|
4551
|
+
.status-badge-small.status-flaky {
|
|
4552
|
+
background-color: #00ccd3;
|
|
4553
|
+
color: #fff;
|
|
4554
|
+
}
|
|
3944
4555
|
.status-badge-small.status-unknown {
|
|
3945
4556
|
background-color: var(--dark-gray-color);
|
|
3946
4557
|
}
|
|
@@ -5166,8 +5777,17 @@ function generateHTML(reportData, trendData = null) {
|
|
|
5166
5777
|
color: #f9fafb;
|
|
5167
5778
|
text-transform: capitalize;
|
|
5168
5779
|
font-size: 1.05em;
|
|
5780
|
+
white-space: nowrap;
|
|
5781
|
+
overflow: hidden;
|
|
5782
|
+
text-overflow: ellipsis;
|
|
5783
|
+
flex: 1;
|
|
5784
|
+
min-width: 0;
|
|
5785
|
+
margin-right: 8px;
|
|
5169
5786
|
}
|
|
5170
5787
|
.browser-stats {
|
|
5788
|
+
color: #9ca3af;
|
|
5789
|
+
white-space: nowrap;
|
|
5790
|
+
flex-shrink: 0;
|
|
5171
5791
|
color: #9ca3af;
|
|
5172
5792
|
font-weight: 700;
|
|
5173
5793
|
font-size: 0.95em;
|
|
@@ -5632,31 +6252,33 @@ function generateHTML(reportData, trendData = null) {
|
|
|
5632
6252
|
<div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
|
|
5633
6253
|
runSummary.skipped || 0
|
|
5634
6254
|
}</div><div class="trend-percentage">${skipPercentage}%</div></div>
|
|
5635
|
-
<div class="summary-card"><h3>
|
|
6255
|
+
<div class="summary-card flaky-status"><h3>Flaky</h3><div class="value">${runSummary.flaky || 0}</div>
|
|
6256
|
+
<div class="trend-percentage">${flakyPercentage}%</div></div>
|
|
5636
6257
|
<div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
|
|
5637
6258
|
runSummary.duration,
|
|
5638
|
-
)}</div></div>
|
|
6259
|
+
)}</div><div class="trend-percentage">Avg. Test Duration ${avgTestDuration}</div></div>
|
|
5639
6260
|
<div class="summary-card">
|
|
5640
|
-
<h3
|
|
6261
|
+
<h3>Total Retry Count</h3>
|
|
5641
6262
|
<div class="value">${totalRetried}</div>
|
|
6263
|
+
<div class="trend-percentage">Test Retried ${retriedTestsCount}</div>
|
|
5642
6264
|
</div>
|
|
5643
6265
|
<div class="summary-card">
|
|
5644
6266
|
<h3>🌐 Browser Distribution <span style="font-size: 0.7em; color: var(--text-color-secondary); font-weight: 400;">(${browserBreakdown.length} total)</span></h3>
|
|
5645
6267
|
<div class="browser-breakdown" style="max-height: 200px; overflow-y: auto; padding-right: 4px;">
|
|
5646
6268
|
${browserBreakdown
|
|
5647
|
-
.slice(0,
|
|
6269
|
+
.slice(0, 3)
|
|
5648
6270
|
.map(
|
|
5649
6271
|
(b) =>
|
|
5650
6272
|
`<div class="browser-item">
|
|
5651
|
-
<span class="browser-name">${sanitizeHTML(b.browser)}</span>
|
|
6273
|
+
<span class="browser-name" title="${sanitizeHTML(b.browser)}">${sanitizeHTML(b.browser)}</span>
|
|
5652
6274
|
<span class="browser-stats">${b.percentage}% (${b.count})</span>
|
|
5653
6275
|
</div>`,
|
|
5654
6276
|
)
|
|
5655
6277
|
.join("")}
|
|
5656
6278
|
${
|
|
5657
|
-
browserBreakdown.length >
|
|
6279
|
+
browserBreakdown.length > 3
|
|
5658
6280
|
? `<div class="browser-item" style="opacity: 0.6; font-style: italic; justify-content: center; border-top: 1px solid var(--border-light); margin-top: 8px; padding-top: 8px;">
|
|
5659
|
-
<span>+${browserBreakdown.length -
|
|
6281
|
+
<span>+${browserBreakdown.length - 3} more browsers</span>
|
|
5660
6282
|
</div>`
|
|
5661
6283
|
: ""
|
|
5662
6284
|
}
|
|
@@ -5669,17 +6291,13 @@ function generateHTML(reportData, trendData = null) {
|
|
|
5669
6291
|
[
|
|
5670
6292
|
{ label: "Passed", value: runSummary.passed },
|
|
5671
6293
|
{ label: "Failed", value: runSummary.failed },
|
|
6294
|
+
{ label: "Flaky", value: runSummary.flaky || 0 },
|
|
5672
6295
|
{ label: "Skipped", value: runSummary.skipped || 0 },
|
|
5673
6296
|
],
|
|
5674
6297
|
400,
|
|
5675
6298
|
390,
|
|
5676
6299
|
)}
|
|
5677
|
-
${
|
|
5678
|
-
runSummary.environment &&
|
|
5679
|
-
Object.keys(runSummary.environment).length > 0
|
|
5680
|
-
? generateEnvironmentDashboard(runSummary.environment)
|
|
5681
|
-
: '<div class="no-data">Environment data not available.</div>'
|
|
5682
|
-
}
|
|
6300
|
+
${generateEnvironmentSection(runSummary.environment)}
|
|
5683
6301
|
</div>
|
|
5684
6302
|
<div style="display: flex; flex-direction: column; gap: 28px;">
|
|
5685
6303
|
${generateSuitesWidget(suitesData)}
|
|
@@ -5690,7 +6308,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
5690
6308
|
<div id="test-runs" class="tab-content">
|
|
5691
6309
|
<div class="filters" style="border-color: black; border-style: groove;">
|
|
5692
6310
|
<input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
|
|
5693
|
-
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
|
|
6311
|
+
<select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="flaky">Flaky</option><option value="skipped">Skipped</option></select>
|
|
5694
6312
|
<select id="filter-browser"><option value="">All Browsers</option>${Array.from(
|
|
5695
6313
|
new Set(
|
|
5696
6314
|
(results || []).map((test) => test.browser || "unknown"),
|
|
@@ -5781,6 +6399,29 @@ function generateHTML(reportData, trendData = null) {
|
|
|
5781
6399
|
return (ms / 1000).toFixed(1) + "s";
|
|
5782
6400
|
}
|
|
5783
6401
|
}
|
|
6402
|
+
function switchRetryTab(event, tabId) {
|
|
6403
|
+
// Find container
|
|
6404
|
+
const container = event.target.closest('.retry-tabs-container');
|
|
6405
|
+
|
|
6406
|
+
// Update tab buttons
|
|
6407
|
+
const buttons = container.querySelectorAll('.retry-tab');
|
|
6408
|
+
buttons.forEach(btn => btn.classList.remove('active'));
|
|
6409
|
+
event.target.classList.add('active');
|
|
6410
|
+
|
|
6411
|
+
// Update content
|
|
6412
|
+
const contents = container.querySelectorAll('.retry-tab-content');
|
|
6413
|
+
contents.forEach(content => {
|
|
6414
|
+
content.style.display = 'none';
|
|
6415
|
+
content.classList.remove('active');
|
|
6416
|
+
});
|
|
6417
|
+
|
|
6418
|
+
const activeContent = container.querySelector('#' + tabId);
|
|
6419
|
+
if (activeContent) {
|
|
6420
|
+
activeContent.style.display = 'block';
|
|
6421
|
+
activeContent.classList.add('active');
|
|
6422
|
+
}
|
|
6423
|
+
}
|
|
6424
|
+
|
|
5784
6425
|
function copyLogContent(elementId, button) {
|
|
5785
6426
|
const logElement = document.getElementById(elementId);
|
|
5786
6427
|
if (!logElement) {
|
|
@@ -6437,6 +7078,8 @@ async function runScript(scriptPath, args = []) {
|
|
|
6437
7078
|
* prepares the data, and then generates and writes the final HTML report file.
|
|
6438
7079
|
*/
|
|
6439
7080
|
async function main() {
|
|
7081
|
+
await animate();
|
|
7082
|
+
|
|
6440
7083
|
const __filename = fileURLToPath(import.meta.url);
|
|
6441
7084
|
const __dirname = path.dirname(__filename);
|
|
6442
7085
|
|
|
@@ -6605,6 +7248,7 @@ async function main() {
|
|
|
6605
7248
|
passed: histRunReport.run.passed,
|
|
6606
7249
|
failed: histRunReport.run.failed,
|
|
6607
7250
|
skipped: histRunReport.run.skipped || 0,
|
|
7251
|
+
flaky: histRunReport.run.flaky || (histRunReport.results ? histRunReport.results.filter(r => r.status === 'flaky' || r.outcome === 'flaky').length : 0),
|
|
6608
7252
|
});
|
|
6609
7253
|
|
|
6610
7254
|
if (histRunReport.results && Array.isArray(histRunReport.results)) {
|
|
@@ -6613,7 +7257,7 @@ async function main() {
|
|
|
6613
7257
|
(test) => ({
|
|
6614
7258
|
testName: test.name,
|
|
6615
7259
|
duration: test.duration,
|
|
6616
|
-
status: test.status,
|
|
7260
|
+
status: test.final_status || test.status,
|
|
6617
7261
|
timestamp: new Date(test.startTime),
|
|
6618
7262
|
}),
|
|
6619
7263
|
);
|
|
@@ -6631,7 +7275,7 @@ async function main() {
|
|
|
6631
7275
|
await fs.writeFile(reportHtmlPath, htmlContent, "utf-8");
|
|
6632
7276
|
console.log(
|
|
6633
7277
|
chalk.green.bold(
|
|
6634
|
-
|
|
7278
|
+
`Pulse report generated successfully at: ${reportHtmlPath}`,
|
|
6635
7279
|
),
|
|
6636
7280
|
);
|
|
6637
7281
|
console.log(chalk.gray(`(You can open this file in your browser)`));
|