@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,852 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ } from "@testing-library/react";
17
+ import * as React from "react";
18
+ import { MemoryRouter } from "react-router-dom";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+
21
+ /*
22
+ * The (i) tooltips on the network device pages, RENDERED: the device
23
+ * Overview hero, the summary strip above the device list, the Overview's
24
+ * interface digest and Inventory uptime, the Network Overview landing page,
25
+ * the Interfaces tab and the Probe Latency Matrix.
26
+ *
27
+ * Each surface is rendered for real with only the network (ModelAPI, the
28
+ * summary endpoints) and the ModelTable replaced, and every metric title is
29
+ * checked for an (i) whose tooltip is the matching
30
+ * NETWORK_DEVICE_METRIC_DESCRIPTIONS entry. Titles that only name something
31
+ * (Site, Polled By, Index, MAC Address) are checked for having none, and the
32
+ * clickable summary tiles are checked for not toggling their filter when
33
+ * someone asks what the number means.
34
+ *
35
+ * Asserting tippy under jsdom: see InfoTooltip.test.tsx. Tooltips are lazy,
36
+ * portalled to document.body, and never finish animating out, so each
37
+ * assertion looks for its text among the tooltips on the page.
38
+ */
39
+
40
+ const DEVICE_ID: string = "22222222-2222-4222-8222-222222222222";
41
+ const PROBE_ID: string = "44444444-4444-4444-8444-444444444444";
42
+
43
+ const getItemMock: MockFunction = getJestMockFunction();
44
+ const getListMock: MockFunction = getJestMockFunction();
45
+ const countMock: MockFunction = getJestMockFunction();
46
+ const fetchDeviceSummaryMock: MockFunction = getJestMockFunction();
47
+ const fetchNetworkOverviewMock: MockFunction = getJestMockFunction();
48
+ const modelTableMock: MockFunction = getJestMockFunction();
49
+
50
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
51
+ return {
52
+ __esModule: true,
53
+ default: {
54
+ getItem: (...args: Array<unknown>): unknown => {
55
+ return getItemMock(...args);
56
+ },
57
+ getList: (...args: Array<unknown>): unknown => {
58
+ return getListMock(...args);
59
+ },
60
+ count: (...args: Array<unknown>): unknown => {
61
+ return countMock(...args);
62
+ },
63
+ getCommonHeaders: (): Record<string, string> => {
64
+ return {};
65
+ },
66
+ },
67
+ };
68
+ });
69
+
70
+ jest.mock(
71
+ "../../../../App/FeatureSet/Dashboard/src/Components/Network/NetworkSummaryApi",
72
+ () => {
73
+ return {
74
+ __esModule: true,
75
+ fetchDeviceSummary: (...args: Array<unknown>): unknown => {
76
+ return fetchDeviceSummaryMock(...args);
77
+ },
78
+ fetchNetworkOverview: (...args: Array<unknown>): unknown => {
79
+ return fetchNetworkOverviewMock(...args);
80
+ },
81
+ };
82
+ },
83
+ );
84
+
85
+ jest.mock("../../../UI/Utils/Navigation", () => {
86
+ return {
87
+ __esModule: true,
88
+ default: {
89
+ // Read at render time, long after the module-level id below exists.
90
+ getLastParamAsObjectID: (): unknown => {
91
+ return mockDeviceObjectId;
92
+ },
93
+ navigate: (): void => {},
94
+ isOnThisPage: (): boolean => {
95
+ return false;
96
+ },
97
+ },
98
+ };
99
+ });
100
+
101
+ jest.mock("../../../UI/Utils/Project", () => {
102
+ return {
103
+ __esModule: true,
104
+ default: {
105
+ getCurrentProjectId: (): unknown => {
106
+ return mockProjectObjectId;
107
+ },
108
+ },
109
+ };
110
+ });
111
+
112
+ /*
113
+ * The Interfaces tab's list is a ModelTable. It is replaced by a recorder,
114
+ * so the columns the page hands it — titles and header tooltips — can be
115
+ * read without a server.
116
+ */
117
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
118
+ return {
119
+ __esModule: true,
120
+ default: (props: unknown) => {
121
+ modelTableMock(props);
122
+ return <div data-testid="model-table" />;
123
+ },
124
+ };
125
+ });
126
+
127
+ import DeviceStatusHero, {
128
+ DeviceHeroTileTitle,
129
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceStatusHero";
130
+ import DeviceSummaryCards from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceSummaryCards";
131
+ import DeviceInterfacesPreview from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceInterfacesPreview";
132
+ import {
133
+ DEVICE_SUMMARY_TILES,
134
+ DeviceSummaryTile,
135
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceSummaryTiles";
136
+ import {
137
+ NETWORK_DEVICE_METRIC_DESCRIPTIONS,
138
+ NetworkDeviceMetric,
139
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/NetworkDeviceMetricDescriptions";
140
+ import { InventoryUptimeValue } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceInventoryCard";
141
+ import NetworkOverview from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/Overview";
142
+ import NetworkDeviceLatencyMatrix from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/LatencyMatrix";
143
+ import NetworkDeviceInterfaces from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkDevice/View/Interfaces";
144
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
145
+ import OneUptimeDate from "../../../Types/Date";
146
+ import { JSONObject } from "../../../Types/JSON";
147
+ import API from "../../../UI/Utils/API/API";
148
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
149
+ import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
150
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
151
+ import NetworkInterface from "../../../Models/DatabaseModels/NetworkInterface";
152
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
153
+ import Probe from "../../../Models/DatabaseModels/Probe";
154
+ import { Green } from "../../../Types/BrandColors";
155
+ import NetworkDeviceMonitoringMethod from "../../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
156
+ import ObjectID from "../../../Types/ObjectID";
157
+
158
+ const DESCRIPTIONS: Record<NetworkDeviceMetric, string> =
159
+ NETWORK_DEVICE_METRIC_DESCRIPTIONS;
160
+
161
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
162
+
163
+ /*
164
+ * The ids the Navigation and Project mocks hand out. Below the imports on
165
+ * purpose: the mock factories are hoisted above everything, but they only
166
+ * read these when a page asks, by which time both exist.
167
+ */
168
+ const mockDeviceObjectId: ObjectID = new ObjectID(DEVICE_ID);
169
+ const mockProjectObjectId: ObjectID = new ObjectID(
170
+ "10000000-0000-4000-8000-000000000001",
171
+ );
172
+
173
+ async function flush(): Promise<void> {
174
+ for (let i: number = 0; i < 5; i++) {
175
+ await act(async () => {
176
+ await Promise.resolve();
177
+ });
178
+ }
179
+ }
180
+
181
+ async function renderAndSettle(element: React.ReactElement): Promise<void> {
182
+ render(<MemoryRouter>{element}</MemoryRouter>);
183
+ await flush();
184
+ }
185
+
186
+ function infoButtonNames(): Array<string> {
187
+ return screen
188
+ .queryAllByRole("button", { name: /^About / })
189
+ .map((button: HTMLElement): string => {
190
+ return button.getAttribute("aria-label") || "";
191
+ });
192
+ }
193
+
194
+ /*
195
+ * Hover the (i) named "About <label>" and expect `text` among the tooltips
196
+ * tippy has mounted.
197
+ */
198
+ async function expectExplained(label: string, text: string): Promise<void> {
199
+ const button: HTMLElement = screen.getByRole("button", {
200
+ name: `About ${label}`,
201
+ });
202
+
203
+ fireEvent.mouseEnter(button);
204
+ await act(async () => {
205
+ jest.advanceTimersByTime(200);
206
+ });
207
+
208
+ expect(
209
+ screen.getAllByRole("tooltip").map((tooltip: HTMLElement): string => {
210
+ return tooltip.textContent || "";
211
+ }),
212
+ ).toContain(text);
213
+
214
+ fireEvent.mouseLeave(button);
215
+ await act(async () => {
216
+ jest.advanceTimersByTime(400);
217
+ });
218
+ }
219
+
220
+ function probeDevice(): NetworkDevice {
221
+ const now: Date = new Date();
222
+ const row: NetworkDevice = new NetworkDevice();
223
+ row.id = new ObjectID(DEVICE_ID);
224
+ row.name = "core-switch-01";
225
+ row.hostname = "10.0.0.1";
226
+ row.monitoringMethod = NetworkDeviceMonitoringMethod.Probe;
227
+ row.probeId = new ObjectID(PROBE_ID);
228
+ row.isPollingEnabled = true;
229
+ row.isReachable = true;
230
+ row.isSnmpReachable = true;
231
+ row.lastPolledAt = now;
232
+ row.lastSeenAt = now;
233
+ row.lastSnmpSeenAt = now;
234
+ row.lastRebootedAt = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
235
+ row.interfacesTotal = 48;
236
+ row.interfacesUp = 40;
237
+ row.interfacesDown = 3;
238
+
239
+ const status: MonitorStatus = new MonitorStatus();
240
+ status.name = "Operational";
241
+ status.color = Green;
242
+ status.isOfflineState = false;
243
+ row.currentMonitorStatus = status;
244
+
245
+ const probe: Probe = new Probe();
246
+ probe.name = "Rack 3";
247
+ row.probe = probe;
248
+
249
+ const site: NetworkSite = new NetworkSite();
250
+ site._id = "55555555-5555-4555-8555-555555555555";
251
+ site.name = "HQ";
252
+ row.site = site;
253
+
254
+ return row;
255
+ }
256
+
257
+ function monitorBackedDevice(): NetworkDevice {
258
+ const row: NetworkDevice = new NetworkDevice();
259
+ row.id = new ObjectID(DEVICE_ID);
260
+ row.name = "lobby-ap";
261
+ row.monitoringMethod = NetworkDeviceMonitoringMethod.Monitor;
262
+ return row;
263
+ }
264
+
265
+ beforeEach(() => {
266
+ jest.useFakeTimers();
267
+ getItemMock.mockReset();
268
+ getListMock.mockReset();
269
+ countMock.mockReset();
270
+ fetchDeviceSummaryMock.mockReset();
271
+ fetchNetworkOverviewMock.mockReset();
272
+ modelTableMock.mockReset();
273
+
274
+ getListMock.mockResolvedValue({ data: [], count: 0 });
275
+ countMock.mockResolvedValue(0);
276
+ });
277
+
278
+ afterEach(() => {
279
+ cleanup();
280
+ jest.useRealTimers();
281
+ });
282
+
283
+ // ---------------------------------------------------------------- the hero
284
+
285
+ describe("DeviceHeroTileTitle", () => {
286
+ test("with a description, the title gets an (i) named after it", async () => {
287
+ render(
288
+ <DeviceHeroTileTitle
289
+ title="Reachability"
290
+ description={DESCRIPTIONS.reachability}
291
+ />,
292
+ );
293
+
294
+ expect(screen.getByText("Reachability")).toBeInTheDocument();
295
+ await expectExplained("Reachability", DESCRIPTIONS.reachability);
296
+ });
297
+
298
+ test("without one, the title stands alone", () => {
299
+ render(<DeviceHeroTileTitle title="Site" />);
300
+
301
+ expect(screen.getByText("Site")).toBeInTheDocument();
302
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
303
+ });
304
+
305
+ test("the text is not in the page until someone reaches for it", () => {
306
+ render(
307
+ <DeviceHeroTileTitle
308
+ title="Hardware Uptime"
309
+ description={DESCRIPTIONS.hardwareUptime}
310
+ />,
311
+ );
312
+
313
+ expect(
314
+ screen.queryByText(DESCRIPTIONS.hardwareUptime),
315
+ ).not.toBeInTheDocument();
316
+ });
317
+ });
318
+
319
+ describe("the device Overview hero", () => {
320
+ const HERO_TILES: Array<[string, NetworkDeviceMetric]> = [
321
+ ["Reachability", "reachability"],
322
+ ["Monitor Status", "monitorStatus"],
323
+ ["Interfaces", "heroInterfaces"],
324
+ ["Hardware Uptime", "hardwareUptime"],
325
+ ];
326
+
327
+ test("the four metric tiles, and only they, carry an (i)", async () => {
328
+ getItemMock.mockResolvedValue(probeDevice());
329
+
330
+ await renderAndSettle(
331
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />,
332
+ );
333
+
334
+ expect(screen.getByTestId("device-status-hero")).toBeInTheDocument();
335
+ expect(infoButtonNames()).toEqual(
336
+ HERO_TILES.map(([title]: [string, NetworkDeviceMetric]): string => {
337
+ return `About ${title}`;
338
+ }),
339
+ );
340
+ });
341
+
342
+ test.each(HERO_TILES)(
343
+ "the %s tile's (i) reads NETWORK_DEVICE_METRIC_DESCRIPTIONS.%s",
344
+ async (title: string, key: NetworkDeviceMetric) => {
345
+ getItemMock.mockResolvedValue(probeDevice());
346
+
347
+ await renderAndSettle(
348
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />,
349
+ );
350
+
351
+ await expectExplained(title, DESCRIPTIONS[key]);
352
+ },
353
+ );
354
+
355
+ test("a monitor-backed device keeps every (i), even where the value is 'Not collected'", async () => {
356
+ getItemMock.mockResolvedValue(monitorBackedDevice());
357
+
358
+ await renderAndSettle(
359
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />,
360
+ );
361
+
362
+ expect(screen.getByText("Not collected")).toBeInTheDocument();
363
+ expect(infoButtonNames()).toHaveLength(4);
364
+ await expectExplained("Interfaces", DESCRIPTIONS.heroInterfaces);
365
+ });
366
+
367
+ test("the loading skeleton draws no titles, and so no (i)", async () => {
368
+ getItemMock.mockReturnValue(
369
+ new Promise<NetworkDevice>(() => {
370
+ // Never settles: the device read is still in flight.
371
+ }),
372
+ );
373
+
374
+ await renderAndSettle(
375
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />,
376
+ );
377
+
378
+ expect(
379
+ screen.getByTestId("device-status-hero-skeleton"),
380
+ ).toBeInTheDocument();
381
+ expect(infoButtonNames()).toEqual([]);
382
+ });
383
+
384
+ test("an (i) is never inside a link", async () => {
385
+ getItemMock.mockResolvedValue(probeDevice());
386
+
387
+ await renderAndSettle(
388
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />,
389
+ );
390
+
391
+ for (const button of screen.getAllByRole("button", { name: /^About / })) {
392
+ expect(button.closest("a")).toBeNull();
393
+ expect(button.parentElement?.closest("button")).toBeNull();
394
+ }
395
+ });
396
+ });
397
+
398
+ // ------------------------------------------------------ the summary strip
399
+
400
+ const SUMMARY_COUNTS: {
401
+ devicesUp: number;
402
+ devicesDown: number;
403
+ devicesPending: number;
404
+ interfacesDown: number;
405
+ totalDevices: number;
406
+ devicesWithoutSite: number;
407
+ } = {
408
+ devicesUp: 12,
409
+ devicesDown: 2,
410
+ devicesPending: 1,
411
+ interfacesDown: 7,
412
+ totalDevices: 15,
413
+ devicesWithoutSite: 3,
414
+ };
415
+
416
+ describe("the device list summary strip", () => {
417
+ test("every tile carries an (i), in tile order", async () => {
418
+ fetchDeviceSummaryMock.mockResolvedValue(SUMMARY_COUNTS);
419
+
420
+ await renderAndSettle(
421
+ <DeviceSummaryCards facetSelections={{}} facetOperators={{}} />,
422
+ );
423
+
424
+ expect(infoButtonNames()).toEqual(
425
+ DEVICE_SUMMARY_TILES.map((tile: DeviceSummaryTile): string => {
426
+ return `About ${tile.label}`;
427
+ }),
428
+ );
429
+ });
430
+
431
+ test.each(
432
+ DEVICE_SUMMARY_TILES.map((tile: DeviceSummaryTile): [string, string] => {
433
+ return [tile.label, tile.description];
434
+ }),
435
+ )(
436
+ "the %s tile's (i) reads its description",
437
+ async (label: string, text: string) => {
438
+ fetchDeviceSummaryMock.mockResolvedValue(SUMMARY_COUNTS);
439
+
440
+ await renderAndSettle(
441
+ <DeviceSummaryCards facetSelections={{}} facetOperators={{}} />,
442
+ );
443
+
444
+ await expectExplained(label, text);
445
+ },
446
+ );
447
+
448
+ test("the (i)s are there while the counts are still loading", async () => {
449
+ fetchDeviceSummaryMock.mockReturnValue(
450
+ new Promise<unknown>(() => {
451
+ // Never settles.
452
+ }),
453
+ );
454
+
455
+ await renderAndSettle(
456
+ <DeviceSummaryCards facetSelections={{}} facetOperators={{}} />,
457
+ );
458
+
459
+ expect(infoButtonNames()).toHaveLength(DEVICE_SUMMARY_TILES.length);
460
+ await expectExplained("Devices Down", DESCRIPTIONS.devicesDown);
461
+ });
462
+
463
+ test("asking what a tile means does not filter the list", async () => {
464
+ const onTileClick: MockFunction = getJestMockFunction();
465
+ fetchDeviceSummaryMock.mockResolvedValue(SUMMARY_COUNTS);
466
+
467
+ await renderAndSettle(
468
+ <DeviceSummaryCards
469
+ facetSelections={{}}
470
+ facetOperators={{}}
471
+ onTileClick={onTileClick}
472
+ />,
473
+ );
474
+
475
+ const info: HTMLElement = screen.getByRole("button", {
476
+ name: "About Devices Up",
477
+ });
478
+
479
+ fireEvent.click(info);
480
+ fireEvent.keyDown(info, { key: "Enter" });
481
+ fireEvent.keyDown(info, { key: " " });
482
+
483
+ expect(onTileClick).not.toHaveBeenCalled();
484
+
485
+ /*
486
+ * ...while the tile itself still does. Found by its accessible name (the
487
+ * card's ariaLabel), which is the card's own control whether InfoCard
488
+ * draws it as a role="button" card or as a button laid over the card.
489
+ */
490
+ fireEvent.click(
491
+ screen.getByRole("button", { name: /^Devices Up: 12\. Activate/ }),
492
+ );
493
+ expect(onTileClick).toHaveBeenCalledTimes(1);
494
+ expect(
495
+ (onTileClick.mock.calls[0]![0] as DeviceSummaryTile).description,
496
+ ).toBe(DESCRIPTIONS.devicesUp);
497
+ });
498
+ });
499
+
500
+ // ------------------------------------------------ the interface digest
501
+
502
+ function networkInterface(data: {
503
+ index: number;
504
+ name: string;
505
+ isUp: boolean;
506
+ utilization?: number | undefined;
507
+ }): NetworkInterface {
508
+ const row: NetworkInterface = new NetworkInterface();
509
+ row._id = `66666666-6666-4666-8666-00000000000${data.index}`;
510
+ row.interfaceIndex = data.index;
511
+ row.name = data.name;
512
+ row.isAdministrativelyUp = true;
513
+ row.isOperationallyUp = data.isUp;
514
+ row.inRateMbps = 12;
515
+ row.outRateMbps = 3;
516
+
517
+ if (data.utilization !== undefined) {
518
+ row.utilizationPercent = data.utilization;
519
+ }
520
+
521
+ return row;
522
+ }
523
+
524
+ describe("the device Overview's interface digest", () => {
525
+ test("the card title's (i) explains the bare per-port numbers", async () => {
526
+ getListMock.mockResolvedValue({
527
+ data: [
528
+ networkInterface({ index: 1, name: "Gi0/1", isUp: false }),
529
+ networkInterface({
530
+ index: 2,
531
+ name: "Gi0/2",
532
+ isUp: true,
533
+ utilization: 84,
534
+ }),
535
+ ],
536
+ count: 2,
537
+ });
538
+
539
+ await renderAndSettle(
540
+ <DeviceInterfacesPreview modelId={new ObjectID(DEVICE_ID)} />,
541
+ );
542
+
543
+ // The numbers the (i) has to explain: no header names them.
544
+ expect(screen.getAllByText("12 / 3 Mbps")).toHaveLength(2);
545
+ expect(screen.getByText("84%")).toBeInTheDocument();
546
+ expect(infoButtonNames()).toEqual(["About Interfaces"]);
547
+ await expectExplained("Interfaces", DESCRIPTIONS.interfacesPreview);
548
+ });
549
+
550
+ test("the (i) is on the card while it loads and when it is empty", async () => {
551
+ getListMock.mockResolvedValue({ data: [], count: 0 });
552
+
553
+ await renderAndSettle(
554
+ <DeviceInterfacesPreview modelId={new ObjectID(DEVICE_ID)} />,
555
+ );
556
+
557
+ expect(
558
+ screen.getByText("No interfaces discovered yet.", { exact: false }),
559
+ ).toBeInTheDocument();
560
+ expect(infoButtonNames()).toEqual(["About Interfaces"]);
561
+ });
562
+ });
563
+
564
+ // ------------------------------------------------- the Network Overview
565
+
566
+ function overviewSummary(): unknown {
567
+ return {
568
+ fleet: { total: 20, up: 15, down: 3, pending: 2, interfacesDown: 9 },
569
+ siteCount: 4,
570
+ unhealthySiteCount: 1,
571
+ endpointCount: 311,
572
+ vendors: [
573
+ { vendor: "Cisco", count: 12 },
574
+ { vendor: "Unknown", count: 8 },
575
+ ],
576
+ attentionDevices: [
577
+ {
578
+ id: DEVICE_ID,
579
+ name: "core-switch-01",
580
+ lastSeenAt: null,
581
+ interfacesDown: 0,
582
+ isDown: true,
583
+ isMonitorBacked: false,
584
+ isSnmpFailing: false,
585
+ },
586
+ ],
587
+ attentionSites: [],
588
+ };
589
+ }
590
+
591
+ describe("the Network Overview", () => {
592
+ const FLEET_CARDS: Array<[string, NetworkDeviceMetric]> = [
593
+ ["Devices", "fleetDevices"],
594
+ ["Interfaces Down", "fleetInterfacesDown"],
595
+ ["Sites", "fleetSites"],
596
+ ["Endpoints", "fleetEndpoints"],
597
+ ];
598
+
599
+ test("the four fleet cards and the vendor card carry an (i); the lists do not", async () => {
600
+ fetchNetworkOverviewMock.mockResolvedValue(overviewSummary());
601
+
602
+ await renderAndSettle(<NetworkOverview {...PAGE_PROPS} />);
603
+
604
+ expect(infoButtonNames()).toEqual([
605
+ ...FLEET_CARDS.map(([title]: [string, NetworkDeviceMetric]): string => {
606
+ return `About ${title}`;
607
+ }),
608
+ "About Fleet by vendor",
609
+ ]);
610
+ expect(screen.getByText("Devices needing attention")).toBeInTheDocument();
611
+ });
612
+
613
+ test.each(FLEET_CARDS)(
614
+ "the %s card's (i) reads NETWORK_DEVICE_METRIC_DESCRIPTIONS.%s",
615
+ async (title: string, key: NetworkDeviceMetric) => {
616
+ fetchNetworkOverviewMock.mockResolvedValue(overviewSummary());
617
+
618
+ await renderAndSettle(<NetworkOverview {...PAGE_PROPS} />);
619
+
620
+ await expectExplained(title, DESCRIPTIONS[key]);
621
+ },
622
+ );
623
+
624
+ test("the vendor card's (i) says only the top six are listed and what Unknown is", async () => {
625
+ fetchNetworkOverviewMock.mockResolvedValue(overviewSummary());
626
+
627
+ await renderAndSettle(<NetworkOverview {...PAGE_PROPS} />);
628
+
629
+ // "Unknown" is a real bar the (i) has to account for.
630
+ expect(screen.getByText("Unknown")).toBeInTheDocument();
631
+ await expectExplained("Fleet by vendor", DESCRIPTIONS.fleetByVendor);
632
+ });
633
+
634
+ test("the onboarding state shows no numbers, and no (i)", async () => {
635
+ fetchNetworkOverviewMock.mockResolvedValue({
636
+ fleet: { total: 0, up: 0, down: 0, pending: 0, interfacesDown: 0 },
637
+ siteCount: 0,
638
+ unhealthySiteCount: 0,
639
+ endpointCount: 0,
640
+ vendors: [],
641
+ attentionDevices: [],
642
+ attentionSites: [],
643
+ });
644
+
645
+ await renderAndSettle(<NetworkOverview {...PAGE_PROPS} />);
646
+
647
+ expect(
648
+ screen.getByText("Welcome to Network Monitoring"),
649
+ ).toBeInTheDocument();
650
+ expect(infoButtonNames()).toEqual([]);
651
+ });
652
+ });
653
+
654
+ // ---------------------------------------------------- the Interfaces tab
655
+
656
+ type CapturedColumn = {
657
+ title: string;
658
+ headerTooltip?: string | undefined;
659
+ };
660
+
661
+ describe("the Interfaces tab", () => {
662
+ function deviceCounts(): NetworkDevice {
663
+ const row: NetworkDevice = new NetworkDevice();
664
+ row.interfacesTotal = 52;
665
+ row.interfacesUp = 40;
666
+ row.interfacesDown = 4;
667
+ return row;
668
+ }
669
+
670
+ const COUNT_CARDS: Array<[string, NetworkDeviceMetric]> = [
671
+ ["Total Interfaces", "totalInterfaces"],
672
+ ["Up", "interfacesUp"],
673
+ ["Down", "interfacesDown"],
674
+ ["Monitored", "interfacesMonitored"],
675
+ ];
676
+
677
+ test("every count card carries an (i)", async () => {
678
+ getItemMock.mockResolvedValue(deviceCounts());
679
+ countMock.mockResolvedValue(50);
680
+
681
+ await renderAndSettle(<NetworkDeviceInterfaces {...PAGE_PROPS} />);
682
+
683
+ expect(screen.getByText("52")).toBeInTheDocument();
684
+ expect(infoButtonNames()).toEqual(
685
+ COUNT_CARDS.map(([title]: [string, NetworkDeviceMetric]): string => {
686
+ return `About ${title}`;
687
+ }),
688
+ );
689
+ });
690
+
691
+ test.each(COUNT_CARDS)(
692
+ "the %s card's (i) reads NETWORK_DEVICE_METRIC_DESCRIPTIONS.%s",
693
+ async (title: string, key: NetworkDeviceMetric) => {
694
+ getItemMock.mockResolvedValue(deviceCounts());
695
+ countMock.mockResolvedValue(50);
696
+
697
+ await renderAndSettle(<NetworkDeviceInterfaces {...PAGE_PROPS} />);
698
+
699
+ await expectExplained(title, DESCRIPTIONS[key]);
700
+ },
701
+ );
702
+
703
+ test("the metric columns carry their header tooltip; the rest carry none", async () => {
704
+ getItemMock.mockResolvedValue(deviceCounts());
705
+ countMock.mockResolvedValue(50);
706
+
707
+ await renderAndSettle(<NetworkDeviceInterfaces {...PAGE_PROPS} />);
708
+
709
+ expect(modelTableMock).toHaveBeenCalled();
710
+
711
+ const props: { columns: Array<CapturedColumn> } = modelTableMock.mock.calls[
712
+ modelTableMock.mock.calls.length - 1
713
+ ]![0] as {
714
+ columns: Array<CapturedColumn>;
715
+ };
716
+ const tooltips: Record<string, string | undefined> = {};
717
+
718
+ for (const column of props.columns) {
719
+ tooltips[column.title] = column.headerTooltip;
720
+ }
721
+
722
+ expect(tooltips).toEqual({
723
+ Index: undefined,
724
+ Name: undefined,
725
+ Status: DESCRIPTIONS.interfaceStatus,
726
+ "MAC Address": undefined,
727
+ "Speed (Mbps)": DESCRIPTIONS.interfaceSpeed,
728
+ "In / Out (Mbps)": DESCRIPTIONS.interfaceInOutRate,
729
+ Utilization: DESCRIPTIONS.interfaceUtilization,
730
+ "Errors / sec": DESCRIPTIONS.interfaceErrorsPerSecond,
731
+ Monitored: undefined,
732
+ });
733
+ });
734
+ });
735
+
736
+ // ------------------------------------------ the Overview's Inventory card
737
+
738
+ describe("the Inventory card's Uptime value", () => {
739
+ const THREE_DAYS_IN_MS: number = 3 * 24 * 60 * 60 * 1000;
740
+
741
+ test("carries an (i) named after the field, reading the Hardware Uptime text", async () => {
742
+ render(
743
+ <InventoryUptimeValue
744
+ lastRebootedAt={new Date(Date.now() - THREE_DAYS_IN_MS)}
745
+ />,
746
+ );
747
+
748
+ expect(infoButtonNames()).toEqual(["About Uptime"]);
749
+ await expectExplained("Uptime", DESCRIPTIONS.hardwareUptime);
750
+ });
751
+
752
+ test("is the same number as the hero's Hardware Uptime, so one text explains both", async () => {
753
+ const device: NetworkDevice = probeDevice();
754
+ const expected: string =
755
+ OneUptimeDate.differenceBetweenTwoDatesAsFromattedString(
756
+ device.lastRebootedAt as Date,
757
+ OneUptimeDate.getCurrentDate(),
758
+ );
759
+ getItemMock.mockResolvedValue(device);
760
+
761
+ await renderAndSettle(
762
+ <div>
763
+ <DeviceStatusHero modelId={new ObjectID(DEVICE_ID)} />
764
+ <InventoryUptimeValue lastRebootedAt={device.lastRebootedAt as Date} />
765
+ </div>,
766
+ );
767
+
768
+ // Fake timers hold the clock, so both read the same instant.
769
+ expect(screen.getAllByText(expected)).toHaveLength(2);
770
+ await expectExplained("Hardware Uptime", DESCRIPTIONS.hardwareUptime);
771
+ await expectExplained("Uptime", DESCRIPTIONS.hardwareUptime);
772
+ });
773
+
774
+ test("the (i) is beside the value, never inside a link or button", () => {
775
+ render(
776
+ <InventoryUptimeValue
777
+ lastRebootedAt={new Date(Date.now() - THREE_DAYS_IN_MS)}
778
+ />,
779
+ );
780
+
781
+ const button: HTMLElement = screen.getByRole("button", {
782
+ name: "About Uptime",
783
+ });
784
+
785
+ expect(button.closest("a")).toBeNull();
786
+ expect(button.parentElement?.closest("button")).toBeNull();
787
+ });
788
+ });
789
+
790
+ // ------------------------------------------------ the Probe Latency Matrix
791
+
792
+ describe("the Probe Latency Matrix", () => {
793
+ let postSpy: ReturnType<typeof jest.spyOn> | null = null;
794
+
795
+ function matrixResponse(): HTTPResponse<JSONObject> {
796
+ return new HTTPResponse<JSONObject>(
797
+ 200,
798
+ {
799
+ monitors: [{ id: "web", name: "Website" }],
800
+ probes: [{ id: "rack-3", name: "Rack 3" }],
801
+ cells: {
802
+ web: {
803
+ "rack-3": {
804
+ monitorId: "web",
805
+ probeId: "rack-3",
806
+ hasData: true,
807
+ latencyInMs: 42,
808
+ isOnline: true,
809
+ ageInSeconds: 30,
810
+ },
811
+ },
812
+ },
813
+ },
814
+ {},
815
+ );
816
+ }
817
+
818
+ beforeEach(() => {
819
+ postSpy = jest.spyOn(API, "post").mockResolvedValue(matrixResponse());
820
+ });
821
+
822
+ afterEach(() => {
823
+ postSpy?.mockRestore();
824
+ postSpy = null;
825
+ });
826
+
827
+ test("the card title carries the (i) that says what a cell is", async () => {
828
+ await renderAndSettle(<NetworkDeviceLatencyMatrix {...PAGE_PROPS} />);
829
+
830
+ // The bare number the (i) has to explain.
831
+ expect(screen.getByText("42 ms")).toBeInTheDocument();
832
+ expect(screen.getByText("Probe Latency Matrix")).toBeInTheDocument();
833
+ expect(infoButtonNames()).toEqual(["About Probe Latency Matrix"]);
834
+ await expectExplained("Probe Latency Matrix", DESCRIPTIONS.latencyMatrix);
835
+ });
836
+
837
+ test("asking what a cell means does not refetch the matrix", async () => {
838
+ await renderAndSettle(<NetworkDeviceLatencyMatrix {...PAGE_PROPS} />);
839
+
840
+ expect(postSpy).toHaveBeenCalledTimes(1);
841
+
842
+ const info: HTMLElement = screen.getByRole("button", {
843
+ name: "About Probe Latency Matrix",
844
+ });
845
+
846
+ fireEvent.click(info);
847
+ fireEvent.keyDown(info, { key: "Enter" });
848
+ await flush();
849
+
850
+ expect(postSpy).toHaveBeenCalledTimes(1);
851
+ });
852
+ });