@openshift-migration-advisor/shared-components 0.25.0 → 0.27.0-5d0cc20d809e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +25 -0
  2. package/dist/report/ChartDownloadButton.d.ts +11 -0
  3. package/dist/report/ChartDownloadButton.d.ts.map +1 -0
  4. package/dist/report/ChartDownloadButton.js +24 -0
  5. package/dist/report/ChartExportProvider.d.ts +23 -0
  6. package/dist/report/ChartExportProvider.d.ts.map +1 -0
  7. package/dist/report/ChartExportProvider.js +222 -0
  8. package/dist/report/ChartExportSurface.d.ts +12 -0
  9. package/dist/report/ChartExportSurface.d.ts.map +1 -0
  10. package/dist/report/ChartExportSurface.js +32 -0
  11. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  12. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  13. package/dist/report/CpuAndMemoryOverview.js +112 -0
  14. package/dist/report/ErrorTable.d.ts +18 -0
  15. package/dist/report/ErrorTable.d.ts.map +1 -0
  16. package/dist/report/ErrorTable.js +20 -0
  17. package/dist/report/HostPowerStates.d.ts +0 -1
  18. package/dist/report/HostPowerStates.d.ts.map +1 -1
  19. package/dist/report/HostPowerStates.js +2 -2
  20. package/dist/report/HostsOverview.d.ts +13 -0
  21. package/dist/report/HostsOverview.d.ts.map +1 -0
  22. package/dist/report/HostsOverview.js +84 -0
  23. package/dist/report/InfrastructureSummary.d.ts.map +1 -1
  24. package/dist/report/InfrastructureSummary.js +6 -2
  25. package/dist/report/NetworkOverview.d.ts +31 -0
  26. package/dist/report/NetworkOverview.d.ts.map +1 -0
  27. package/dist/report/NetworkOverview.js +201 -0
  28. package/dist/report/OSDistribution.d.ts +0 -2
  29. package/dist/report/OSDistribution.d.ts.map +1 -1
  30. package/dist/report/OSDistribution.js +8 -5
  31. package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
  32. package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
  33. package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
  34. package/dist/report/PowerStateCard.d.ts +0 -1
  35. package/dist/report/PowerStateCard.d.ts.map +1 -1
  36. package/dist/report/PowerStateCard.js +4 -2
  37. package/dist/report/ReportExportMenu.d.ts +19 -0
  38. package/dist/report/ReportExportMenu.d.ts.map +1 -0
  39. package/dist/report/ReportExportMenu.js +31 -0
  40. package/dist/report/ReportTable.d.ts +14 -0
  41. package/dist/report/ReportTable.d.ts.map +1 -0
  42. package/dist/report/ReportTable.js +23 -0
  43. package/dist/report/SupportTierBadge.d.ts +0 -1
  44. package/dist/report/SupportTierBadge.d.ts.map +1 -1
  45. package/dist/report/SupportTierBadge.js +1 -21
  46. package/dist/report/VCenterClusterDetails.d.ts +0 -1
  47. package/dist/report/VCenterClusterDetails.d.ts.map +1 -1
  48. package/dist/report/VCenterClusterDetails.js +9 -4
  49. package/dist/report/VmPowerStates.d.ts +0 -1
  50. package/dist/report/VmPowerStates.d.ts.map +1 -1
  51. package/dist/report/VmPowerStates.js +2 -2
  52. package/dist/report/WarningsTable.d.ts +9 -0
  53. package/dist/report/WarningsTable.d.ts.map +1 -0
  54. package/dist/report/WarningsTable.js +20 -0
  55. package/dist/report/captureChartElement.d.ts +8 -0
  56. package/dist/report/captureChartElement.d.ts.map +1 -0
  57. package/dist/report/captureChartElement.js +304 -0
  58. package/dist/report/chartExport.d.ts +80 -0
  59. package/dist/report/chartExport.d.ts.map +1 -0
  60. package/dist/report/chartExport.js +182 -0
  61. package/dist/report/chartExportContext.d.ts +32 -0
  62. package/dist/report/chartExportContext.d.ts.map +1 -0
  63. package/dist/report/chartExportContext.js +36 -0
  64. package/dist/report/chartExportFilenames.d.ts +4 -0
  65. package/dist/report/chartExportFilenames.d.ts.map +1 -0
  66. package/dist/report/chartExportFilenames.js +16 -0
  67. package/dist/report/constants.d.ts +2 -0
  68. package/dist/report/constants.d.ts.map +1 -1
  69. package/dist/report/constants.js +2 -0
  70. package/dist/report/dashboardStyles.d.ts +0 -4
  71. package/dist/report/dashboardStyles.d.ts.map +1 -1
  72. package/dist/report/dashboardStyles.js +0 -19
  73. package/dist/report/htmlExport.d.ts +9 -0
  74. package/dist/report/htmlExport.d.ts.map +1 -0
  75. package/dist/report/htmlExport.js +65 -0
  76. package/dist/report/index.d.ts +19 -0
  77. package/dist/report/index.d.ts.map +1 -1
  78. package/dist/report/index.js +19 -0
  79. package/dist/report/pdfExport.d.ts +3 -0
  80. package/dist/report/pdfExport.d.ts.map +1 -0
  81. package/dist/report/pdfExport.js +131 -0
  82. package/dist/report/pdfPage.d.ts +15 -0
  83. package/dist/report/pdfPage.d.ts.map +1 -0
  84. package/dist/report/pdfPage.js +44 -0
  85. package/dist/report/reportExportOptions.d.ts +17 -0
  86. package/dist/report/reportExportOptions.d.ts.map +1 -0
  87. package/dist/report/reportExportOptions.js +31 -0
  88. package/dist/report/zipChartPngs.d.ts +3 -0
  89. package/dist/report/zipChartPngs.d.ts.map +1 -0
  90. package/dist/report/zipChartPngs.js +8 -0
  91. package/package.json +7 -2
  92. package/src/report/ChartDownloadButton.tsx +59 -0
  93. package/src/report/ChartExportProvider.tsx +392 -0
  94. package/src/report/ChartExportSurface.tsx +60 -0
  95. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  96. package/src/report/ErrorTable.tsx +85 -0
  97. package/src/report/HostPowerStates.tsx +0 -3
  98. package/src/report/HostsOverview.tsx +185 -0
  99. package/src/report/InfrastructureSummary.tsx +48 -31
  100. package/src/report/NetworkOverview.tsx +405 -0
  101. package/src/report/OSDistribution.tsx +46 -42
  102. package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
  103. package/src/report/PowerStateCard.tsx +49 -36
  104. package/src/report/ReportExportMenu.tsx +119 -0
  105. package/src/report/ReportTable.tsx +81 -0
  106. package/src/report/SupportTierBadge.tsx +12 -40
  107. package/src/report/VCenterClusterDetails.tsx +38 -16
  108. package/src/report/VmPowerStates.tsx +0 -3
  109. package/src/report/WarningsTable.tsx +75 -0
  110. package/src/report/__tests__/ChartExportProvider.test.tsx +582 -0
  111. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  112. package/src/report/__tests__/HostsOverview.test.tsx +145 -0
  113. package/src/report/__tests__/InfrastructureSummary.test.tsx +0 -22
  114. package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
  115. package/src/report/__tests__/OSDistribution.test.tsx +10 -11
  116. package/src/report/__tests__/captureChartElement.test.ts +205 -0
  117. package/src/report/__tests__/chartExportPackagers.test.ts +299 -0
  118. package/src/report/captureChartElement.ts +398 -0
  119. package/src/report/chartExport.ts +277 -0
  120. package/src/report/chartExportContext.tsx +92 -0
  121. package/src/report/chartExportFilenames.ts +21 -0
  122. package/src/report/constants.ts +2 -0
  123. package/src/report/dashboardStyles.ts +0 -23
  124. package/src/report/htmlExport.ts +83 -0
  125. package/src/report/index.ts +102 -0
  126. package/src/report/pdfExport.ts +233 -0
  127. package/src/report/pdfPage.ts +83 -0
  128. package/src/report/reportExportOptions.ts +53 -0
  129. package/src/report/zipChartPngs.ts +10 -0
@@ -0,0 +1,200 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
6
+ import { CpuAndMemoryOverview } from "../CpuAndMemoryOverview.js";
7
+
8
+ const donutProps = vi.hoisted(() => ({
9
+ current: null as MigrationDonutChartProps | null,
10
+ }));
11
+
12
+ vi.mock("../../charts/MigrationDonutChart.js", () => ({
13
+ MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
14
+ donutProps.current = props;
15
+ return (
16
+ <div data-testid="cpu-memory-donut">
17
+ {props.title} {props.subTitle}
18
+ </div>
19
+ );
20
+ },
21
+ }));
22
+
23
+ afterEach(() => {
24
+ donutProps.current = null;
25
+ cleanup();
26
+ });
27
+
28
+ const renderedDonut = (): MigrationDonutChartProps => {
29
+ const props = donutProps.current;
30
+ if (!props) {
31
+ throw new Error("expected a CPU and memory donut to render");
32
+ }
33
+ return props;
34
+ };
35
+
36
+ describe("CpuAndMemoryOverview", () => {
37
+ it("shows the memory empty state when no memory tiers were collected", () => {
38
+ render(<CpuAndMemoryOverview />);
39
+
40
+ expect(screen.getByText("CPU & memory")).toBeInTheDocument();
41
+ expect(screen.getByText("Memory size tiers")).toBeInTheDocument();
42
+ expect(screen.getByText("Memory data not collected")).toBeInTheDocument();
43
+ expect(screen.queryByTestId("cpu-memory-donut")).not.toBeInTheDocument();
44
+ });
45
+
46
+ it("plots memory tiers from a plain count map and appends GB", () => {
47
+ render(
48
+ <CpuAndMemoryOverview
49
+ memoryTierDistribution={{
50
+ "129-256": 1,
51
+ "0-4": 2,
52
+ "256+": 3,
53
+ "5-16 GB": 4,
54
+ "17–32": 5,
55
+ unused: 0,
56
+ }}
57
+ memoryTotalGB={1024}
58
+ />,
59
+ );
60
+
61
+ const props = renderedDonut();
62
+ expect(props.title).toBe("15 VMs");
63
+ expect(props.subTitle).toBe(`${(1024).toLocaleString()} GB`);
64
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
65
+ ["0-4 GB", 2],
66
+ ["5-16 GB", 4],
67
+ ["17–32 GB", 5],
68
+ ["129-256 GB", 1],
69
+ ["256+ GB", 3],
70
+ ]);
71
+ expect(props.data.map((slice) => slice.legendCategory)).toEqual([
72
+ "0-4",
73
+ "5-16 GB",
74
+ "17–32",
75
+ "129-256",
76
+ "256+",
77
+ ]);
78
+ expect(props.legend).toEqual({
79
+ "0-4": "#0066cc",
80
+ "5-16 GB": "#5e40be",
81
+ "17–32": "#b6a6e9",
82
+ "129-256": "#73c5c5",
83
+ "256+": "#b98412",
84
+ });
85
+ expect(props.legendVariant).toBe("html");
86
+ expect(props.marginLeft).toBeUndefined();
87
+ expect(props.onItemClick).toBeUndefined();
88
+ expect(props.onTitleClick).toBeUndefined();
89
+ expect(
90
+ props.legendLabelFormatter?.({ x: "0-4 GB", countDisplay: "2 VMs" }),
91
+ ).toBe("0-4 GB (2 VMs)");
92
+ expect(
93
+ props.tooltipLabelFormatter?.({
94
+ datum: {
95
+ x: "0-4 GB",
96
+ y: 2,
97
+ countDisplay: "2 VMs",
98
+ legendCategory: "0-4",
99
+ },
100
+ percent: 13.333,
101
+ total: 15,
102
+ }),
103
+ ).toBe("2 VMs\n13.3%");
104
+ });
105
+
106
+ it("calls onTierSelect with the memory distribution key", () => {
107
+ const onTierSelect = vi.fn();
108
+ const onTitleClick = vi.fn();
109
+ render(
110
+ <CpuAndMemoryOverview
111
+ memoryTierDistribution={{ "0-4": 2 }}
112
+ onTierSelect={onTierSelect}
113
+ onTitleClick={onTitleClick}
114
+ />,
115
+ );
116
+
117
+ renderedDonut().onItemClick?.({
118
+ name: "0-4 GB",
119
+ count: 2,
120
+ legendCategory: "0-4",
121
+ });
122
+ renderedDonut().onTitleClick?.();
123
+
124
+ expect(onTierSelect).toHaveBeenCalledTimes(1);
125
+ expect(onTierSelect).toHaveBeenCalledWith("0-4");
126
+ expect(onTitleClick).toHaveBeenCalledTimes(1);
127
+ });
128
+
129
+ it("switches to vCPU tiers and omits slice clicks", async () => {
130
+ const user = userEvent.setup();
131
+ const onTierSelect = vi.fn();
132
+ render(
133
+ <CpuAndMemoryOverview
134
+ legendVariant="chart"
135
+ cpuTierDistribution={{
136
+ "8": 1,
137
+ "2 cores": 2,
138
+ "1-2": 4,
139
+ "4+": 1,
140
+ idle: 0,
141
+ }}
142
+ cpuTotalCores={64}
143
+ memoryTierDistribution={{ "0-4": 2 }}
144
+ onTierSelect={onTierSelect}
145
+ />,
146
+ );
147
+
148
+ await user.click(
149
+ screen.getByRole("button", {
150
+ name: "VM distribution by memory size tier",
151
+ }),
152
+ );
153
+ await user.click(
154
+ screen.getByRole("menuitem", {
155
+ name: "VM distribution by vCPU count tier",
156
+ }),
157
+ );
158
+
159
+ expect(screen.getByText("vCPU count tiers")).toBeInTheDocument();
160
+ expect(
161
+ screen.getByRole("button", {
162
+ name: "VM distribution by vCPU count tier",
163
+ }),
164
+ ).toBeInTheDocument();
165
+
166
+ const props = renderedDonut();
167
+ expect(props.title).toBe("8 VMs");
168
+ expect(props.subTitle).toBe(`${(64).toLocaleString()} Cores`);
169
+ expect(props.legendVariant).toBe("chart");
170
+ expect(props.marginLeft).toBe("52%");
171
+ expect(props.labelFontSize).toBe(18);
172
+ expect(props.itemsPerRow).toBe(2);
173
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
174
+ ["1-2 cores", 4],
175
+ ["2 cores", 2],
176
+ ["4+ cores", 1],
177
+ ["8 cores", 1],
178
+ ]);
179
+ expect(props.onItemClick).toBeUndefined();
180
+ });
181
+
182
+ it("shows the CPU empty state when the selected distribution is empty", async () => {
183
+ const user = userEvent.setup();
184
+ render(<CpuAndMemoryOverview cpuTierDistribution={{}} />);
185
+
186
+ await user.click(
187
+ screen.getByRole("button", {
188
+ name: "VM distribution by memory size tier",
189
+ }),
190
+ );
191
+ await user.click(
192
+ screen.getByRole("menuitem", {
193
+ name: "VM distribution by vCPU count tier",
194
+ }),
195
+ );
196
+
197
+ expect(screen.getByText("CPU data not collected")).toBeInTheDocument();
198
+ expect(screen.queryByTestId("cpu-memory-donut")).not.toBeInTheDocument();
199
+ });
200
+ });
@@ -0,0 +1,145 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ chart_color_blue_300,
4
+ chart_color_green_300,
5
+ chart_color_purple_100,
6
+ chart_color_purple_300,
7
+ chart_color_teal_200,
8
+ chart_color_yellow_400,
9
+ } from "@patternfly/react-tokens";
10
+ import { cleanup, render, screen } from "@testing-library/react";
11
+ import { afterEach, describe, expect, it, vi } from "vitest";
12
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
13
+ import { HostsOverview } from "../HostsOverview.js";
14
+
15
+ const donutProps = vi.hoisted(() => ({
16
+ current: null as MigrationDonutChartProps | null,
17
+ }));
18
+
19
+ vi.mock("../../charts/MigrationDonutChart.js", () => ({
20
+ MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
21
+ donutProps.current = props;
22
+ return (
23
+ <div data-testid="hosts-donut">
24
+ {props.title} {props.subTitle}
25
+ </div>
26
+ );
27
+ },
28
+ }));
29
+
30
+ afterEach(() => {
31
+ donutProps.current = null;
32
+ cleanup();
33
+ });
34
+
35
+ const renderedDonut = (): MigrationDonutChartProps => {
36
+ const props = donutProps.current;
37
+ if (!props) {
38
+ throw new Error("expected the host model donut to render");
39
+ }
40
+ return props;
41
+ };
42
+
43
+ describe("HostsOverview", () => {
44
+ it("shows the shared empty state when no hosts are available", () => {
45
+ render(<HostsOverview />);
46
+
47
+ expect(screen.getByText("Host distribution by model")).toBeInTheDocument();
48
+ expect(screen.getByText("Top 5 models")).toBeInTheDocument();
49
+ expect(screen.getByText("Host data not collected")).toBeInTheDocument();
50
+ expect(screen.queryByTestId("hosts-donut")).not.toBeInTheDocument();
51
+ });
52
+
53
+ it("buckets host counts into the top 5 models plus Other models", () => {
54
+ render(
55
+ <HostsOverview
56
+ hosts={[
57
+ { model: "M1" },
58
+ { model: "M1" },
59
+ { model: "M1" },
60
+ { model: "M1" },
61
+ { model: "M1" },
62
+ { model: "M1" },
63
+ { model: "M2" },
64
+ { model: "M2" },
65
+ { model: "M2" },
66
+ { model: "M2" },
67
+ { model: "M2" },
68
+ { model: "M3" },
69
+ { model: "M3" },
70
+ { model: "M3" },
71
+ { model: "M3" },
72
+ { model: "M4" },
73
+ { model: "M4" },
74
+ { model: "M4" },
75
+ { model: "M5" },
76
+ { model: "M5" },
77
+ { model: "M6" },
78
+ { model: "M7" },
79
+ ]}
80
+ />,
81
+ );
82
+
83
+ expect(screen.getByTestId("hosts-donut")).toHaveTextContent("22 Hosts");
84
+
85
+ const props = renderedDonut();
86
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
87
+ ["M1", 6],
88
+ ["M2", 5],
89
+ ["M3", 4],
90
+ ["M4", 3],
91
+ ["M5", 2],
92
+ ["Other models", 2],
93
+ ]);
94
+ expect(props.data[5]?.countDisplay).toBe("2 hosts");
95
+ expect(props.legend).toEqual({
96
+ M1: chart_color_blue_300.value,
97
+ M2: chart_color_purple_300.value,
98
+ M3: chart_color_purple_100.value,
99
+ M4: chart_color_teal_200.value,
100
+ M5: chart_color_yellow_400.value,
101
+ "Other models": chart_color_green_300.value,
102
+ });
103
+ expect(props.legendVariant).toBe("html");
104
+ expect(props.height).toBe(300);
105
+ expect(props.width).toBe(420);
106
+ expect(props.donutThickness).toBe(18);
107
+ expect(props.titleFontSize).toBe(34);
108
+ expect(props.legendWidth).toBe(680);
109
+ expect(props.itemsPerRow).toBe(2);
110
+ expect(props.labelFontSize).toBe(16);
111
+ expect(props.marginLeft).toBe("0%");
112
+ expect(props.subTitleColor).toBe(
113
+ "var(--pf-t--global--text--color--subtle)",
114
+ );
115
+ expect(
116
+ props.tooltipLabelFormatter?.({
117
+ datum: {
118
+ x: "M1",
119
+ y: 6,
120
+ countDisplay: "6 hosts",
121
+ legendCategory: "M1",
122
+ },
123
+ percent: 27.2727,
124
+ total: 22,
125
+ }),
126
+ ).toBe("6 hosts\n27.3%");
127
+ });
128
+
129
+ it("treats numeric models as labels and blank models as unknown", () => {
130
+ render(
131
+ <HostsOverview
132
+ legendVariant="chart"
133
+ hosts={[{ model: 42 }, { model: " " }, { model: " R640 " }]}
134
+ />,
135
+ );
136
+
137
+ expect(renderedDonut().data.map((slice) => slice.name)).toEqual([
138
+ "42",
139
+ "Unknown model",
140
+ "R640",
141
+ ]);
142
+ expect(renderedDonut().legendVariant).toBe("chart");
143
+ expect(renderedDonut().title).toBe("3");
144
+ });
145
+ });
@@ -84,28 +84,6 @@ describe("VCenterClusterDetails", () => {
84
84
  ).toHaveAttribute("data-scroll-constrained", "true");
85
85
  });
86
86
 
87
- it("does not clip the aggregate table in export mode", () => {
88
- render(
89
- <VCenterClusterDetails
90
- isAggregateView
91
- isExportMode
92
- rows={Array.from({ length: 12 }, (_, index) => ({
93
- id: `cluster-${index}`,
94
- name: `cluster-${index}`,
95
- hosts: index + 1,
96
- vms: (index + 1) * 10,
97
- vmotion: "unknown",
98
- drs: "unknown",
99
- vsan: "disabled",
100
- }))}
101
- />,
102
- );
103
-
104
- expect(
105
- screen.getByTestId("vcenter-cluster-details-table").parentElement,
106
- ).not.toHaveAttribute("data-scroll-constrained");
107
- });
108
-
109
87
  it("renders the detailed cluster cards for a single cluster", () => {
110
88
  render(
111
89
  <VCenterClusterDetails
@@ -0,0 +1,269 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ chart_color_black_500,
4
+ chart_color_blue_300,
5
+ chart_color_green_300,
6
+ chart_color_orange_100,
7
+ chart_color_purple_100,
8
+ chart_color_purple_300,
9
+ chart_color_red_orange_200,
10
+ chart_color_teal_200,
11
+ chart_color_teal_400,
12
+ chart_color_yellow_400,
13
+ } from "@patternfly/react-tokens";
14
+ import { cleanup, render, screen } from "@testing-library/react";
15
+ import userEvent from "@testing-library/user-event";
16
+ import { afterEach, describe, expect, it, vi } from "vitest";
17
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
18
+ import { NetworkOverview } from "../NetworkOverview.js";
19
+
20
+ const donutProps = vi.hoisted(() => ({
21
+ current: null as MigrationDonutChartProps | null,
22
+ }));
23
+
24
+ vi.mock("../../charts/MigrationDonutChart.js", () => ({
25
+ MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
26
+ donutProps.current = props;
27
+ return (
28
+ <div data-testid="network-donut">
29
+ {props.title} {props.subTitle}
30
+ </div>
31
+ );
32
+ },
33
+ }));
34
+
35
+ afterEach(() => {
36
+ donutProps.current = null;
37
+ cleanup();
38
+ });
39
+
40
+ const renderedDonut = (): MigrationDonutChartProps => {
41
+ const props = donutProps.current;
42
+ if (!props) {
43
+ throw new Error("expected a network donut to render");
44
+ }
45
+ return props;
46
+ };
47
+
48
+ describe("NetworkOverview", () => {
49
+ it("shows the network empty state when no networks were collected", () => {
50
+ render(<NetworkOverview />);
51
+
52
+ expect(screen.getByText("Networks")).toBeInTheDocument();
53
+ expect(screen.getByText("Top 5 networks")).toBeInTheDocument();
54
+ expect(screen.getByText("Network data not collected")).toBeInTheDocument();
55
+ expect(screen.queryByTestId("network-donut")).not.toBeInTheDocument();
56
+ });
57
+
58
+ it("keeps the top 4 networks and rolls the rest into one slice", () => {
59
+ render(
60
+ <NetworkOverview
61
+ infra={{
62
+ networks: [
63
+ { vmsCount: 10, vlanId: "100" },
64
+ { vmsCount: 8, vlanId: 200 },
65
+ { vmsCount: 6, vlanId: " " },
66
+ { vmsCount: 4 },
67
+ { vmsCount: 3, vlanId: "9" },
68
+ { vmsCount: 1, vlanId: "8" },
69
+ { vmsCount: Number.NaN },
70
+ ],
71
+ }}
72
+ />,
73
+ );
74
+
75
+ const props = renderedDonut();
76
+ expect(props.title).toBe("32");
77
+ expect(props.subTitle).toBe("VMs");
78
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
79
+ ["Network 1", 10],
80
+ ["Network 2", 8],
81
+ ["Network 3", 6],
82
+ ["Network 4", 4],
83
+ ["Rest of networks", 4],
84
+ ]);
85
+ expect(props.legend).toEqual({
86
+ "Network 1": chart_color_blue_300.value,
87
+ "Network 2": chart_color_purple_300.value,
88
+ "Network 3": chart_color_purple_100.value,
89
+ "Network 4": chart_color_teal_200.value,
90
+ "Rest of networks": chart_color_yellow_400.value,
91
+ });
92
+ expect(props.legendVariant).toBe("html");
93
+ expect(props.height).toBe(300);
94
+ expect(props.width).toBe(420);
95
+ expect(props.donutThickness).toBe(18);
96
+ expect(props.titleFontSize).toBe(34);
97
+ expect(props.labelFontSize).toBe(18);
98
+ expect(props.itemsPerRow).toBe(3);
99
+ expect(props.marginLeft).toBe("0%");
100
+ expect(
101
+ props.tooltipLabelFormatter?.({
102
+ datum: {
103
+ x: "Network 1",
104
+ y: 10,
105
+ countDisplay: "10 VMs",
106
+ legendCategory: "Network 1",
107
+ },
108
+ percent: 31.25,
109
+ total: 32,
110
+ }),
111
+ ).toBe("10 VMs\n31.3%\nVLAN: 100");
112
+ expect(
113
+ props.tooltipLabelFormatter?.({
114
+ datum: {
115
+ x: "Network 2",
116
+ y: 8,
117
+ countDisplay: "8 VMs",
118
+ legendCategory: "Network 2",
119
+ },
120
+ percent: 25,
121
+ total: 32,
122
+ }),
123
+ ).toBe("8 VMs\n25.0%\nVLAN: 200");
124
+ expect(
125
+ props.tooltipLabelFormatter?.({
126
+ datum: {
127
+ x: "Rest of networks",
128
+ y: 4,
129
+ countDisplay: "4 VMs",
130
+ legendCategory: "Rest of networks",
131
+ },
132
+ percent: 12.5,
133
+ total: 32,
134
+ }),
135
+ ).toBe("4 VMs\n12.5%\nVLAN: -");
136
+ });
137
+
138
+ it("switches to NIC count and falls back to the histogram", async () => {
139
+ const user = userEvent.setup();
140
+ render(
141
+ <NetworkOverview
142
+ legendVariant="chart"
143
+ nicCount={{
144
+ total: 9,
145
+ histogram: { minValue: 1, step: 1, data: [4, 0, 5] },
146
+ }}
147
+ />,
148
+ );
149
+
150
+ await user.click(
151
+ screen.getByRole("button", { name: "VM distribution by network" }),
152
+ );
153
+ await user.click(
154
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
155
+ );
156
+
157
+ expect(screen.queryByText("Top 5 networks")).not.toBeInTheDocument();
158
+ expect(
159
+ screen.getByRole("button", { name: "VM distribution by NIC count" }),
160
+ ).toBeInTheDocument();
161
+
162
+ const props = renderedDonut();
163
+ expect(props.legendVariant).toBe("chart");
164
+ expect(props.title).toBe("9");
165
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
166
+ ["1 NIC", 4],
167
+ ["3 NIC", 5],
168
+ ]);
169
+ expect(
170
+ props.tooltipLabelFormatter?.({
171
+ datum: {
172
+ x: "1 NIC",
173
+ y: 4,
174
+ countDisplay: "4 VMs",
175
+ legendCategory: "1 NIC",
176
+ },
177
+ percent: 44.444,
178
+ total: 9,
179
+ }),
180
+ ).toBe("4 VMs\n44.4%");
181
+ });
182
+
183
+ it("plots distributionByNicCount ahead of the deprecated histogram", async () => {
184
+ const user = userEvent.setup();
185
+ render(
186
+ <NetworkOverview
187
+ distributionByNicCount={{ "2": 5, "4+": 2, "1": 3, "0": 1 }}
188
+ nicCount={{
189
+ total: 99,
190
+ histogram: { minValue: 1, step: 1, data: [9] },
191
+ }}
192
+ />,
193
+ );
194
+
195
+ await user.click(
196
+ screen.getByRole("button", { name: "VM distribution by network" }),
197
+ );
198
+ await user.click(
199
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
200
+ );
201
+
202
+ const props = renderedDonut();
203
+ expect(props.title).toBe("11");
204
+ expect(props.data.map((slice) => slice.name)).toEqual([
205
+ "0 NIC",
206
+ "1 NIC",
207
+ "2 NIC",
208
+ "4+ NIC",
209
+ ]);
210
+ expect(props.legend).toEqual({
211
+ "0 NIC": chart_color_blue_300.value,
212
+ "1 NIC": chart_color_purple_300.value,
213
+ "2 NIC": chart_color_purple_100.value,
214
+ "4+ NIC": chart_color_teal_200.value,
215
+ });
216
+ });
217
+
218
+ it("shows the NIC empty state when the selected distribution is empty", async () => {
219
+ const user = userEvent.setup();
220
+ render(<NetworkOverview distributionByNicCount={{}} />);
221
+
222
+ await user.click(
223
+ screen.getByRole("button", { name: "VM distribution by network" }),
224
+ );
225
+ await user.click(
226
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
227
+ );
228
+
229
+ expect(
230
+ screen.getByText("NIC count data not collected"),
231
+ ).toBeInTheDocument();
232
+ expect(screen.queryByTestId("network-donut")).not.toBeInTheDocument();
233
+ });
234
+
235
+ it("uses the extended palette when there are more NIC buckets than base colors", async () => {
236
+ const user = userEvent.setup();
237
+ render(
238
+ <NetworkOverview
239
+ distributionByNicCount={{
240
+ "0": 1,
241
+ "1": 1,
242
+ "2": 1,
243
+ "3": 1,
244
+ "4": 1,
245
+ "5": 1,
246
+ "6": 1,
247
+ "7": 1,
248
+ "8": 1,
249
+ "9": 1,
250
+ }}
251
+ />,
252
+ );
253
+
254
+ await user.click(
255
+ screen.getByRole("button", { name: "VM distribution by network" }),
256
+ );
257
+ await user.click(
258
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
259
+ );
260
+
261
+ expect(renderedDonut().legend).toMatchObject({
262
+ "5 NIC": chart_color_green_300.value,
263
+ "6 NIC": chart_color_orange_100.value,
264
+ "7 NIC": chart_color_red_orange_200.value,
265
+ "8 NIC": chart_color_teal_400.value,
266
+ "9 NIC": chart_color_black_500.value,
267
+ });
268
+ });
269
+ });
@@ -82,14 +82,13 @@ describe("OSBarChart", () => {
82
82
  expect(within(popover).getByText("Upgrade to RHEL 7")).toBeInTheDocument();
83
83
  });
84
84
 
85
- it("does not show upgrade recommendation icon in export mode", () => {
86
- render(<OSBarChart osData={sampleOsData} isExportMode />);
85
+ it("marks the upgrade recommendation icon as capture chrome", () => {
86
+ render(<OSBarChart osData={sampleOsData} />);
87
87
 
88
- expect(
89
- screen.queryByRole("button", {
90
- name: "Open operating system upgrade information",
91
- }),
92
- ).not.toBeInTheDocument();
88
+ const upgradeButton = screen.getByRole("button", {
89
+ name: "Open operating system upgrade information",
90
+ });
91
+ expect(upgradeButton.closest("[data-chart-export-hide]")).not.toBeNull();
93
92
  });
94
93
 
95
94
  it("shows official Red Hat definitions in tier badge tooltips", async () => {
@@ -149,12 +148,12 @@ describe("OSBarChart", () => {
149
148
  ).toBeInTheDocument();
150
149
  });
151
150
 
152
- it("renders export-friendly inline tier badge colors in export mode", () => {
153
- render(<OSBarChart osData={sampleOsData} isExportMode />);
151
+ it("uses explicit badge colors so html2canvas can capture them", () => {
152
+ render(<OSBarChart osData={sampleOsData} />);
154
153
 
155
154
  const certifiedBadge = screen.getByText("Certified");
156
- expect(certifiedBadge.tagName).toBe("SPAN");
157
- expect(certifiedBadge).toHaveStyle({
155
+ const styledBadge = certifiedBadge.closest("[style]") ?? certifiedBadge;
156
+ expect(styledBadge).toHaveStyle({
158
157
  backgroundColor: "rgb(185, 218, 252)",
159
158
  color: "rgb(0, 77, 153)",
160
159
  });