@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,601 @@
1
+ /**
2
+ * @timezone UTC
3
+ */
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ jest,
10
+ test,
11
+ } from "@jest/globals";
12
+ import "@testing-library/jest-dom";
13
+ import {
14
+ act,
15
+ cleanup,
16
+ fireEvent,
17
+ render,
18
+ screen,
19
+ waitFor,
20
+ } from "@testing-library/react";
21
+ import React from "react";
22
+
23
+ /*
24
+ * The VMware vCenter overview, rendered for real with the network and the
25
+ * heavy children (charts, activity cards, the details card) replaced. It
26
+ * checks what a customer sees: every tile, chart card, summary card and
27
+ * top-5 section has an (i) beside its title, the (i) says the right thing,
28
+ * the chart cards have it from the first paint (while their data is still
29
+ * loading), and pressing an (i) on a clickable summary card explains
30
+ * rather than navigates.
31
+ */
32
+
33
+ interface Gate {
34
+ promise: Promise<void>;
35
+ open: () => void;
36
+ }
37
+
38
+ function gate(): Gate {
39
+ let open: () => void = (): void => {};
40
+ const promise: Promise<void> = new Promise<void>((resolve: () => void) => {
41
+ open = resolve;
42
+ });
43
+
44
+ return { promise, open };
45
+ }
46
+
47
+ const mockGetItemCalls: Array<unknown> = [];
48
+ const mockGetListCalls: Array<unknown> = [];
49
+ const mockAggregateCalls: Array<unknown> = [];
50
+
51
+ let mockVCenter: unknown = null;
52
+ let mockRows: Array<unknown> = [];
53
+ let mockListGate: Promise<void> = Promise.resolve();
54
+ let mockAggregateGate: Promise<void> = Promise.resolve();
55
+ let mockAggregateFails: boolean = false;
56
+
57
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
58
+ return {
59
+ __esModule: true,
60
+ default: {
61
+ getItem: (request: unknown): Promise<unknown> => {
62
+ mockGetItemCalls.push(request);
63
+ return Promise.resolve(mockVCenter);
64
+ },
65
+ getList: async (request: unknown): Promise<unknown> => {
66
+ mockGetListCalls.push(request);
67
+ await mockListGate;
68
+ return { data: mockRows, count: mockRows.length };
69
+ },
70
+ },
71
+ };
72
+ });
73
+
74
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
75
+ return {
76
+ __esModule: true,
77
+ default: {
78
+ aggregate: async (request: unknown): Promise<unknown> => {
79
+ mockAggregateCalls.push(request);
80
+ await mockAggregateGate;
81
+ if (mockAggregateFails) {
82
+ throw new Error("ClickHouse is not reachable");
83
+ }
84
+ return { data: [] };
85
+ },
86
+ },
87
+ };
88
+ });
89
+
90
+ // Recharts has no layout under jsdom; the card around the chart is the subject.
91
+ jest.mock("../../../UI/Components/Charts/Line/LineChart", () => {
92
+ return {
93
+ __esModule: true,
94
+ default: (): React.ReactElement => {
95
+ return React.createElement("div", { "data-testid": "line-chart" });
96
+ },
97
+ };
98
+ });
99
+
100
+ jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
101
+ return {
102
+ __esModule: true,
103
+ default: (): React.ReactElement => {
104
+ return React.createElement("section", { "data-testid": "details" });
105
+ },
106
+ };
107
+ });
108
+
109
+ jest.mock(
110
+ "../../../UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker",
111
+ () => {
112
+ return {
113
+ __esModule: true,
114
+ default: (): React.ReactElement => {
115
+ return React.createElement("div", { "data-testid": "range-picker" });
116
+ },
117
+ };
118
+ },
119
+ );
120
+
121
+ jest.mock(
122
+ "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/AutoRefreshControl",
123
+ () => {
124
+ return {
125
+ __esModule: true,
126
+ default: (): React.ReactElement => {
127
+ return React.createElement("div", { "data-testid": "auto-refresh" });
128
+ },
129
+ };
130
+ },
131
+ );
132
+
133
+ jest.mock(
134
+ "../../../../App/FeatureSet/Dashboard/src/Components/ResourceActivity/ResourceActivityCards",
135
+ () => {
136
+ return {
137
+ __esModule: true,
138
+ default: (): React.ReactElement => {
139
+ return React.createElement("div", { "data-testid": "activity" });
140
+ },
141
+ };
142
+ },
143
+ );
144
+
145
+ import VMwareVCenterOverview from "../../../../App/FeatureSet/Dashboard/src/Pages/VMware/View/Index";
146
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
147
+ import {
148
+ VMWARE_METRIC_DESCRIPTIONS,
149
+ VMwareMetric,
150
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/VMwareMetricDescriptions";
151
+ import Route from "../../../Types/API/Route";
152
+ import ObjectID from "../../../Types/ObjectID";
153
+ import Navigation from "../../../UI/Utils/Navigation";
154
+ import ProjectUtil from "../../../UI/Utils/Project";
155
+ import { getJestSpyOn } from "../../Spy";
156
+ import {
157
+ GIB,
158
+ explainedLabels,
159
+ explanationOnFocus,
160
+ explanationOnHover,
161
+ expectNotNested,
162
+ infoButtonsFor,
163
+ infoLabels,
164
+ inventoryRow,
165
+ vcenterModel,
166
+ } from "./VMwareTooltipHarness";
167
+
168
+ const PROJECT_ID: string = "22222222-2222-4222-8222-222222222222";
169
+ const VCENTER_ID: string = "11111111-1111-4111-8111-111111111111";
170
+
171
+ const D: Record<VMwareMetric, string> = VMWARE_METRIC_DESCRIPTIONS;
172
+
173
+ const PAGE_PROPS: PageComponentProps = {
174
+ pageRoute: new Route(`/dashboard/${PROJECT_ID}/vmware/${VCENTER_ID}`),
175
+ currentProject: null,
176
+ hasPaymentMethod: false,
177
+ };
178
+
179
+ // One (i) after the hero's count chips (datacenters, clusters, hosts...).
180
+ const HERO: Array<[string, string]> = [
181
+ ["vCenter inventory counts", D.overviewInventoryCounts],
182
+ ];
183
+
184
+ const TILES: Array<[string, string]> = [
185
+ ["Host Effectiveness", D.overviewHostEffectiveness],
186
+ ["Host CPU", D.overviewHostCpu],
187
+ ["Host Memory", D.overviewHostMemory],
188
+ ["Datastores", D.overviewDatastores],
189
+ ["VM CPU Ready", D.overviewVmCpuReady],
190
+ ["Virtual Machines", D.overviewVirtualMachines],
191
+ ];
192
+
193
+ const CHARTS: Array<[string, string]> = [
194
+ ["Host CPU", D.overviewHostCpuChart],
195
+ ["Host Memory", D.overviewHostMemoryChart],
196
+ ["Datastore Used", D.overviewDatastoreUsedChart],
197
+ ["VM CPU Ready", D.overviewVmCpuReadyChart],
198
+ ];
199
+
200
+ const SUMMARY: Array<[string, string]> = [
201
+ ["vCenter Health", D.overviewVCenterHealth],
202
+ ["Datacenters", D.overviewDatacenterCount],
203
+ ["Clusters", D.overviewClusterCount],
204
+ ["Hosts", D.overviewHostCount],
205
+ ["Virtual Machines", D.overviewVirtualMachineCount],
206
+ ["Datastores", D.overviewDatastoreCount],
207
+ ["Resource Pools", D.overviewResourcePoolCount],
208
+ ["Agent Status", D.overviewAgentStatus],
209
+ ];
210
+
211
+ const TOP: Array<[string, string]> = [
212
+ ["Host CPU", D.topHostsByCpu],
213
+ ["Host Memory", D.topHostsByMemory],
214
+ ["Datastore Utilization", D.topDatastoresByUtilization],
215
+ ["VM CPU Ready", D.topVmsByCpuReady],
216
+ ];
217
+
218
+ function labelsOf(pairs: Array<[string, string]>): Array<string> {
219
+ return pairs.map(([label]: [string, string]): string => {
220
+ return label;
221
+ });
222
+ }
223
+
224
+ function inventory(): Array<unknown> {
225
+ const now: Date = new Date();
226
+
227
+ return [
228
+ inventoryRow({
229
+ kind: "Datacenter",
230
+ externalId: "datacenter/dc1",
231
+ name: "dc1",
232
+ hostCount: 2,
233
+ poweredOnHostCount: 2,
234
+ }),
235
+ inventoryRow({
236
+ kind: "Cluster",
237
+ externalId: "cluster/dc1/cl1",
238
+ name: "cl1",
239
+ datacenterName: "dc1",
240
+ hostCount: 2,
241
+ effectiveHostCount: 2,
242
+ metricsUpdatedAt: now,
243
+ }),
244
+ inventoryRow({
245
+ kind: "Host",
246
+ externalId: "host/dc1/esx01",
247
+ name: "esx01",
248
+ datacenterName: "dc1",
249
+ clusterName: "cl1",
250
+ latestCpuPercent: 42,
251
+ latestMemoryBytes: 64 * GIB,
252
+ latestMemoryPercent: 50,
253
+ metricsUpdatedAt: now,
254
+ }),
255
+ inventoryRow({
256
+ kind: "VirtualMachine",
257
+ externalId: "vm/uuid-1",
258
+ name: "web-01",
259
+ hostName: "esx01",
260
+ isTemplate: false,
261
+ isPoweredOn: true,
262
+ cpuReadinessPercent: 3,
263
+ metricsUpdatedAt: now,
264
+ }),
265
+ inventoryRow({
266
+ kind: "Datastore",
267
+ externalId: "datastore/dc1/ds1",
268
+ name: "ds1",
269
+ datacenterName: "dc1",
270
+ latestDiskBytes: 40 * GIB,
271
+ maxDiskBytes: 100 * GIB,
272
+ latestDiskPercent: 40,
273
+ metricsUpdatedAt: now,
274
+ }),
275
+ inventoryRow({
276
+ kind: "ResourcePool",
277
+ externalId: "resourcepool/dc1/cl1/Resources",
278
+ name: "Resources",
279
+ }),
280
+ ];
281
+ }
282
+
283
+ let navigateSpy: ReturnType<typeof getJestSpyOn>;
284
+
285
+ beforeEach(() => {
286
+ jest.useFakeTimers();
287
+ window.history.pushState(
288
+ {},
289
+ "",
290
+ `/dashboard/${PROJECT_ID}/vmware/${VCENTER_ID}`,
291
+ );
292
+ mockGetItemCalls.length = 0;
293
+ mockGetListCalls.length = 0;
294
+ mockAggregateCalls.length = 0;
295
+ mockVCenter = vcenterModel();
296
+ mockRows = inventory();
297
+ mockListGate = Promise.resolve();
298
+ mockAggregateGate = Promise.resolve();
299
+ mockAggregateFails = false;
300
+ jest
301
+ .spyOn(ProjectUtil, "getCurrentProjectId")
302
+ .mockReturnValue(new ObjectID(PROJECT_ID));
303
+ navigateSpy = getJestSpyOn(Navigation, "navigate").mockImplementation(
304
+ (): void => {},
305
+ );
306
+ });
307
+
308
+ afterEach(() => {
309
+ cleanup();
310
+ jest.restoreAllMocks();
311
+ jest.useRealTimers();
312
+ window.history.pushState({}, "", "/");
313
+ });
314
+
315
+ async function renderLoaded(): Promise<void> {
316
+ render(<VMwareVCenterOverview {...PAGE_PROPS} />);
317
+
318
+ // The last golden tile and the last top-5 section are both on screen.
319
+ await waitFor(() => {
320
+ expect(infoButtonsFor("Host Effectiveness")).toHaveLength(1);
321
+ expect(infoButtonsFor("Datastore Utilization")).toHaveLength(1);
322
+ });
323
+ }
324
+
325
+ describe("VMware overview: an (i) beside every number", () => {
326
+ test("hero counts, tiles, chart cards, summary cards and top-5 sections, in page order", async () => {
327
+ await renderLoaded();
328
+
329
+ expect(infoLabels()).toEqual([
330
+ ...labelsOf(HERO),
331
+ ...labelsOf(TILES),
332
+ ...labelsOf(CHARTS),
333
+ ...labelsOf(SUMMARY),
334
+ ...labelsOf(TOP),
335
+ ]);
336
+ });
337
+
338
+ test("each (i) says what its own number means", async () => {
339
+ await renderLoaded();
340
+
341
+ expect(await explainedLabels()).toEqual([
342
+ ...HERO,
343
+ ...TILES,
344
+ ...CHARTS,
345
+ ...SUMMARY,
346
+ ...TOP,
347
+ ]);
348
+ });
349
+
350
+ test("the hero's (i) follows its count chips, in the same row", async () => {
351
+ await renderLoaded();
352
+
353
+ const [button] = infoButtonsFor("vCenter inventory counts");
354
+ const row: HTMLElement = button!.parentElement!;
355
+
356
+ expectNotNested(button!);
357
+ // The chips of the fixture inventory, then the (i) - last in the row.
358
+ expect(row.textContent).toContain("1 datacenter");
359
+ expect(row.textContent).toContain("1 cluster");
360
+ expect(row.textContent).toContain("1 ESXi host");
361
+ expect(row.textContent).toContain("1/1 VM powered on");
362
+ expect(row.textContent).toContain("1 datastore");
363
+ expect(row.lastElementChild).toBe(button);
364
+ });
365
+
366
+ test("a hero with only the agent version chip gets no (i)", async () => {
367
+ mockVCenter = vcenterModel({ agentVersion: "1.2.3" });
368
+ mockRows = [];
369
+
370
+ render(<VMwareVCenterOverview {...PAGE_PROPS} />);
371
+
372
+ await waitFor(() => {
373
+ expect(screen.getByText("Agent 1.2.3")).toBeInTheDocument();
374
+ });
375
+ await waitFor(() => {
376
+ expect(infoButtonsFor("Agent Status")).toHaveLength(1);
377
+ });
378
+
379
+ expect(infoButtonsFor("vCenter inventory counts")).toHaveLength(0);
380
+ });
381
+
382
+ test("the same explanation is reachable from the keyboard", async () => {
383
+ await renderLoaded();
384
+
385
+ const [tileCpu, chartCpu, topCpu] = infoButtonsFor("Host CPU");
386
+
387
+ expect(await explanationOnFocus(tileCpu!)).toBe(D.overviewHostCpu);
388
+ expect(await explanationOnFocus(chartCpu!)).toBe(D.overviewHostCpuChart);
389
+ expect(await explanationOnFocus(topCpu!)).toBe(D.topHostsByCpu);
390
+ });
391
+
392
+ test("each (i) sits right beside the title it explains", async () => {
393
+ await renderLoaded();
394
+
395
+ for (const label of new Set(
396
+ labelsOf([...TILES, ...CHARTS, ...SUMMARY, ...TOP]),
397
+ )) {
398
+ for (const button of infoButtonsFor(label)) {
399
+ // The (i)'s own row holds the title and nothing else.
400
+ expect({
401
+ label,
402
+ row: button.parentElement?.textContent?.trim(),
403
+ }).toEqual({ label, row: label });
404
+ }
405
+ }
406
+ });
407
+
408
+ test("no (i) is nested in a link or another button", async () => {
409
+ await renderLoaded();
410
+
411
+ for (const label of new Set(
412
+ labelsOf([...HERO, ...TILES, ...CHARTS, ...SUMMARY, ...TOP]),
413
+ )) {
414
+ for (const button of infoButtonsFor(label)) {
415
+ expectNotNested(button);
416
+ }
417
+ }
418
+ });
419
+ });
420
+
421
+ describe("VMware overview: the chart cards explain themselves while they load", () => {
422
+ test("before the metrics arrive, every chart skeleton already has its (i)", async () => {
423
+ const metrics: Gate = gate();
424
+ mockAggregateGate = metrics.promise;
425
+
426
+ render(<VMwareVCenterOverview {...PAGE_PROPS} />);
427
+
428
+ await waitFor(() => {
429
+ expect(infoButtonsFor("Datastore Used")).toHaveLength(1);
430
+ });
431
+
432
+ // The golden tiles are skeletons with no titles yet...
433
+ expect(infoButtonsFor("Host Effectiveness")).toHaveLength(0);
434
+ // ...but each chart card is drawn with its title and (i).
435
+ expect(document.querySelectorAll(".animate-pulse").length).toBeGreaterThan(
436
+ 0,
437
+ );
438
+ expect(screen.queryAllByTestId("line-chart")).toHaveLength(0);
439
+
440
+ const chartButtons: Array<HTMLElement> = [
441
+ infoButtonsFor("Host CPU")[0]!,
442
+ infoButtonsFor("Host Memory")[0]!,
443
+ infoButtonsFor("Datastore Used")[0]!,
444
+ infoButtonsFor("VM CPU Ready")[0]!,
445
+ ];
446
+ const texts: Array<string> = [];
447
+
448
+ for (const button of chartButtons) {
449
+ texts.push(await explanationOnHover(button));
450
+ }
451
+
452
+ expect(texts).toEqual(
453
+ CHARTS.map(([, text]: [string, string]): string => {
454
+ return text;
455
+ }),
456
+ );
457
+
458
+ await act(async () => {
459
+ metrics.open();
460
+ });
461
+
462
+ await waitFor(() => {
463
+ expect(screen.getAllByTestId("line-chart")).toHaveLength(4);
464
+ });
465
+
466
+ // Same four (i)s once the charts are in.
467
+ expect(infoButtonsFor("Datastore Used")).toHaveLength(1);
468
+ expect(await explanationOnHover(infoButtonsFor("Datastore Used")[0]!)).toBe(
469
+ D.overviewDatastoreUsedChart,
470
+ );
471
+ });
472
+
473
+ test("the summary cards are explained before the inventory arrives", async () => {
474
+ const list: Gate = gate();
475
+ mockListGate = list.promise;
476
+
477
+ render(<VMwareVCenterOverview {...PAGE_PROPS} />);
478
+
479
+ await waitFor(() => {
480
+ expect(infoButtonsFor("vCenter Health")).toHaveLength(1);
481
+ });
482
+
483
+ // Values are still the "…" placeholder, the (i)s are already there.
484
+ expect(screen.getAllByText("…").length).toBeGreaterThan(0);
485
+ expect(await explanationOnHover(infoButtonsFor("Resource Pools")[0]!)).toBe(
486
+ D.overviewResourcePoolCount,
487
+ );
488
+
489
+ await act(async () => {
490
+ list.open();
491
+ });
492
+ });
493
+
494
+ test("when the metrics fail, the inventory cards keep their (i)s", async () => {
495
+ mockAggregateFails = true;
496
+
497
+ render(<VMwareVCenterOverview {...PAGE_PROPS} />);
498
+
499
+ await waitFor(() => {
500
+ expect(
501
+ screen.getByText("ClickHouse is not reachable", { exact: false }),
502
+ ).toBeInTheDocument();
503
+ });
504
+ await waitFor(() => {
505
+ expect(infoButtonsFor("Datastore Utilization")).toHaveLength(1);
506
+ });
507
+
508
+ /*
509
+ * No tiles and no charts - but the hero counts, the summary strip and
510
+ * the top-5 lists remain.
511
+ */
512
+ expect(infoLabels()).toEqual([
513
+ ...labelsOf(HERO),
514
+ ...labelsOf(SUMMARY),
515
+ ...labelsOf(TOP),
516
+ ]);
517
+ });
518
+ });
519
+
520
+ describe("VMware overview: an (i) on a clickable summary card explains, it does not navigate", () => {
521
+ const CLICKABLE: Array<[string, string]> = [
522
+ ["Clusters", "/clusters"],
523
+ ["Hosts", "/hosts"],
524
+ ["Virtual Machines", "/virtual-machines"],
525
+ ["Datastores", "/datastores"],
526
+ ["Resource Pools", "/resource-pools"],
527
+ ];
528
+
529
+ /*
530
+ * The summary card's (i), not the golden tile's: "Datastores" and
531
+ * "Virtual Machines" are both a tile and a card, and the tile comes first.
532
+ */
533
+ function summaryButton(label: string): HTMLElement {
534
+ const buttons: Array<HTMLElement> = infoButtonsFor(label);
535
+
536
+ return buttons[buttons.length === 1 ? 0 : 1]!;
537
+ }
538
+
539
+ test.each(CLICKABLE)(
540
+ "%s: clicking the (i) stays on the page, clicking the card opens the list",
541
+ async (label: string, routeSuffix: string) => {
542
+ await renderLoaded();
543
+
544
+ const info: HTMLElement = summaryButton(label);
545
+ /*
546
+ * The card's own click target is a separate button named after the
547
+ * card, laid over it - a sibling of the (i), never its ancestor.
548
+ */
549
+ const cardButton: HTMLElement = screen.getByRole("button", {
550
+ name: label,
551
+ });
552
+
553
+ expect(cardButton).not.toBe(info);
554
+ expect(cardButton.contains(info)).toBe(false);
555
+ expect(info.parentElement?.closest("button, a, [role='button']")).toBe(
556
+ null,
557
+ );
558
+ expect(cardButton.parentElement?.contains(info)).toBe(true);
559
+ // Lifted above the overlay, so the pointer reaches the (i) first.
560
+ expect(info.className).toContain("z-10");
561
+
562
+ fireEvent.click(info);
563
+ expect(navigateSpy).not.toHaveBeenCalled();
564
+
565
+ fireEvent.keyDown(info, { key: "Enter" });
566
+ fireEvent.keyDown(info, { key: " " });
567
+ expect(navigateSpy).not.toHaveBeenCalled();
568
+
569
+ fireEvent.click(cardButton);
570
+ expect(navigateSpy).toHaveBeenCalledTimes(1);
571
+ expect(String(navigateSpy.mock.calls[0]![0])).toContain(
572
+ `/vmware/${VCENTER_ID}${routeSuffix}`,
573
+ );
574
+ },
575
+ );
576
+
577
+ test("the (i) on a clickable card still shows its explanation", async () => {
578
+ await renderLoaded();
579
+
580
+ expect(await explanationOnHover(summaryButton("Clusters"))).toBe(
581
+ D.overviewClusterCount,
582
+ );
583
+ expect(await explanationOnHover(summaryButton("Datastores"))).toBe(
584
+ D.overviewDatastoreCount,
585
+ );
586
+ expect(navigateSpy).not.toHaveBeenCalled();
587
+ });
588
+
589
+ test("cards that do not navigate are not turned into buttons by the (i)", async () => {
590
+ await renderLoaded();
591
+
592
+ for (const label of ["vCenter Health", "Datacenters", "Agent Status"]) {
593
+ const button: HTMLElement = infoButtonsFor(label)[0]!;
594
+
595
+ expect(button.parentElement?.closest("button, [role='button']")).toBe(
596
+ null,
597
+ );
598
+ expect(screen.queryByRole("button", { name: label })).toBeNull();
599
+ }
600
+ });
601
+ });