@openshift-migration-advisor/shared-components 0.24.0 → 0.25.0-6fd4e1ee8e11
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 +31 -0
- package/dist/charts/index.d.ts +1 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/forms/RadioButtonFormGroup.d.ts.map +1 -1
- package/dist/forms/RadioButtonFormGroup.js +2 -2
- package/dist/report/ChartDownloadButton.d.ts +11 -0
- package/dist/report/ChartDownloadButton.d.ts.map +1 -0
- package/dist/report/ChartDownloadButton.js +24 -0
- package/dist/report/ChartExportProvider.d.ts +22 -0
- package/dist/report/ChartExportProvider.d.ts.map +1 -0
- package/dist/report/ChartExportProvider.js +222 -0
- package/dist/report/ChartExportSurface.d.ts +12 -0
- package/dist/report/ChartExportSurface.d.ts.map +1 -0
- package/dist/report/ChartExportSurface.js +32 -0
- package/dist/report/FeatureStatusBadge.d.ts +7 -0
- package/dist/report/FeatureStatusBadge.d.ts.map +1 -0
- package/dist/report/FeatureStatusBadge.js +13 -0
- package/dist/report/HostPowerStates.d.ts +8 -0
- package/dist/report/HostPowerStates.d.ts.map +1 -0
- package/dist/report/HostPowerStates.js +11 -0
- package/dist/report/InfrastructureSummary.d.ts +7 -0
- package/dist/report/InfrastructureSummary.d.ts.map +1 -0
- package/dist/report/InfrastructureSummary.js +9 -0
- package/dist/report/OSDistribution.d.ts +0 -2
- package/dist/report/OSDistribution.d.ts.map +1 -1
- package/dist/report/OSDistribution.js +10 -15
- package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -1
- package/dist/report/OsSupportTiersHelpPopover.js +2 -31
- package/dist/report/OsUpgradeNotice.d.ts.map +1 -1
- package/dist/report/OsUpgradeNotice.js +1 -5
- package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
- package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
- package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
- package/dist/report/PowerStateCard.d.ts +16 -0
- package/dist/report/PowerStateCard.d.ts.map +1 -0
- package/dist/report/PowerStateCard.js +9 -0
- package/dist/report/ReportExportMenu.d.ts +19 -0
- package/dist/report/ReportExportMenu.d.ts.map +1 -0
- package/dist/report/ReportExportMenu.js +31 -0
- package/dist/report/SupportTierBadge.d.ts +0 -1
- package/dist/report/SupportTierBadge.d.ts.map +1 -1
- package/dist/report/SupportTierBadge.js +1 -19
- package/dist/report/VCenterClusterDetails.d.ts +9 -0
- package/dist/report/VCenterClusterDetails.d.ts.map +1 -0
- package/dist/report/VCenterClusterDetails.js +31 -0
- package/dist/report/VmPowerStates.d.ts +8 -0
- package/dist/report/VmPowerStates.d.ts.map +1 -0
- package/dist/report/VmPowerStates.js +11 -0
- package/dist/report/captureChartElement.d.ts +8 -0
- package/dist/report/captureChartElement.d.ts.map +1 -0
- package/dist/report/captureChartElement.js +284 -0
- package/dist/report/chartExport.d.ts +57 -0
- package/dist/report/chartExport.d.ts.map +1 -0
- package/dist/report/chartExport.js +162 -0
- package/dist/report/chartExportContext.d.ts +28 -0
- package/dist/report/chartExportContext.d.ts.map +1 -0
- package/dist/report/chartExportContext.js +36 -0
- package/dist/report/chartExportFilenames.d.ts +4 -0
- package/dist/report/chartExportFilenames.d.ts.map +1 -0
- package/dist/report/chartExportFilenames.js +16 -0
- package/dist/report/constants.d.ts +2 -0
- package/dist/report/constants.d.ts.map +1 -1
- package/dist/report/constants.js +2 -0
- package/dist/report/dashboardStyles.d.ts +0 -4
- package/dist/report/dashboardStyles.d.ts.map +1 -1
- package/dist/report/dashboardStyles.js +0 -19
- package/dist/report/htmlExport.d.ts +9 -0
- package/dist/report/htmlExport.d.ts.map +1 -0
- package/dist/report/htmlExport.js +65 -0
- package/dist/report/index.d.ts +23 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +21 -0
- package/dist/report/infrastructureSummaryModel.d.ts +82 -0
- package/dist/report/infrastructureSummaryModel.d.ts.map +1 -0
- package/dist/report/infrastructureSummaryModel.js +139 -0
- package/dist/report/pdfExport.d.ts +3 -0
- package/dist/report/pdfExport.d.ts.map +1 -0
- package/dist/report/pdfExport.js +82 -0
- package/dist/report/pdfPage.d.ts +15 -0
- package/dist/report/pdfPage.d.ts.map +1 -0
- package/dist/report/pdfPage.js +44 -0
- package/dist/report/powerStates.d.ts +11 -0
- package/dist/report/powerStates.d.ts.map +1 -0
- package/dist/report/powerStates.js +113 -0
- package/dist/report/reportExportOptions.d.ts +17 -0
- package/dist/report/reportExportOptions.d.ts.map +1 -0
- package/dist/report/reportExportOptions.js +31 -0
- package/dist/report/zipChartPngs.d.ts +3 -0
- package/dist/report/zipChartPngs.d.ts.map +1 -0
- package/dist/report/zipChartPngs.js +8 -0
- package/package.json +7 -2
- package/src/charts/index.ts +1 -0
- package/src/forms/RadioButtonFormGroup.tsx +10 -3
- package/src/report/ChartDownloadButton.tsx +59 -0
- package/src/report/ChartExportProvider.tsx +384 -0
- package/src/report/ChartExportSurface.tsx +60 -0
- package/src/report/FeatureStatusBadge.tsx +30 -0
- package/src/report/HostPowerStates.tsx +38 -0
- package/src/report/InfrastructureSummary.tsx +73 -0
- package/src/report/OSDistribution.tsx +96 -99
- package/src/report/OsSupportTiersHelpPopover.tsx +32 -62
- package/src/report/OsUpgradeNotice.tsx +1 -7
- package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
- package/src/report/PowerStateCard.tsx +86 -0
- package/src/report/ReportExportMenu.tsx +119 -0
- package/src/report/SupportTierBadge.tsx +12 -38
- package/src/report/VCenterClusterDetails.tsx +233 -0
- package/src/report/VmPowerStates.tsx +38 -0
- package/src/report/__tests__/ChartExportProvider.test.tsx +474 -0
- package/src/report/__tests__/HostPowerStates.test.tsx +47 -0
- package/src/report/__tests__/InfrastructureSummary.test.tsx +126 -0
- package/src/report/__tests__/OSDistribution.test.tsx +10 -11
- package/src/report/__tests__/captureChartElement.test.ts +132 -0
- package/src/report/__tests__/chartExportPackagers.test.ts +224 -0
- package/src/report/__tests__/infrastructureSummaryModel.test.ts +224 -0
- package/src/report/__tests__/powerStates.test.ts +70 -0
- package/src/report/captureChartElement.ts +368 -0
- package/src/report/chartExport.ts +237 -0
- package/src/report/chartExportContext.tsx +84 -0
- package/src/report/chartExportFilenames.ts +21 -0
- package/src/report/constants.ts +2 -0
- package/src/report/dashboardStyles.ts +0 -23
- package/src/report/htmlExport.ts +83 -0
- package/src/report/index.ts +128 -0
- package/src/report/infrastructureSummaryModel.ts +281 -0
- package/src/report/pdfExport.ts +154 -0
- package/src/report/pdfPage.ts +83 -0
- package/src/report/powerStates.ts +169 -0
- package/src/report/reportExportOptions.ts +53 -0
- package/src/report/zipChartPngs.ts +10 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import html2canvas from "html2canvas-pro";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { captureChartElement } from "../captureChartElement.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("html2canvas-pro", () => ({
|
|
6
|
+
default: vi.fn(async () => {
|
|
7
|
+
const canvas = document.createElement("canvas");
|
|
8
|
+
canvas.width = 1;
|
|
9
|
+
canvas.height = 1;
|
|
10
|
+
return canvas;
|
|
11
|
+
}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockedHtml2Canvas = vi.mocked(html2canvas);
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
mockedHtml2Canvas.mockClear();
|
|
18
|
+
document.body.replaceChildren();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("captureChartElement", () => {
|
|
22
|
+
it("temporarily unhides ancestors so a hidden tab panel can be measured", async () => {
|
|
23
|
+
const panel = document.createElement("div");
|
|
24
|
+
panel.hidden = true;
|
|
25
|
+
const chart = document.createElement("div");
|
|
26
|
+
panel.append(chart);
|
|
27
|
+
document.body.append(panel);
|
|
28
|
+
|
|
29
|
+
mockedHtml2Canvas.mockImplementation(async () => {
|
|
30
|
+
expect(panel.hidden).toBe(false);
|
|
31
|
+
expect(panel.style.display).toBe("block");
|
|
32
|
+
const canvas = document.createElement("canvas");
|
|
33
|
+
canvas.width = 1;
|
|
34
|
+
canvas.height = 1;
|
|
35
|
+
return canvas;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await captureChartElement(chart);
|
|
39
|
+
|
|
40
|
+
expect(panel.hidden).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("temporarily unhides ancestors hidden with display none", async () => {
|
|
44
|
+
const panel = document.createElement("div");
|
|
45
|
+
panel.style.display = "none";
|
|
46
|
+
const chart = document.createElement("div");
|
|
47
|
+
panel.append(chart);
|
|
48
|
+
document.body.append(panel);
|
|
49
|
+
|
|
50
|
+
mockedHtml2Canvas.mockImplementation(async () => {
|
|
51
|
+
expect(panel.style.display).toBe("block");
|
|
52
|
+
const canvas = document.createElement("canvas");
|
|
53
|
+
canvas.width = 1;
|
|
54
|
+
canvas.height = 1;
|
|
55
|
+
return canvas;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await captureChartElement(chart);
|
|
59
|
+
|
|
60
|
+
expect(panel.style.display).toBe("none");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("paints a real card border and hides the PatternFly ::before during capture", async () => {
|
|
64
|
+
const root = document.createElement("div");
|
|
65
|
+
const card = document.createElement("div");
|
|
66
|
+
card.className = "pf-v6-c-card";
|
|
67
|
+
card.style.borderRadius = "16px";
|
|
68
|
+
card.style.backgroundColor = "rgb(255, 255, 255)";
|
|
69
|
+
card.style.border = "1px solid rgb(200, 200, 200)";
|
|
70
|
+
root.append(card);
|
|
71
|
+
document.body.append(root);
|
|
72
|
+
|
|
73
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
74
|
+
expect(card.style.borderWidth).toBe("1px");
|
|
75
|
+
expect(card.style.overflow).toBe("hidden");
|
|
76
|
+
expect(
|
|
77
|
+
document.head.querySelector("[data-chart-export-hide-before]"),
|
|
78
|
+
).not.toBeNull();
|
|
79
|
+
|
|
80
|
+
const cloneRoot = document.createElement("div");
|
|
81
|
+
const cloneCard = document.createElement("div");
|
|
82
|
+
cloneCard.className = "pf-v6-c-card";
|
|
83
|
+
cloneRoot.append(cloneCard);
|
|
84
|
+
options?.onclone?.(document, cloneRoot);
|
|
85
|
+
expect(cloneCard.style.borderWidth).toBe("1px");
|
|
86
|
+
expect(cloneCard.style.borderStyle).toBe("solid");
|
|
87
|
+
expect(cloneCard.style.overflow).toBe("hidden");
|
|
88
|
+
|
|
89
|
+
const canvas = document.createElement("canvas");
|
|
90
|
+
canvas.width = 1;
|
|
91
|
+
canvas.height = 1;
|
|
92
|
+
return canvas;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await captureChartElement(root);
|
|
96
|
+
|
|
97
|
+
expect(card.style.borderWidth).toBe("1px");
|
|
98
|
+
expect(card.style.overflow).toBe("");
|
|
99
|
+
expect(
|
|
100
|
+
document.head.querySelector("[data-chart-export-hide-before]"),
|
|
101
|
+
).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("inlines computed card title padding so content stays inset", async () => {
|
|
105
|
+
const root = document.createElement("div");
|
|
106
|
+
const title = document.createElement("div");
|
|
107
|
+
title.className = "pf-v6-c-card__title";
|
|
108
|
+
title.style.padding = "24px 24px 16px 24px";
|
|
109
|
+
root.append(title);
|
|
110
|
+
document.body.append(root);
|
|
111
|
+
|
|
112
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
113
|
+
expect(title.style.paddingTop).not.toBe("");
|
|
114
|
+
expect(title.style.paddingLeft).not.toBe("");
|
|
115
|
+
|
|
116
|
+
const cloneRoot = document.createElement("div");
|
|
117
|
+
const cloneTitle = document.createElement("div");
|
|
118
|
+
cloneTitle.className = "pf-v6-c-card__title";
|
|
119
|
+
cloneRoot.append(cloneTitle);
|
|
120
|
+
options?.onclone?.(document, cloneRoot);
|
|
121
|
+
expect(cloneTitle.style.paddingTop).not.toBe("");
|
|
122
|
+
expect(cloneTitle.style.paddingLeft).not.toBe("");
|
|
123
|
+
|
|
124
|
+
const canvas = document.createElement("canvas");
|
|
125
|
+
canvas.width = 1;
|
|
126
|
+
canvas.height = 1;
|
|
127
|
+
return canvas;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await captureChartElement(root);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
chartExportViewsFromLabels,
|
|
4
|
+
restoreChartExportViews,
|
|
5
|
+
sortRegisteredChartsByDocumentOrder,
|
|
6
|
+
toBulkChartCaptureSources,
|
|
7
|
+
} from "../chartExport.js";
|
|
8
|
+
import { getChartExportFilename } from "../chartExportFilenames.js";
|
|
9
|
+
import { buildHtmlReport, escapeHtml } from "../htmlExport.js";
|
|
10
|
+
import {
|
|
11
|
+
fitPdfImageSize,
|
|
12
|
+
placePdfBlock,
|
|
13
|
+
splitSegmentForPageHeight,
|
|
14
|
+
} from "../pdfPage.js";
|
|
15
|
+
|
|
16
|
+
describe("chart export filenames", () => {
|
|
17
|
+
const date = new Date("2026-07-01T15:30:00");
|
|
18
|
+
|
|
19
|
+
it("names pdf, html, and zip downloads with the export date", () => {
|
|
20
|
+
expect(getChartExportFilename("pdf", date)).toBe(
|
|
21
|
+
"migration-export-2026-07-01.pdf",
|
|
22
|
+
);
|
|
23
|
+
expect(getChartExportFilename("html", date)).toBe(
|
|
24
|
+
"migration-export-2026-07-01.html",
|
|
25
|
+
);
|
|
26
|
+
expect(getChartExportFilename("zip", date)).toBe(
|
|
27
|
+
"migration-export-2026-07-01-charts.zip",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("dropdown chart export views", () => {
|
|
33
|
+
it("builds a titled view list from card dropdown labels", () => {
|
|
34
|
+
expect(
|
|
35
|
+
chartExportViewsFromLabels("VM migration status", {
|
|
36
|
+
issuesVsNoIssues: "No issues vs with issues",
|
|
37
|
+
issuesBreakdown: "With issues breakdown",
|
|
38
|
+
}),
|
|
39
|
+
).toEqual([
|
|
40
|
+
{
|
|
41
|
+
id: "issuesVsNoIssues",
|
|
42
|
+
title: "VM migration status — No issues vs with issues",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "issuesBreakdown",
|
|
46
|
+
title: "VM migration status — With issues breakdown",
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("captures every dropdown view then restores the visible one", async () => {
|
|
52
|
+
const element = document.createElement("div");
|
|
53
|
+
const activated: string[] = [];
|
|
54
|
+
const charts = [
|
|
55
|
+
{
|
|
56
|
+
id: "vm",
|
|
57
|
+
title: "Visible",
|
|
58
|
+
filename: "visible.png",
|
|
59
|
+
element,
|
|
60
|
+
exportViews: [
|
|
61
|
+
{ id: "a", title: "View A" },
|
|
62
|
+
{ id: "b", title: "View B" },
|
|
63
|
+
],
|
|
64
|
+
activeExportViewId: "a",
|
|
65
|
+
setExportView: async (viewId: string) => {
|
|
66
|
+
activated.push(viewId);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const sources = toBulkChartCaptureSources(charts, async () => {
|
|
72
|
+
const canvas = document.createElement("canvas");
|
|
73
|
+
canvas.width = 1;
|
|
74
|
+
canvas.height = 1;
|
|
75
|
+
return canvas;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(sources.map((source) => source.title)).toEqual(["View A", "View B"]);
|
|
79
|
+
await sources[0].capture();
|
|
80
|
+
await sources[1].capture();
|
|
81
|
+
await restoreChartExportViews(charts);
|
|
82
|
+
|
|
83
|
+
expect(activated).toEqual(["a", "b", "a"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("lays out hidden cards before switching dropdown views", async () => {
|
|
87
|
+
const element = document.createElement("div");
|
|
88
|
+
const order: string[] = [];
|
|
89
|
+
const charts = [
|
|
90
|
+
{
|
|
91
|
+
id: "vm",
|
|
92
|
+
title: "Visible",
|
|
93
|
+
filename: "visible.png",
|
|
94
|
+
element,
|
|
95
|
+
exportViews: [
|
|
96
|
+
{ id: "a", title: "View A" },
|
|
97
|
+
{ id: "b", title: "View B" },
|
|
98
|
+
],
|
|
99
|
+
activeExportViewId: "a",
|
|
100
|
+
setExportView: async () => {
|
|
101
|
+
order.push("setView");
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const [source] = toBulkChartCaptureSources(
|
|
107
|
+
charts,
|
|
108
|
+
async () => {
|
|
109
|
+
order.push("capture");
|
|
110
|
+
const canvas = document.createElement("canvas");
|
|
111
|
+
canvas.width = 1;
|
|
112
|
+
canvas.height = 1;
|
|
113
|
+
return canvas;
|
|
114
|
+
},
|
|
115
|
+
() => {
|
|
116
|
+
order.push("prepare");
|
|
117
|
+
return () => {
|
|
118
|
+
order.push("restore");
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await source.capture();
|
|
124
|
+
expect(order).toEqual(["prepare", "setView", "capture", "restore"]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("sortRegisteredChartsByDocumentOrder", () => {
|
|
129
|
+
it("orders registered nodes by their position in the document", () => {
|
|
130
|
+
const root = document.createElement("div");
|
|
131
|
+
const first = document.createElement("div");
|
|
132
|
+
const second = document.createElement("div");
|
|
133
|
+
root.append(first, second);
|
|
134
|
+
document.body.append(root);
|
|
135
|
+
|
|
136
|
+
const sorted = sortRegisteredChartsByDocumentOrder([
|
|
137
|
+
{
|
|
138
|
+
id: "second",
|
|
139
|
+
title: "Second",
|
|
140
|
+
filename: "second.png",
|
|
141
|
+
element: second,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "first",
|
|
145
|
+
title: "First",
|
|
146
|
+
filename: "first.png",
|
|
147
|
+
element: first,
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
expect(sorted.map((chart) => chart.id)).toEqual(["first", "second"]);
|
|
152
|
+
root.remove();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("splitSegmentForPageHeight", () => {
|
|
157
|
+
it("splits a tall chart across page height", () => {
|
|
158
|
+
expect(
|
|
159
|
+
splitSegmentForPageHeight({ top: 0, height: 250 }, 250, 100),
|
|
160
|
+
).toEqual([
|
|
161
|
+
{ top: 0, height: 100 },
|
|
162
|
+
{ top: 100, height: 100 },
|
|
163
|
+
{ top: 200, height: 50 },
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("fitPdfImageSize", () => {
|
|
169
|
+
it("keeps full content width when the chart fits the page", () => {
|
|
170
|
+
expect(fitPdfImageSize(800, 200, 190, 277)).toEqual({
|
|
171
|
+
widthMm: 190,
|
|
172
|
+
heightMm: 47.5,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("scales down a chart taller than the page", () => {
|
|
177
|
+
expect(fitPdfImageSize(1000, 2000, 200, 100)).toEqual({
|
|
178
|
+
widthMm: 50,
|
|
179
|
+
heightMm: 100,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("placePdfBlock", () => {
|
|
185
|
+
it("stacks charts on the same page while they fit", () => {
|
|
186
|
+
expect(placePdfBlock(null, 50, 10, 287)).toEqual({
|
|
187
|
+
y: 10,
|
|
188
|
+
needsNewPage: true,
|
|
189
|
+
});
|
|
190
|
+
expect(placePdfBlock(68, 50, 10, 287)).toEqual({
|
|
191
|
+
y: 68,
|
|
192
|
+
needsNewPage: false,
|
|
193
|
+
});
|
|
194
|
+
expect(placePdfBlock(250, 50, 10, 287)).toEqual({
|
|
195
|
+
y: 10,
|
|
196
|
+
needsNewPage: true,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("buildHtmlReport", () => {
|
|
202
|
+
it("embeds captured chart images and escapes the title", () => {
|
|
203
|
+
const html = buildHtmlReport(
|
|
204
|
+
'Report <script>alert("x")</script>',
|
|
205
|
+
[
|
|
206
|
+
{
|
|
207
|
+
name: "Infrastructure summary",
|
|
208
|
+
dataUrl: "data:image/png;base64,abc",
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
new Date("2026-07-01T15:30:00"),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(html).toContain(
|
|
215
|
+
"Report <script>alert("x")</script>",
|
|
216
|
+
);
|
|
217
|
+
expect(html).toContain('src="data:image/png;base64,abc"');
|
|
218
|
+
expect(html).toContain("Infrastructure summary");
|
|
219
|
+
expect(html).not.toContain("border:");
|
|
220
|
+
expect(html).toContain("width: 100%");
|
|
221
|
+
expect(escapeHtml("<b>")).toBe("<b>");
|
|
222
|
+
expect(escapeHtml("it's")).toBe("it's");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
booleanToFeatureStatus,
|
|
4
|
+
buildClusterDetailRows,
|
|
5
|
+
buildClusterDetails,
|
|
6
|
+
buildInfrastructureSummary,
|
|
7
|
+
countVCenters,
|
|
8
|
+
formatVSphereVersion,
|
|
9
|
+
hostCapabilityStatus,
|
|
10
|
+
type InventoryCluster,
|
|
11
|
+
type InventoryHost,
|
|
12
|
+
type InventoryInfra,
|
|
13
|
+
visibleNetworks,
|
|
14
|
+
vsanStatus,
|
|
15
|
+
} from "../infrastructureSummaryModel.js";
|
|
16
|
+
|
|
17
|
+
const createInfra = (
|
|
18
|
+
overrides: Partial<InventoryInfra> = {},
|
|
19
|
+
): InventoryInfra => ({
|
|
20
|
+
totalHosts: 0,
|
|
21
|
+
networks: [],
|
|
22
|
+
datastores: [],
|
|
23
|
+
...overrides,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("formatVSphereVersion", () => {
|
|
27
|
+
it("formats API versions as vSphere labels and drops trailing build zeros", () => {
|
|
28
|
+
expect(formatVSphereVersion("7.0.3.0")).toBe("vSphere 7.0.3");
|
|
29
|
+
expect(formatVSphereVersion("8.0.3.0")).toBe("vSphere 8.0.3");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps an existing vSphere prefix and missing values", () => {
|
|
33
|
+
expect(formatVSphereVersion("vSphere 7.0.3")).toBe("vSphere 7.0.3");
|
|
34
|
+
expect(formatVSphereVersion(undefined)).toBe("—");
|
|
35
|
+
expect(formatVSphereVersion("")).toBe("—");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("feature status helpers", () => {
|
|
40
|
+
it("maps booleans to enabled, disabled, or unknown", () => {
|
|
41
|
+
expect(booleanToFeatureStatus(true)).toBe("enabled");
|
|
42
|
+
expect(booleanToFeatureStatus(false)).toBe("disabled");
|
|
43
|
+
expect(booleanToFeatureStatus(undefined)).toBe("unknown");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("treats vMotion as enabled when any host supports it", () => {
|
|
47
|
+
const hosts: InventoryHost[] = [
|
|
48
|
+
{ vmotionSupported: false },
|
|
49
|
+
{ vmotionSupported: true },
|
|
50
|
+
];
|
|
51
|
+
expect(hostCapabilityStatus(hosts, "vmotionSupported")).toBe("enabled");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("treats vMotion as disabled when every known host reports false", () => {
|
|
55
|
+
const hosts: InventoryHost[] = [
|
|
56
|
+
{ vmotionSupported: false },
|
|
57
|
+
{ vmotionSupported: false },
|
|
58
|
+
];
|
|
59
|
+
expect(hostCapabilityStatus(hosts, "vmotionSupported")).toBe("disabled");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("treats missing host capability flags as unknown", () => {
|
|
63
|
+
expect(hostCapabilityStatus([{}], "vmotionSupported")).toBe("unknown");
|
|
64
|
+
expect(hostCapabilityStatus([], "vmotionSupported")).toBe("unknown");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("detects vSAN from datastore type or disk type", () => {
|
|
68
|
+
expect(
|
|
69
|
+
vsanStatus(
|
|
70
|
+
createInfra({
|
|
71
|
+
datastores: [{ type: "vSAN" }],
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
).toBe("enabled");
|
|
75
|
+
expect(
|
|
76
|
+
vsanStatus(createInfra(), { total: 10, diskTypes: { vSAN: {} } }),
|
|
77
|
+
).toBe("enabled");
|
|
78
|
+
expect(vsanStatus(createInfra({ datastores: [] }))).toBe("disabled");
|
|
79
|
+
expect(vsanStatus()).toBe("unknown");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("buildInfrastructureSummary", () => {
|
|
84
|
+
it("maps inventory metadata into the summary card model", () => {
|
|
85
|
+
const summary = buildInfrastructureSummary({
|
|
86
|
+
infra: createInfra({
|
|
87
|
+
totalHosts: 12,
|
|
88
|
+
totalDatacenters: 1,
|
|
89
|
+
}),
|
|
90
|
+
vcenterVersion: "7.0.3.0",
|
|
91
|
+
vcenterId: "vc-1",
|
|
92
|
+
clusters: {},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(summary).toEqual({
|
|
96
|
+
vmwareVersion: "vSphere 7.0.3",
|
|
97
|
+
datacenters: 1,
|
|
98
|
+
vCenters: 1,
|
|
99
|
+
esxiHosts: 12,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("falls back to clustersPerDatacenter length and unique vCenter ids", () => {
|
|
104
|
+
const summary = buildInfrastructureSummary({
|
|
105
|
+
infra: createInfra({
|
|
106
|
+
totalHosts: 7,
|
|
107
|
+
clustersPerDatacenter: [1, 1],
|
|
108
|
+
}),
|
|
109
|
+
clusters: {
|
|
110
|
+
A: {
|
|
111
|
+
infra: createInfra(),
|
|
112
|
+
vms: { total: 1 },
|
|
113
|
+
vcenter: { id: "vc-a" },
|
|
114
|
+
},
|
|
115
|
+
B: {
|
|
116
|
+
infra: createInfra(),
|
|
117
|
+
vms: { total: 1 },
|
|
118
|
+
vcenter: { id: "vc-b" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(summary.datacenters).toBe(2);
|
|
124
|
+
expect(summary.vCenters).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("countVCenters", () => {
|
|
129
|
+
it("returns undefined when no vCenter identity is present", () => {
|
|
130
|
+
expect(countVCenters(undefined, undefined)).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("cluster details", () => {
|
|
135
|
+
it("builds sorted aggregate rows from cluster features", () => {
|
|
136
|
+
const clusters: Record<string, InventoryCluster> = {
|
|
137
|
+
"Cluster-Dev-02": {
|
|
138
|
+
infra: createInfra({
|
|
139
|
+
totalHosts: 7,
|
|
140
|
+
hosts: [{ vmotionSupported: true }],
|
|
141
|
+
datastores: [],
|
|
142
|
+
}),
|
|
143
|
+
vms: { total: 350 },
|
|
144
|
+
clusterFeatures: { drsEnabled: false, haEnabled: true },
|
|
145
|
+
},
|
|
146
|
+
"Cluster-Prod-01": {
|
|
147
|
+
infra: createInfra({
|
|
148
|
+
totalHosts: 5,
|
|
149
|
+
hosts: [{ vmotionSupported: true }],
|
|
150
|
+
datastores: [{ type: "vSAN" }],
|
|
151
|
+
}),
|
|
152
|
+
vms: { total: 280 },
|
|
153
|
+
clusterFeatures: { drsEnabled: true, haEnabled: true },
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const rows = buildClusterDetailRows(clusters);
|
|
158
|
+
expect(rows.map((row) => row.name)).toEqual([
|
|
159
|
+
"Cluster-Dev-02",
|
|
160
|
+
"Cluster-Prod-01",
|
|
161
|
+
]);
|
|
162
|
+
expect(rows[0]).toMatchObject({
|
|
163
|
+
hosts: 7,
|
|
164
|
+
vms: 350,
|
|
165
|
+
vmotion: "enabled",
|
|
166
|
+
drs: "disabled",
|
|
167
|
+
vsan: "disabled",
|
|
168
|
+
});
|
|
169
|
+
expect(rows[1]).toMatchObject({
|
|
170
|
+
hosts: 5,
|
|
171
|
+
vms: 280,
|
|
172
|
+
drs: "enabled",
|
|
173
|
+
vsan: "enabled",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("builds the detailed cluster model including HA and network labels", () => {
|
|
178
|
+
const details = buildClusterDetails({
|
|
179
|
+
infra: createInfra({
|
|
180
|
+
totalHosts: 7,
|
|
181
|
+
hosts: [{ vmotionSupported: true }],
|
|
182
|
+
networks: [
|
|
183
|
+
{ type: "distributed", name: "VDS-Dev", vlanId: "10" },
|
|
184
|
+
{ type: "distributed", name: "VDS-Dev", vlanId: "11" },
|
|
185
|
+
{ type: "dvswitch", name: "vDSwitch0" },
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
vms: { total: 350 },
|
|
189
|
+
clusterFeatures: { drsEnabled: false, haEnabled: true },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(details).toMatchObject({
|
|
193
|
+
hosts: 7,
|
|
194
|
+
vms: 350,
|
|
195
|
+
networksDetected: 2,
|
|
196
|
+
vmotion: "enabled",
|
|
197
|
+
drs: "disabled",
|
|
198
|
+
ha: "enabled",
|
|
199
|
+
vsan: "disabled",
|
|
200
|
+
});
|
|
201
|
+
expect(details?.networks.map((network) => network.displayName)).toEqual([
|
|
202
|
+
"VDS-Dev (VLAN 10)",
|
|
203
|
+
"VDS-Dev (VLAN 11)",
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("visibleNetworks", () => {
|
|
209
|
+
it("omits unnamed networks and dvswitches", () => {
|
|
210
|
+
expect(
|
|
211
|
+
visibleNetworks(
|
|
212
|
+
createInfra({
|
|
213
|
+
networks: [
|
|
214
|
+
{ type: "distributed", name: " " },
|
|
215
|
+
{ type: "dvswitch", name: "switch" },
|
|
216
|
+
{ type: "standard", name: "VM Network" },
|
|
217
|
+
],
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
).toEqual([
|
|
221
|
+
{ name: "VM Network", vlanId: undefined, displayName: "VM Network" },
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildHostPowerStateChart,
|
|
4
|
+
buildVmPowerStateChart,
|
|
5
|
+
} from "../powerStates.js";
|
|
6
|
+
|
|
7
|
+
describe("buildHostPowerStateChart", () => {
|
|
8
|
+
it("maps overallStatus green to powered on and keeps zero categories", () => {
|
|
9
|
+
const chart = buildHostPowerStateChart({ green: 7 });
|
|
10
|
+
|
|
11
|
+
expect(chart.total).toBe(7);
|
|
12
|
+
expect(chart.slices.map((slice) => [slice.name, slice.count])).toEqual([
|
|
13
|
+
["Powered on", 7],
|
|
14
|
+
["Powered off", 0],
|
|
15
|
+
["Standby", 0],
|
|
16
|
+
["Not responding", 0],
|
|
17
|
+
["Maintenance", 0],
|
|
18
|
+
]);
|
|
19
|
+
expect(chart.slices[0].countDisplay).toBe("7 hosts");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("maps explicit host power and maintenance keys", () => {
|
|
23
|
+
const chart = buildHostPowerStateChart({
|
|
24
|
+
poweredOn: 5,
|
|
25
|
+
poweredOff: 1,
|
|
26
|
+
standBy: 1,
|
|
27
|
+
notResponding: 1,
|
|
28
|
+
inMaintenanceMode: 2,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(chart.total).toBe(10);
|
|
32
|
+
expect(
|
|
33
|
+
chart.slices.find((slice) => slice.name === "Maintenance")?.count,
|
|
34
|
+
).toBe(2);
|
|
35
|
+
expect(
|
|
36
|
+
chart.slices.find((slice) => slice.name === "Not responding")?.count,
|
|
37
|
+
).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("buildVmPowerStateChart", () => {
|
|
42
|
+
it("maps VM power states and omits unused suspended", () => {
|
|
43
|
+
const chart = buildVmPowerStateChart({
|
|
44
|
+
poweredOff: 297,
|
|
45
|
+
poweredOn: 53,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(chart.total).toBe(350);
|
|
49
|
+
expect(chart.slices.map((slice) => [slice.name, slice.count])).toEqual([
|
|
50
|
+
["Powered off", 297],
|
|
51
|
+
["Powered on", 53],
|
|
52
|
+
]);
|
|
53
|
+
expect(chart.slices[0].countDisplay).toBe("297 VMs");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("includes suspended when present", () => {
|
|
57
|
+
const chart = buildVmPowerStateChart({
|
|
58
|
+
poweredOn: 10,
|
|
59
|
+
poweredOff: 2,
|
|
60
|
+
suspended: 1,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(chart.slices.map((slice) => slice.name)).toEqual([
|
|
64
|
+
"Powered off",
|
|
65
|
+
"Powered on",
|
|
66
|
+
"Suspended",
|
|
67
|
+
]);
|
|
68
|
+
expect(chart.slices[2].countDisplay).toBe("1 VM");
|
|
69
|
+
});
|
|
70
|
+
});
|