@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.
- package/README.md +25 -0
- 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 +23 -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/CpuAndMemoryOverview.d.ts +19 -0
- package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
- package/dist/report/CpuAndMemoryOverview.js +112 -0
- package/dist/report/ErrorTable.d.ts +18 -0
- package/dist/report/ErrorTable.d.ts.map +1 -0
- package/dist/report/ErrorTable.js +20 -0
- package/dist/report/HostPowerStates.d.ts +0 -1
- package/dist/report/HostPowerStates.d.ts.map +1 -1
- package/dist/report/HostPowerStates.js +2 -2
- package/dist/report/HostsOverview.d.ts +13 -0
- package/dist/report/HostsOverview.d.ts.map +1 -0
- package/dist/report/HostsOverview.js +84 -0
- package/dist/report/InfrastructureSummary.d.ts.map +1 -1
- package/dist/report/InfrastructureSummary.js +6 -2
- package/dist/report/NetworkOverview.d.ts +31 -0
- package/dist/report/NetworkOverview.d.ts.map +1 -0
- package/dist/report/NetworkOverview.js +201 -0
- package/dist/report/OSDistribution.d.ts +0 -2
- package/dist/report/OSDistribution.d.ts.map +1 -1
- package/dist/report/OSDistribution.js +8 -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 +0 -1
- package/dist/report/PowerStateCard.d.ts.map +1 -1
- package/dist/report/PowerStateCard.js +4 -2
- 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/ReportTable.d.ts +14 -0
- package/dist/report/ReportTable.d.ts.map +1 -0
- package/dist/report/ReportTable.js +23 -0
- package/dist/report/SupportTierBadge.d.ts +0 -1
- package/dist/report/SupportTierBadge.d.ts.map +1 -1
- package/dist/report/SupportTierBadge.js +1 -21
- package/dist/report/VCenterClusterDetails.d.ts +0 -1
- package/dist/report/VCenterClusterDetails.d.ts.map +1 -1
- package/dist/report/VCenterClusterDetails.js +9 -4
- package/dist/report/VmPowerStates.d.ts +0 -1
- package/dist/report/VmPowerStates.d.ts.map +1 -1
- package/dist/report/VmPowerStates.js +2 -2
- package/dist/report/WarningsTable.d.ts +9 -0
- package/dist/report/WarningsTable.d.ts.map +1 -0
- package/dist/report/WarningsTable.js +20 -0
- package/dist/report/captureChartElement.d.ts +8 -0
- package/dist/report/captureChartElement.d.ts.map +1 -0
- package/dist/report/captureChartElement.js +304 -0
- package/dist/report/chartExport.d.ts +80 -0
- package/dist/report/chartExport.d.ts.map +1 -0
- package/dist/report/chartExport.js +182 -0
- package/dist/report/chartExportContext.d.ts +32 -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 +19 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +19 -0
- package/dist/report/pdfExport.d.ts +3 -0
- package/dist/report/pdfExport.d.ts.map +1 -0
- package/dist/report/pdfExport.js +131 -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/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/report/ChartDownloadButton.tsx +59 -0
- package/src/report/ChartExportProvider.tsx +392 -0
- package/src/report/ChartExportSurface.tsx +60 -0
- package/src/report/CpuAndMemoryOverview.tsx +277 -0
- package/src/report/ErrorTable.tsx +85 -0
- package/src/report/HostPowerStates.tsx +0 -3
- package/src/report/HostsOverview.tsx +185 -0
- package/src/report/InfrastructureSummary.tsx +48 -31
- package/src/report/NetworkOverview.tsx +405 -0
- package/src/report/OSDistribution.tsx +46 -42
- package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
- package/src/report/PowerStateCard.tsx +49 -36
- package/src/report/ReportExportMenu.tsx +119 -0
- package/src/report/ReportTable.tsx +81 -0
- package/src/report/SupportTierBadge.tsx +12 -40
- package/src/report/VCenterClusterDetails.tsx +38 -16
- package/src/report/VmPowerStates.tsx +0 -3
- package/src/report/WarningsTable.tsx +75 -0
- package/src/report/__tests__/ChartExportProvider.test.tsx +582 -0
- package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
- package/src/report/__tests__/HostsOverview.test.tsx +145 -0
- package/src/report/__tests__/InfrastructureSummary.test.tsx +0 -22
- package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
- package/src/report/__tests__/OSDistribution.test.tsx +10 -11
- package/src/report/__tests__/captureChartElement.test.ts +205 -0
- package/src/report/__tests__/chartExportPackagers.test.ts +299 -0
- package/src/report/captureChartElement.ts +398 -0
- package/src/report/chartExport.ts +277 -0
- package/src/report/chartExportContext.tsx +92 -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 +102 -0
- package/src/report/pdfExport.ts +233 -0
- package/src/report/pdfPage.ts +83 -0
- package/src/report/reportExportOptions.ts +53 -0
- package/src/report/zipChartPngs.ts +10 -0
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
document.documentElement.classList.remove(
|
|
20
|
+
"pf-v6-theme-dark",
|
|
21
|
+
"pf-v5-theme-dark",
|
|
22
|
+
);
|
|
23
|
+
document.documentElement.style.colorScheme = "";
|
|
24
|
+
document.body.style.colorScheme = "";
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("captureChartElement", () => {
|
|
28
|
+
it("temporarily unhides ancestors so a hidden tab panel can be measured", async () => {
|
|
29
|
+
const panel = document.createElement("div");
|
|
30
|
+
panel.hidden = true;
|
|
31
|
+
const chart = document.createElement("div");
|
|
32
|
+
panel.append(chart);
|
|
33
|
+
document.body.append(panel);
|
|
34
|
+
|
|
35
|
+
mockedHtml2Canvas.mockImplementation(async () => {
|
|
36
|
+
expect(panel.hidden).toBe(false);
|
|
37
|
+
expect(panel.style.display).toBe("block");
|
|
38
|
+
const canvas = document.createElement("canvas");
|
|
39
|
+
canvas.width = 1;
|
|
40
|
+
canvas.height = 1;
|
|
41
|
+
return canvas;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await captureChartElement(chart);
|
|
45
|
+
|
|
46
|
+
expect(panel.hidden).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("temporarily unhides ancestors hidden with display none", async () => {
|
|
50
|
+
const panel = document.createElement("div");
|
|
51
|
+
panel.style.display = "none";
|
|
52
|
+
const chart = document.createElement("div");
|
|
53
|
+
panel.append(chart);
|
|
54
|
+
document.body.append(panel);
|
|
55
|
+
|
|
56
|
+
mockedHtml2Canvas.mockImplementation(async () => {
|
|
57
|
+
expect(panel.style.display).toBe("block");
|
|
58
|
+
const canvas = document.createElement("canvas");
|
|
59
|
+
canvas.width = 1;
|
|
60
|
+
canvas.height = 1;
|
|
61
|
+
return canvas;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await captureChartElement(chart);
|
|
65
|
+
|
|
66
|
+
expect(panel.style.display).toBe("none");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("paints a real card border and hides the PatternFly ::before during capture", async () => {
|
|
70
|
+
const root = document.createElement("div");
|
|
71
|
+
const card = document.createElement("div");
|
|
72
|
+
card.className = "pf-v6-c-card";
|
|
73
|
+
card.style.borderRadius = "16px";
|
|
74
|
+
card.style.backgroundColor = "rgb(255, 255, 255)";
|
|
75
|
+
card.style.border = "1px solid rgb(200, 200, 200)";
|
|
76
|
+
root.append(card);
|
|
77
|
+
document.body.append(root);
|
|
78
|
+
|
|
79
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
80
|
+
expect(card.style.borderWidth).toBe("1px");
|
|
81
|
+
expect(card.style.overflow).toBe("hidden");
|
|
82
|
+
expect(
|
|
83
|
+
document.head.querySelector("[data-chart-export-hide-before]"),
|
|
84
|
+
).not.toBeNull();
|
|
85
|
+
|
|
86
|
+
const cloneRoot = document.createElement("div");
|
|
87
|
+
const cloneCard = document.createElement("div");
|
|
88
|
+
cloneCard.className = "pf-v6-c-card";
|
|
89
|
+
cloneRoot.append(cloneCard);
|
|
90
|
+
options?.onclone?.(document, cloneRoot);
|
|
91
|
+
expect(cloneCard.style.borderWidth).toBe("1px");
|
|
92
|
+
expect(cloneCard.style.borderStyle).toBe("solid");
|
|
93
|
+
expect(cloneCard.style.overflow).toBe("hidden");
|
|
94
|
+
|
|
95
|
+
const canvas = document.createElement("canvas");
|
|
96
|
+
canvas.width = 1;
|
|
97
|
+
canvas.height = 1;
|
|
98
|
+
return canvas;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await captureChartElement(root);
|
|
102
|
+
|
|
103
|
+
expect(card.style.borderWidth).toBe("1px");
|
|
104
|
+
expect(card.style.overflow).toBe("");
|
|
105
|
+
expect(
|
|
106
|
+
document.head.querySelector("[data-chart-export-hide-before]"),
|
|
107
|
+
).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("inlines computed card title padding so content stays inset", async () => {
|
|
111
|
+
const root = document.createElement("div");
|
|
112
|
+
const title = document.createElement("div");
|
|
113
|
+
title.className = "pf-v6-c-card__title";
|
|
114
|
+
title.style.padding = "24px 24px 16px 24px";
|
|
115
|
+
root.append(title);
|
|
116
|
+
document.body.append(root);
|
|
117
|
+
|
|
118
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
119
|
+
expect(title.style.paddingTop).not.toBe("");
|
|
120
|
+
expect(title.style.paddingLeft).not.toBe("");
|
|
121
|
+
|
|
122
|
+
const cloneRoot = document.createElement("div");
|
|
123
|
+
const cloneTitle = document.createElement("div");
|
|
124
|
+
cloneTitle.className = "pf-v6-c-card__title";
|
|
125
|
+
cloneRoot.append(cloneTitle);
|
|
126
|
+
options?.onclone?.(document, cloneRoot);
|
|
127
|
+
expect(cloneTitle.style.paddingTop).not.toBe("");
|
|
128
|
+
expect(cloneTitle.style.paddingLeft).not.toBe("");
|
|
129
|
+
|
|
130
|
+
const canvas = document.createElement("canvas");
|
|
131
|
+
canvas.width = 1;
|
|
132
|
+
canvas.height = 1;
|
|
133
|
+
return canvas;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await captureChartElement(root);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("applies light theme on the clone and leaves the live document dark", async () => {
|
|
140
|
+
document.documentElement.classList.add("pf-v6-theme-dark");
|
|
141
|
+
const root = document.createElement("div");
|
|
142
|
+
const card = document.createElement("div");
|
|
143
|
+
card.className = "pf-v6-c-card";
|
|
144
|
+
card.style.backgroundColor = "rgb(30, 30, 30)";
|
|
145
|
+
card.style.border = "1px solid rgb(80, 80, 80)";
|
|
146
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
147
|
+
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
148
|
+
text.style.fill = "rgb(255, 255, 255)";
|
|
149
|
+
text.textContent = "42";
|
|
150
|
+
svg.append(text);
|
|
151
|
+
card.append(svg);
|
|
152
|
+
root.append(card);
|
|
153
|
+
document.body.append(root);
|
|
154
|
+
|
|
155
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
156
|
+
expect(
|
|
157
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
|
|
160
|
+
const clonedDoc = document.implementation.createHTMLDocument("export");
|
|
161
|
+
clonedDoc.documentElement.classList.add("pf-v6-theme-dark");
|
|
162
|
+
const cloneRoot = clonedDoc.createElement("div");
|
|
163
|
+
const cloneCard = clonedDoc.createElement("div");
|
|
164
|
+
cloneCard.className = "pf-v6-c-card";
|
|
165
|
+
cloneCard.style.backgroundColor = "rgb(30, 30, 30)";
|
|
166
|
+
const cloneSvg = clonedDoc.createElementNS(
|
|
167
|
+
"http://www.w3.org/2000/svg",
|
|
168
|
+
"svg",
|
|
169
|
+
);
|
|
170
|
+
const cloneText = clonedDoc.createElementNS(
|
|
171
|
+
"http://www.w3.org/2000/svg",
|
|
172
|
+
"text",
|
|
173
|
+
);
|
|
174
|
+
cloneText.style.fill = "rgb(255, 255, 255)";
|
|
175
|
+
cloneSvg.append(cloneText);
|
|
176
|
+
cloneCard.append(cloneSvg);
|
|
177
|
+
cloneRoot.append(cloneCard);
|
|
178
|
+
clonedDoc.body.append(cloneRoot);
|
|
179
|
+
|
|
180
|
+
await options?.onclone?.(clonedDoc, cloneRoot);
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
clonedDoc.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
expect(
|
|
186
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
expect(cloneCard.style.backgroundColor).toBe("rgb(255, 255, 255)");
|
|
189
|
+
expect(cloneCard.style.borderColor).toBe("rgb(224, 224, 224)");
|
|
190
|
+
expect(cloneText.style.fill).toBe("rgb(21, 21, 21)");
|
|
191
|
+
|
|
192
|
+
const canvas = document.createElement("canvas");
|
|
193
|
+
canvas.width = 1;
|
|
194
|
+
canvas.height = 1;
|
|
195
|
+
return canvas;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await captureChartElement(root);
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
202
|
+
).toBe(true);
|
|
203
|
+
expect(card.style.backgroundColor).toBe("rgb(30, 30, 30)");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyLightThemeToDocument,
|
|
4
|
+
chartExportViewsFromLabels,
|
|
5
|
+
restoreChartExportViews,
|
|
6
|
+
sortRegisteredChartsByDocumentOrder,
|
|
7
|
+
toBulkChartCaptureSources,
|
|
8
|
+
} from "../chartExport.js";
|
|
9
|
+
import { getChartExportFilename } from "../chartExportFilenames.js";
|
|
10
|
+
import { buildHtmlReport, escapeHtml } from "../htmlExport.js";
|
|
11
|
+
import { buildPdfFromCharts } from "../pdfExport.js";
|
|
12
|
+
import {
|
|
13
|
+
fitPdfImageSize,
|
|
14
|
+
placePdfBlock,
|
|
15
|
+
splitSegmentForPageHeight,
|
|
16
|
+
} from "../pdfPage.js";
|
|
17
|
+
|
|
18
|
+
describe("chart export filenames", () => {
|
|
19
|
+
const date = new Date("2026-07-01T15:30:00");
|
|
20
|
+
|
|
21
|
+
it("names pdf, html, and zip downloads with the export date", () => {
|
|
22
|
+
expect(getChartExportFilename("pdf", date)).toBe(
|
|
23
|
+
"migration-export-2026-07-01.pdf",
|
|
24
|
+
);
|
|
25
|
+
expect(getChartExportFilename("html", date)).toBe(
|
|
26
|
+
"migration-export-2026-07-01.html",
|
|
27
|
+
);
|
|
28
|
+
expect(getChartExportFilename("zip", date)).toBe(
|
|
29
|
+
"migration-export-2026-07-01-charts.zip",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("dropdown chart export views", () => {
|
|
35
|
+
it("builds a titled view list from card dropdown labels", () => {
|
|
36
|
+
expect(
|
|
37
|
+
chartExportViewsFromLabels("VM migration status", {
|
|
38
|
+
issuesVsNoIssues: "No issues vs with issues",
|
|
39
|
+
issuesBreakdown: "With issues breakdown",
|
|
40
|
+
}),
|
|
41
|
+
).toEqual([
|
|
42
|
+
{
|
|
43
|
+
id: "issuesVsNoIssues",
|
|
44
|
+
title: "VM migration status — No issues vs with issues",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "issuesBreakdown",
|
|
48
|
+
title: "VM migration status — With issues breakdown",
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("captures every dropdown view then restores the visible one", async () => {
|
|
54
|
+
const element = document.createElement("div");
|
|
55
|
+
const activated: string[] = [];
|
|
56
|
+
const charts = [
|
|
57
|
+
{
|
|
58
|
+
id: "vm",
|
|
59
|
+
title: "Visible",
|
|
60
|
+
filename: "visible.png",
|
|
61
|
+
element,
|
|
62
|
+
exportViews: [
|
|
63
|
+
{ id: "a", title: "View A" },
|
|
64
|
+
{ id: "b", title: "View B" },
|
|
65
|
+
],
|
|
66
|
+
activeExportViewId: "a",
|
|
67
|
+
setExportView: async (viewId: string) => {
|
|
68
|
+
activated.push(viewId);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const sources = toBulkChartCaptureSources(charts, async () => {
|
|
74
|
+
const canvas = document.createElement("canvas");
|
|
75
|
+
canvas.width = 1;
|
|
76
|
+
canvas.height = 1;
|
|
77
|
+
return canvas;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(sources.map((source) => source.title)).toEqual(["View A", "View B"]);
|
|
81
|
+
await sources[0].capture();
|
|
82
|
+
await sources[1].capture();
|
|
83
|
+
await restoreChartExportViews(charts);
|
|
84
|
+
|
|
85
|
+
expect(activated).toEqual(["a", "b", "a"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("lays out hidden cards before switching dropdown views", async () => {
|
|
89
|
+
const element = document.createElement("div");
|
|
90
|
+
const order: string[] = [];
|
|
91
|
+
const charts = [
|
|
92
|
+
{
|
|
93
|
+
id: "vm",
|
|
94
|
+
title: "Visible",
|
|
95
|
+
filename: "visible.png",
|
|
96
|
+
element,
|
|
97
|
+
exportViews: [
|
|
98
|
+
{ id: "a", title: "View A" },
|
|
99
|
+
{ id: "b", title: "View B" },
|
|
100
|
+
],
|
|
101
|
+
activeExportViewId: "a",
|
|
102
|
+
setExportView: async () => {
|
|
103
|
+
order.push("setView");
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const [source] = toBulkChartCaptureSources(
|
|
109
|
+
charts,
|
|
110
|
+
async () => {
|
|
111
|
+
order.push("capture");
|
|
112
|
+
const canvas = document.createElement("canvas");
|
|
113
|
+
canvas.width = 1;
|
|
114
|
+
canvas.height = 1;
|
|
115
|
+
return canvas;
|
|
116
|
+
},
|
|
117
|
+
() => {
|
|
118
|
+
order.push("prepare");
|
|
119
|
+
return () => {
|
|
120
|
+
order.push("restore");
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await source.capture();
|
|
126
|
+
expect(order).toEqual(["prepare", "setView", "capture", "restore"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("sortRegisteredChartsByDocumentOrder", () => {
|
|
131
|
+
it("orders registered nodes by their position in the document", () => {
|
|
132
|
+
const root = document.createElement("div");
|
|
133
|
+
const first = document.createElement("div");
|
|
134
|
+
const second = document.createElement("div");
|
|
135
|
+
root.append(first, second);
|
|
136
|
+
document.body.append(root);
|
|
137
|
+
|
|
138
|
+
const sorted = sortRegisteredChartsByDocumentOrder([
|
|
139
|
+
{
|
|
140
|
+
id: "second",
|
|
141
|
+
title: "Second",
|
|
142
|
+
filename: "second.png",
|
|
143
|
+
element: second,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "first",
|
|
147
|
+
title: "First",
|
|
148
|
+
filename: "first.png",
|
|
149
|
+
element: first,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
expect(sorted.map((chart) => chart.id)).toEqual(["first", "second"]);
|
|
154
|
+
root.remove();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("splitSegmentForPageHeight", () => {
|
|
159
|
+
it("splits a tall chart across page height", () => {
|
|
160
|
+
expect(
|
|
161
|
+
splitSegmentForPageHeight({ top: 0, height: 250 }, 250, 100),
|
|
162
|
+
).toEqual([
|
|
163
|
+
{ top: 0, height: 100 },
|
|
164
|
+
{ top: 100, height: 100 },
|
|
165
|
+
{ top: 200, height: 50 },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("fitPdfImageSize", () => {
|
|
171
|
+
it("keeps full content width when the chart fits the page", () => {
|
|
172
|
+
expect(fitPdfImageSize(800, 200, 190, 277)).toEqual({
|
|
173
|
+
widthMm: 190,
|
|
174
|
+
heightMm: 47.5,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("scales down a chart taller than the page", () => {
|
|
179
|
+
expect(fitPdfImageSize(1000, 2000, 200, 100)).toEqual({
|
|
180
|
+
widthMm: 50,
|
|
181
|
+
heightMm: 100,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("placePdfBlock", () => {
|
|
187
|
+
it("stacks charts on the same page while they fit", () => {
|
|
188
|
+
expect(placePdfBlock(null, 50, 10, 287)).toEqual({
|
|
189
|
+
y: 10,
|
|
190
|
+
needsNewPage: true,
|
|
191
|
+
});
|
|
192
|
+
expect(placePdfBlock(68, 50, 10, 287)).toEqual({
|
|
193
|
+
y: 68,
|
|
194
|
+
needsNewPage: false,
|
|
195
|
+
});
|
|
196
|
+
expect(placePdfBlock(250, 50, 10, 287)).toEqual({
|
|
197
|
+
y: 10,
|
|
198
|
+
needsNewPage: true,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("buildPdfFromCharts with extra text pages", () => {
|
|
204
|
+
it("appends a text page even when there are no chart captures", async () => {
|
|
205
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
206
|
+
{
|
|
207
|
+
title: "Cluster sizing recommendations",
|
|
208
|
+
items: [
|
|
209
|
+
{ label: "Cluster name", value: "cluster-1" },
|
|
210
|
+
{ label: "Target platform", value: "Bare metal" },
|
|
211
|
+
{
|
|
212
|
+
label: "Total nodes",
|
|
213
|
+
value: "6 (3 workers + 3 control plane)",
|
|
214
|
+
},
|
|
215
|
+
{ label: "Failover capacity", value: "1 failover nodes" },
|
|
216
|
+
{ label: "Worker node size", value: "8 CPU, 32 GB memory" },
|
|
217
|
+
{
|
|
218
|
+
label: "Control plane node size",
|
|
219
|
+
value: "16 CPU, 64 GB memory",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
footer: "Note: estimates only.",
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
227
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
228
|
+
expect(blob.type).toBe("application/pdf");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("paginates extra pages with many rows without throwing", async () => {
|
|
232
|
+
const manyItems = Array.from({ length: 60 }, (_, index) => ({
|
|
233
|
+
label: `Field ${index}`,
|
|
234
|
+
value: `Value ${index}`,
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
238
|
+
{ title: "Cluster sizing recommendations", items: manyItems },
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("supports multiple extra pages (e.g. one per sized cluster)", async () => {
|
|
245
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
246
|
+
{
|
|
247
|
+
title: "Cluster sizing recommendations — cluster-a",
|
|
248
|
+
items: [{ label: "Cluster name", value: "cluster-a" }],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
title: "Cluster sizing recommendations — cluster-b",
|
|
252
|
+
items: [{ label: "Cluster name", value: "cluster-b" }],
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("buildHtmlReport", () => {
|
|
261
|
+
it("embeds captured chart images and escapes the title", () => {
|
|
262
|
+
const html = buildHtmlReport(
|
|
263
|
+
'Report <script>alert("x")</script>',
|
|
264
|
+
[
|
|
265
|
+
{
|
|
266
|
+
name: "Infrastructure summary",
|
|
267
|
+
dataUrl: "data:image/png;base64,abc",
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
new Date("2026-07-01T15:30:00"),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(html).toContain(
|
|
274
|
+
"Report <script>alert("x")</script>",
|
|
275
|
+
);
|
|
276
|
+
expect(html).toContain('src="data:image/png;base64,abc"');
|
|
277
|
+
expect(html).toContain("Infrastructure summary");
|
|
278
|
+
expect(html).not.toContain("border:");
|
|
279
|
+
expect(html).toContain("width: 100%");
|
|
280
|
+
expect(escapeHtml("<b>")).toBe("<b>");
|
|
281
|
+
expect(escapeHtml("it's")).toBe("it's");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("applyLightThemeToDocument", () => {
|
|
286
|
+
it("removes PatternFly dark theme classes from the given document", () => {
|
|
287
|
+
const clone = document.implementation.createHTMLDocument("export");
|
|
288
|
+
clone.documentElement.classList.add("pf-v6-theme-dark");
|
|
289
|
+
clone.body.style.colorScheme = "dark";
|
|
290
|
+
|
|
291
|
+
applyLightThemeToDocument(clone);
|
|
292
|
+
|
|
293
|
+
expect(clone.documentElement.classList.contains("pf-v6-theme-dark")).toBe(
|
|
294
|
+
false,
|
|
295
|
+
);
|
|
296
|
+
expect(clone.documentElement.style.colorScheme).toBe("light");
|
|
297
|
+
expect(clone.body.style.colorScheme).toBe("light");
|
|
298
|
+
});
|
|
299
|
+
});
|