@oneuptime/common 14.0.2 → 14.0.3

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 (166) hide show
  1. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  2. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  3. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Project.ts +60 -0
  6. package/Models/DatabaseModels/RumApplication.ts +52 -1
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Services/IncidentAlertService.ts +1811 -0
  11. package/Server/Services/IncidentService.ts +210 -10
  12. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  15. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  16. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  17. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  18. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  19. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  20. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  21. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  22. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  23. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  24. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  25. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  26. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  27. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  28. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  29. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  30. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  31. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  32. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  33. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  34. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  35. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  36. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  37. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  38. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  39. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  40. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  41. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  42. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  43. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  44. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  45. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  46. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  47. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  48. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  49. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  50. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  51. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  52. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  53. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  54. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  55. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  56. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  57. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  58. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  59. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  60. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  61. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  62. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  63. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  64. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  65. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  66. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  67. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  68. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  69. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  70. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  71. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  72. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  73. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  74. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  76. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  77. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  78. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  79. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  80. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  81. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  82. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  83. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  84. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  85. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  86. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  87. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  88. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  89. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  90. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  91. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  92. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  93. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  94. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  95. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  96. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  97. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  98. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  99. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  100. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  101. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  102. package/Types/Incident/IncidentAlertLink.ts +38 -0
  103. package/Types/Permission.ts +48 -0
  104. package/Types/Rum/SessionReplay.ts +25 -6
  105. package/Types/Rum/WebVitals.ts +52 -0
  106. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  107. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  108. package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
  109. package/UI/Components/ModelTable/Column.ts +5 -0
  110. package/UI/Components/Table/TableHeader.tsx +47 -2
  111. package/UI/Components/Table/Types/Column.ts +7 -0
  112. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  113. package/UI/Utils/PermissionGate.ts +11 -2
  114. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  115. package/Utils/Rum/SessionTraceState.ts +212 -0
  116. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  117. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  119. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  120. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  121. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  123. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  125. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  127. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  134. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  135. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  136. package/build/dist/Server/Services/IncidentService.js +164 -12
  137. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  138. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  139. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  140. package/build/dist/Server/Services/Index.js +2 -0
  141. package/build/dist/Server/Services/Index.js.map +1 -1
  142. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  143. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  144. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  145. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  146. package/build/dist/Types/Permission.js +42 -0
  147. package/build/dist/Types/Permission.js.map +1 -1
  148. package/build/dist/Types/Rum/WebVitals.js +25 -0
  149. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  150. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  151. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  152. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  153. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  154. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
  155. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  156. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  157. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  158. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  159. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  160. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  161. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  162. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  163. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  164. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  165. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  166. package/package.json +1 -1
@@ -0,0 +1,699 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ act,
4
+ cleanup,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ } from "@testing-library/react";
9
+ import * as React from "react";
10
+ import {
11
+ afterEach,
12
+ beforeEach,
13
+ describe,
14
+ expect,
15
+ jest,
16
+ test,
17
+ } from "@jest/globals";
18
+
19
+ /*
20
+ * The (i) tooltips on the Kubernetes cluster overview, rendered for real.
21
+ *
22
+ * The page is rendered whole, with only its data sources stubbed (the
23
+ * cluster lookup, the inventory summary, the metric aggregates and the
24
+ * top-pod lists) and the heavy neighbours it composes swapped for stubs
25
+ * (model detail card, activity cards, refresh control, the chart itself).
26
+ * Everything that draws a title and its (i) - GoldenMetricTile, InfoCard,
27
+ * Card, the page's own chart card, chips and badges - is the real code.
28
+ *
29
+ * Tooltips are read through aria-describedby after a hover, so each text is
30
+ * checked against the button it belongs to even where two metrics share a
31
+ * title ("CPU" is both a tile and a chart).
32
+ */
33
+
34
+ const mockLineChartProps: Array<Record<string, unknown>> = [];
35
+ const mockNavigations: Array<unknown> = [];
36
+
37
+ jest.mock("../../../UI/Utils/Navigation", () => {
38
+ return {
39
+ __esModule: true,
40
+ default: {
41
+ getLastParamAsObjectID: () => {
42
+ const { default: ObjectIDType } = jest.requireActual(
43
+ "../../../Types/ObjectID",
44
+ ) as { default: new (id: string) => unknown };
45
+ return new ObjectIDType("0193c0de-7777-4aaa-8bbb-000000000007");
46
+ },
47
+ navigate: (route: unknown) => {
48
+ mockNavigations.push(route);
49
+ },
50
+ },
51
+ };
52
+ });
53
+
54
+ jest.mock("../../../UI/Utils/Project", () => {
55
+ return {
56
+ __esModule: true,
57
+ default: {
58
+ getCurrentProjectId: () => {
59
+ const { default: ObjectIDType } = jest.requireActual(
60
+ "../../../Types/ObjectID",
61
+ ) as { default: new (id: string) => unknown };
62
+ return new ObjectIDType("10000000-0000-4000-8000-000000000001");
63
+ },
64
+ },
65
+ };
66
+ });
67
+
68
+ /*
69
+ * The inventory summary the page reads over API.post. Mutable so a test can
70
+ * describe a quieter cluster; `mock` prefix because jest.mock factories are
71
+ * hoisted and may only close over mock-prefixed names.
72
+ */
73
+ let mockSummary: Record<string, unknown> = {};
74
+
75
+ jest.mock("../../../UI/Utils/API/API", () => {
76
+ return {
77
+ __esModule: true,
78
+ default: {
79
+ post: () => {
80
+ return Promise.resolve({ data: mockSummary });
81
+ },
82
+ getFriendlyMessage: (error: unknown) => {
83
+ return (
84
+ ((error as { message?: unknown } | null)?.message as string) ||
85
+ "Could not load"
86
+ );
87
+ },
88
+ },
89
+ };
90
+ });
91
+
92
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
93
+ return {
94
+ __esModule: true,
95
+ default: {
96
+ getItem: () => {
97
+ return Promise.resolve({
98
+ name: "production-us-east",
99
+ clusterIdentifier: "production-us-east-1",
100
+ otelCollectorStatus: "connected",
101
+ lastSeenAt: new Date(),
102
+ });
103
+ },
104
+ getCommonHeaders: () => {
105
+ return {};
106
+ },
107
+ },
108
+ };
109
+ });
110
+
111
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
112
+ return {
113
+ __esModule: true,
114
+ default: {
115
+ aggregate: () => {
116
+ return Promise.resolve({ data: [] });
117
+ },
118
+ },
119
+ };
120
+ });
121
+
122
+ jest.mock(
123
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils",
124
+ () => {
125
+ const actual: {
126
+ default: {
127
+ formatCpuValue: (value: number | null) => string;
128
+ formatMemoryValue: (bytes: number | null) => string;
129
+ };
130
+ } = jest.requireActual(
131
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils",
132
+ ) as {
133
+ default: {
134
+ formatCpuValue: (value: number | null) => string;
135
+ formatMemoryValue: (bytes: number | null) => string;
136
+ };
137
+ };
138
+
139
+ return {
140
+ __esModule: true,
141
+ default: {
142
+ formatCpuValue: actual.default.formatCpuValue,
143
+ formatMemoryValue: actual.default.formatMemoryValue,
144
+ fetchResourceListWithMemory: () => {
145
+ return Promise.resolve([
146
+ {
147
+ name: "api-7d9f",
148
+ namespace: "shop",
149
+ cpuUtilization: 0.4,
150
+ memoryUsageBytes: 512 * 1024 * 1024,
151
+ memoryLimitBytes: null,
152
+ status: "",
153
+ age: "",
154
+ additionalAttributes: { "resource.k8s.node.name": "node-a" },
155
+ },
156
+ ]);
157
+ },
158
+ fetchNodeAllocatableMemory: () => {
159
+ return Promise.resolve(new Map<string, number>());
160
+ },
161
+ },
162
+ };
163
+ },
164
+ );
165
+
166
+ jest.mock(
167
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher",
168
+ () => {
169
+ return {
170
+ __esModule: true,
171
+ ...(jest.requireActual(
172
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher",
173
+ ) as Record<string, unknown>),
174
+ fetchClusterWarningEvents: () => {
175
+ return Promise.resolve([]);
176
+ },
177
+ };
178
+ },
179
+ );
180
+
181
+ jest.mock(
182
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceActivity/ResourceActivityCards",
183
+ () => {
184
+ return {
185
+ __esModule: true,
186
+ default: () => {
187
+ return <div data-testid="resource-activity-cards" />;
188
+ },
189
+ };
190
+ },
191
+ );
192
+
193
+ jest.mock(
194
+ "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/AutoRefreshControl",
195
+ () => {
196
+ return {
197
+ __esModule: true,
198
+ default: () => {
199
+ return <div data-testid="auto-refresh-control" />;
200
+ },
201
+ };
202
+ },
203
+ );
204
+
205
+ jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
206
+ return {
207
+ __esModule: true,
208
+ default: () => {
209
+ return <div data-testid="cluster-details" />;
210
+ },
211
+ };
212
+ });
213
+
214
+ jest.mock(
215
+ "../../../UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker",
216
+ () => {
217
+ return {
218
+ __esModule: true,
219
+ default: () => {
220
+ return <div data-testid="time-range-picker" />;
221
+ },
222
+ };
223
+ },
224
+ );
225
+
226
+ jest.mock("../../../UI/Components/Charts/Line/LineChart", () => {
227
+ return {
228
+ __esModule: true,
229
+ default: (props: Record<string, unknown>) => {
230
+ mockLineChartProps.push(props);
231
+ return <div data-testid="line-chart" />;
232
+ },
233
+ };
234
+ });
235
+
236
+ import KubernetesClusterOverview, {
237
+ ClusterChartCard,
238
+ titleWithTooltip,
239
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index";
240
+ import { KUBERNETES_CLUSTER_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/KubernetesClusterMetricDescriptions";
241
+ import IconProp from "../../../Types/Icon/IconProp";
242
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
243
+
244
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
245
+
246
+ const TEXT: typeof KUBERNETES_CLUSTER_METRIC_DESCRIPTIONS =
247
+ KUBERNETES_CLUSTER_METRIC_DESCRIPTIONS;
248
+
249
+ /*
250
+ * A busy cluster: something in every pod phase, a node not ready and every
251
+ * pressure condition, so every chip and badge the page can draw is drawn.
252
+ */
253
+ const BUSY_SUMMARY: Record<string, unknown> = {
254
+ nodeCount: 3,
255
+ podCount: 12,
256
+ namespaceCount: 4,
257
+ deploymentCount: 5,
258
+ statefulSetCount: 1,
259
+ daemonSetCount: 1,
260
+ jobCount: 1,
261
+ cronJobCount: 1,
262
+ pvcCount: 2,
263
+ pvCount: 2,
264
+ containerCount: 20,
265
+ podPhaseCounts: { running: 8, pending: 2, failed: 1, succeeded: 1 },
266
+ nodeReadyCounts: { ready: 2, notReady: 1 },
267
+ nodePressureCounts: { memoryPressure: 1, diskPressure: 1, pidPressure: 1 },
268
+ degradedPods: [],
269
+ degradedNodes: [],
270
+ };
271
+
272
+ const QUIET_SUMMARY: Record<string, unknown> = {
273
+ nodeCount: 3,
274
+ podCount: 12,
275
+ namespaceCount: 4,
276
+ deploymentCount: 5,
277
+ containerCount: 20,
278
+ podPhaseCounts: { running: 0, pending: 0, failed: 0, succeeded: 0 },
279
+ nodeReadyCounts: { ready: 3, notReady: 0 },
280
+ nodePressureCounts: { memoryPressure: 0, diskPressure: 0, pidPressure: 0 },
281
+ degradedPods: [],
282
+ degradedNodes: [],
283
+ };
284
+
285
+ /*
286
+ * Every (i) the busy overview draws, in page order, with the text it must
287
+ * show. Twenty-six - every overview description in the record.
288
+ */
289
+ const EXPECTED: Array<[string, string]> = [
290
+ ["Cluster inventory counts", TEXT.inventoryCounts],
291
+ ["Running", TEXT.podsRunning],
292
+ ["Pending", TEXT.podsPending],
293
+ ["Failed", TEXT.podsFailed],
294
+ ["Nodes Not Ready", TEXT.nodesNotReady],
295
+ ["Availability", TEXT.availability],
296
+ ["CPU", TEXT.cpu],
297
+ ["Memory", TEXT.memory],
298
+ ["Filesystem", TEXT.filesystem],
299
+ ["Network", TEXT.network],
300
+ ["Availability", TEXT.availabilityChart],
301
+ ["CPU", TEXT.cpuChart],
302
+ ["Memory", TEXT.memoryChart],
303
+ ["Filesystem", TEXT.filesystemChart],
304
+ ["Network", TEXT.networkChart],
305
+ ["Cluster Health", TEXT.clusterHealth],
306
+ ["Nodes", TEXT.nodes],
307
+ ["Pods", TEXT.pods],
308
+ ["Namespaces", TEXT.namespaces],
309
+ ["Agent Status", TEXT.agentStatus],
310
+ ["Memory Pressure", TEXT.memoryPressure],
311
+ ["Disk Pressure", TEXT.diskPressure],
312
+ ["PID Pressure", TEXT.pidPressure],
313
+ ["Pod Health", TEXT.podHealth],
314
+ ["CPU Usage", TEXT.topCpuPods],
315
+ ["Memory Usage", TEXT.topMemoryPods],
316
+ ];
317
+
318
+ const ABOUT: RegExp = /^About /;
319
+
320
+ async function explanationOf(button: HTMLElement): Promise<string> {
321
+ fireEvent.mouseEnter(button);
322
+ await act(async () => {
323
+ jest.advanceTimersByTime(250);
324
+ });
325
+
326
+ const describedBy: string | null = button.getAttribute("aria-describedby");
327
+
328
+ expect(describedBy).toBeTruthy();
329
+
330
+ return document.getElementById(describedBy as string)?.textContent || "";
331
+ }
332
+
333
+ /*
334
+ * What activates the clickable card an (i) sits in: the card itself when it
335
+ * is a role="button" container, or the full-card button laid over it when
336
+ * the card uses the overlay pattern (a button beside the (i), never around
337
+ * it). Either shape is accepted - the test is about the (i), not the card.
338
+ */
339
+ function cardActivatorOf(info: HTMLElement): HTMLElement {
340
+ const container: HTMLElement | null | undefined =
341
+ info.parentElement?.closest('[role="button"]');
342
+
343
+ if (container) {
344
+ return container as HTMLElement;
345
+ }
346
+
347
+ let node: HTMLElement | null = info.parentElement;
348
+
349
+ while (node) {
350
+ const overlay: HTMLButtonElement | undefined = Array.from(
351
+ node.querySelectorAll("button"),
352
+ ).find((button: HTMLButtonElement): boolean => {
353
+ return !ABOUT.test(button.getAttribute("aria-label") || "");
354
+ });
355
+
356
+ if (overlay) {
357
+ return overlay;
358
+ }
359
+
360
+ node = node.parentElement;
361
+ }
362
+
363
+ throw new Error("The (i) is not inside a clickable card.");
364
+ }
365
+
366
+ function infoButtons(): Array<HTMLElement> {
367
+ return screen.queryAllByRole("button", { name: ABOUT });
368
+ }
369
+
370
+ async function renderOverview(): Promise<void> {
371
+ render(<KubernetesClusterOverview {...PAGE_PROPS} />);
372
+
373
+ // The last sections to settle: the summary cards and the top-pod lists.
374
+ await screen.findByRole("button", { name: "About Memory Usage" });
375
+ await screen.findByRole("button", { name: "About Pod Health" });
376
+ await screen.findAllByTestId("line-chart");
377
+ }
378
+
379
+ beforeEach(() => {
380
+ jest.useFakeTimers();
381
+ mockLineChartProps.length = 0;
382
+ mockNavigations.length = 0;
383
+ mockSummary = BUSY_SUMMARY;
384
+ });
385
+
386
+ afterEach(() => {
387
+ cleanup();
388
+ jest.useRealTimers();
389
+ });
390
+
391
+ describe("ClusterChartCard (the overview's golden chart card)", () => {
392
+ const WINDOW: { start: Date; end: Date } = {
393
+ start: new Date("2026-09-24T10:00:00Z"),
394
+ end: new Date("2026-09-24T10:30:00Z"),
395
+ };
396
+
397
+ test("while the window is unresolved, the skeleton already explains the chart", async () => {
398
+ render(
399
+ <ClusterChartCard
400
+ title="CPU"
401
+ description={TEXT.cpuChart}
402
+ icon={IconProp.ChartBar}
403
+ iconColor="blue"
404
+ data={[]}
405
+ chartWindow={null}
406
+ syncId="sync"
407
+ />,
408
+ );
409
+
410
+ expect(screen.getByText("CPU")).toBeInTheDocument();
411
+ expect(screen.queryByTestId("line-chart")).not.toBeInTheDocument();
412
+ expect(
413
+ await explanationOf(screen.getByRole("button", { name: "About CPU" })),
414
+ ).toBe(TEXT.cpuChart);
415
+ });
416
+
417
+ test("once loaded, the chart and the (i) are both there", async () => {
418
+ render(
419
+ <ClusterChartCard
420
+ title="Memory"
421
+ description={TEXT.memoryChart}
422
+ icon={IconProp.SquareStack}
423
+ iconColor="violet"
424
+ data={[]}
425
+ chartWindow={WINDOW}
426
+ syncId="kubernetes-overview-x"
427
+ />,
428
+ );
429
+
430
+ expect(screen.getByTestId("line-chart")).toBeInTheDocument();
431
+ expect(mockLineChartProps[0]?.["syncid"]).toBe("kubernetes-overview-x");
432
+ expect(mockLineChartProps[0]?.["heightInPx"]).toBe(180);
433
+ expect(
434
+ await explanationOf(screen.getByRole("button", { name: "About Memory" })),
435
+ ).toBe(TEXT.memoryChart);
436
+ });
437
+
438
+ test("the header extra (the uptime badge) sits beside the title in both branches", () => {
439
+ const { rerender } = render(
440
+ <ClusterChartCard
441
+ title="Availability"
442
+ description={TEXT.availabilityChart}
443
+ icon={IconProp.Heartbeat}
444
+ iconColor="emerald"
445
+ data={[]}
446
+ chartWindow={null}
447
+ syncId="sync"
448
+ headerExtra={<span>99.9% uptime</span>}
449
+ />,
450
+ );
451
+
452
+ expect(screen.getByText("99.9% uptime")).toBeInTheDocument();
453
+
454
+ rerender(
455
+ <ClusterChartCard
456
+ title="Availability"
457
+ description={TEXT.availabilityChart}
458
+ icon={IconProp.Heartbeat}
459
+ iconColor="emerald"
460
+ data={[]}
461
+ chartWindow={WINDOW}
462
+ syncId="sync"
463
+ headerExtra={<span>99.9% uptime</span>}
464
+ />,
465
+ );
466
+
467
+ expect(screen.getByText("99.9% uptime")).toBeInTheDocument();
468
+ expect(
469
+ screen.getByRole("button", { name: "About Availability" }),
470
+ ).toBeInTheDocument();
471
+ });
472
+
473
+ test("a chart without a description draws no (i)", () => {
474
+ render(
475
+ <ClusterChartCard
476
+ title="CPU"
477
+ icon={IconProp.ChartBar}
478
+ iconColor="blue"
479
+ data={[]}
480
+ chartWindow={WINDOW}
481
+ syncId="sync"
482
+ />,
483
+ );
484
+
485
+ expect(infoButtons()).toHaveLength(0);
486
+ });
487
+
488
+ test("the default y axis is a 0-100% scale; a custom one replaces it", () => {
489
+ render(
490
+ <ClusterChartCard
491
+ title="CPU"
492
+ description={TEXT.cpuChart}
493
+ icon={IconProp.ChartBar}
494
+ iconColor="blue"
495
+ data={[]}
496
+ chartWindow={WINDOW}
497
+ syncId="sync"
498
+ />,
499
+ );
500
+
501
+ const yAxis: { legend: string; options: { max: unknown } } =
502
+ mockLineChartProps[0]?.["yAxis"] as {
503
+ legend: string;
504
+ options: { max: unknown };
505
+ };
506
+
507
+ expect(yAxis.legend).toBe("%");
508
+ expect(yAxis.options.max).toBe(100);
509
+ });
510
+ });
511
+
512
+ describe("titleWithTooltip", () => {
513
+ test("draws the title and an (i) named after it", async () => {
514
+ render(<h4>{titleWithTooltip("Pod Health", TEXT.podHealth)}</h4>);
515
+
516
+ expect(screen.getByRole("heading")).toHaveTextContent("Pod Health");
517
+ expect(
518
+ await explanationOf(
519
+ screen.getByRole("button", { name: "About Pod Health" }),
520
+ ),
521
+ ).toBe(TEXT.podHealth);
522
+ });
523
+ });
524
+
525
+ describe("the cluster overview page", () => {
526
+ test("every metric on a busy cluster carries an (i) - all twenty-six of them", async () => {
527
+ await renderOverview();
528
+
529
+ const names: Array<string> = infoButtons().map(
530
+ (button: HTMLElement): string => {
531
+ return button.getAttribute("aria-label") || "";
532
+ },
533
+ );
534
+
535
+ expect(names).toEqual(
536
+ EXPECTED.map((pair: [string, string]): string => {
537
+ return `About ${pair[0]}`;
538
+ }),
539
+ );
540
+ });
541
+
542
+ test("each (i) explains its own metric", async () => {
543
+ await renderOverview();
544
+
545
+ const buttons: Array<HTMLElement> = infoButtons();
546
+
547
+ expect(buttons).toHaveLength(EXPECTED.length);
548
+
549
+ for (let index: number = 0; index < EXPECTED.length; index++) {
550
+ const [title, text] = EXPECTED[index]!;
551
+
552
+ expect({ title, shown: await explanationOf(buttons[index]!) }).toEqual({
553
+ title,
554
+ shown: text,
555
+ });
556
+ }
557
+ });
558
+
559
+ test("the page shows every overview description in the record (Insights has the other)", async () => {
560
+ await renderOverview();
561
+
562
+ const shown: Set<string> = new Set(
563
+ EXPECTED.map((pair: [string, string]): string => {
564
+ return pair[1];
565
+ }),
566
+ );
567
+
568
+ for (const [key, text] of Object.entries(TEXT)) {
569
+ expect({ key, shown: shown.has(text) }).toEqual({
570
+ key,
571
+ shown: key !== "networkThroughput",
572
+ });
573
+ }
574
+ });
575
+
576
+ test("no (i) is nested inside another button or a link", async () => {
577
+ await renderOverview();
578
+
579
+ for (const button of infoButtons()) {
580
+ expect(button.parentElement?.closest("button, a")).toBeNull();
581
+ }
582
+ });
583
+
584
+ test("an (i) on a clickable summary card does not navigate", async () => {
585
+ await renderOverview();
586
+
587
+ const nodes: HTMLElement = screen.getByRole("button", {
588
+ name: "About Nodes",
589
+ });
590
+
591
+ // The (i) swallows the click, so asking what "Nodes" means stays here.
592
+ expect(fireEvent.click(nodes)).toBe(false);
593
+ fireEvent.keyDown(nodes, { key: "Enter" });
594
+ expect(mockNavigations).toHaveLength(0);
595
+
596
+ // The card itself still navigates, so the check above means something.
597
+ fireEvent.click(cardActivatorOf(nodes));
598
+ expect(mockNavigations).toHaveLength(1);
599
+ expect(String(mockNavigations[0])).toContain("/nodes");
600
+ });
601
+
602
+ test("the chips and badges only appear - with their (i) - when there is something to count", async () => {
603
+ mockSummary = QUIET_SUMMARY;
604
+
605
+ render(<KubernetesClusterOverview {...PAGE_PROPS} />);
606
+
607
+ await screen.findByRole("button", { name: "About Memory Usage" });
608
+
609
+ for (const title of [
610
+ "Running",
611
+ "Pending",
612
+ "Failed",
613
+ "Nodes Not Ready",
614
+ "Memory Pressure",
615
+ "Disk Pressure",
616
+ "PID Pressure",
617
+ ]) {
618
+ expect(
619
+ screen.queryByRole("button", { name: `About ${title}` }),
620
+ ).not.toBeInTheDocument();
621
+ }
622
+
623
+ // What does not depend on those counts is still explained.
624
+ for (const title of [
625
+ "Cluster inventory counts",
626
+ "Cluster Health",
627
+ "Nodes",
628
+ "Agent Status",
629
+ "Pod Health",
630
+ "CPU Usage",
631
+ ]) {
632
+ expect(
633
+ screen.getByRole("button", { name: `About ${title}` }),
634
+ ).toBeInTheDocument();
635
+ }
636
+ });
637
+
638
+ test("a pending-pod chip and its (i) read as one unit", async () => {
639
+ await renderOverview();
640
+
641
+ const info: HTMLElement = screen.getByRole("button", {
642
+ name: "About Pending",
643
+ });
644
+ const chip: HTMLElement | null = info.closest("span.rounded-full");
645
+
646
+ expect(chip).not.toBeNull();
647
+ expect(chip).toHaveTextContent("2Pending");
648
+ });
649
+
650
+ test("a pressure badge's (i) sits right beside the badge it explains", async () => {
651
+ await renderOverview();
652
+
653
+ const info: HTMLElement = screen.getByRole("button", {
654
+ name: "About Disk Pressure",
655
+ });
656
+
657
+ expect(info.parentElement?.parentElement).toHaveTextContent(
658
+ "1 node: Disk Pressure",
659
+ );
660
+ });
661
+
662
+ test("a cluster with no pods has no Pod Health card, and no (i) for it", async () => {
663
+ mockSummary = { ...QUIET_SUMMARY, podCount: 0 };
664
+
665
+ render(<KubernetesClusterOverview {...PAGE_PROPS} />);
666
+
667
+ await screen.findByRole("button", { name: "About Memory Usage" });
668
+
669
+ expect(
670
+ screen.queryByRole("button", { name: "About Pod Health" }),
671
+ ).not.toBeInTheDocument();
672
+ });
673
+
674
+ test("tiles with no data yet still explain what they would show", async () => {
675
+ await renderOverview();
676
+
677
+ // The stubbed aggregates return nothing, so every value tile reads "-".
678
+ expect(screen.getAllByText("\u2014").length).toBeGreaterThanOrEqual(4);
679
+
680
+ for (const title of ["CPU", "Memory", "Filesystem", "Network"]) {
681
+ const [tile] = screen.getAllByRole("button", { name: `About ${title}` });
682
+
683
+ expect(tile).toBeDefined();
684
+ }
685
+
686
+ expect(
687
+ await explanationOf(
688
+ screen.getAllByRole("button", { name: "About Filesystem" })[0]!,
689
+ ),
690
+ ).toBe(TEXT.filesystem);
691
+ });
692
+
693
+ test("the page loads the cluster named in the route", async () => {
694
+ await renderOverview();
695
+
696
+ expect(screen.getByText("production-us-east")).toBeInTheDocument();
697
+ expect(screen.getByText("production-us-east-1")).toBeInTheDocument();
698
+ });
699
+ });