@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,679 @@
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 site pages, RENDERED: the site Overview
23
+ * hero, the summary strip above the Sites list, the Status Timeline tab's
24
+ * uptime cards and daily strip, the Child Sites and Devices tabs' columns,
25
+ * and the Network Map's Sites section above the site cards.
26
+ *
27
+ * Only the network and the ModelTable are replaced. Every metric title is
28
+ * checked for an (i) whose tooltip is the matching
29
+ * NETWORK_SITE_METRIC_DESCRIPTIONS entry, Site Type (a name, not a number)
30
+ * for having none, and the clickable summary tiles for not activating when
31
+ * someone asks what the number means.
32
+ */
33
+
34
+ const SITE_ID: string = "55555555-5555-4555-8555-555555555555";
35
+ const OPERATIONAL_ID: string = "77777777-7777-4777-8777-000000000001";
36
+ const DEGRADED_ID: string = "77777777-7777-4777-8777-000000000002";
37
+ const MS_PER_DAY: number = 24 * 60 * 60 * 1000;
38
+
39
+ const getItemMock: MockFunction = getJestMockFunction();
40
+ const getListMock: MockFunction = getJestMockFunction();
41
+ const countMock: MockFunction = getJestMockFunction();
42
+ const fetchSiteSummaryMock: MockFunction = getJestMockFunction();
43
+ const fetchMaintenanceWindowsMock: MockFunction = getJestMockFunction();
44
+ const modelTableMock: MockFunction = getJestMockFunction();
45
+
46
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
47
+ return {
48
+ __esModule: true,
49
+ default: {
50
+ getItem: (...args: Array<unknown>): unknown => {
51
+ return getItemMock(...args);
52
+ },
53
+ getList: (...args: Array<unknown>): unknown => {
54
+ return getListMock(...args);
55
+ },
56
+ count: (...args: Array<unknown>): unknown => {
57
+ return countMock(...args);
58
+ },
59
+ getCommonHeaders: (): Record<string, string> => {
60
+ return {};
61
+ },
62
+ },
63
+ };
64
+ });
65
+
66
+ jest.mock(
67
+ "../../../../App/FeatureSet/Dashboard/src/Components/Network/NetworkSummaryApi",
68
+ () => {
69
+ return {
70
+ __esModule: true,
71
+ fetchSiteSummary: (...args: Array<unknown>): unknown => {
72
+ return fetchSiteSummaryMock(...args);
73
+ },
74
+ };
75
+ },
76
+ );
77
+
78
+ jest.mock(
79
+ "../../../../App/FeatureSet/Dashboard/src/Components/NetworkSite/SiteMaintenanceWindows",
80
+ () => {
81
+ const fetcher: (...args: Array<unknown>) => unknown = (
82
+ ...args: Array<unknown>
83
+ ): unknown => {
84
+ return fetchMaintenanceWindowsMock(...args);
85
+ };
86
+
87
+ return {
88
+ __esModule: true,
89
+ default: fetcher,
90
+ fetchSiteMaintenanceWindows: fetcher,
91
+ };
92
+ },
93
+ );
94
+
95
+ jest.mock("../../../UI/Utils/Navigation", () => {
96
+ return {
97
+ __esModule: true,
98
+ default: {
99
+ // Read at render time, long after the module-level id below exists.
100
+ getLastParamAsObjectID: (): unknown => {
101
+ return mockSiteObjectId;
102
+ },
103
+ navigate: (): void => {},
104
+ isOnThisPage: (): boolean => {
105
+ return false;
106
+ },
107
+ },
108
+ };
109
+ });
110
+
111
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
112
+ return {
113
+ __esModule: true,
114
+ default: (props: unknown) => {
115
+ modelTableMock(props);
116
+ return <div data-testid="model-table" />;
117
+ },
118
+ };
119
+ });
120
+
121
+ import SiteStatusHero, {
122
+ SiteHeroTileTitle,
123
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkSite/SiteStatusHero";
124
+ import SiteSummaryCards from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkSite/SiteSummaryCards";
125
+ import {
126
+ SITE_SUMMARY_TILES,
127
+ SiteSummaryTile,
128
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkSite/SiteSummaryTiles";
129
+ import {
130
+ NETWORK_SITE_METRIC_DESCRIPTIONS,
131
+ NetworkSiteMetric,
132
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/NetworkSiteMetricDescriptions";
133
+ import NetworkSiteStatusTimelinePage from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkSite/View/StatusTimeline";
134
+ import NetworkSiteChildSites from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkSite/View/ChildSites";
135
+ import NetworkSiteDevices from "../../../../App/FeatureSet/Dashboard/src/Pages/NetworkSite/View/Devices";
136
+ import NetworkMapSection from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkSite/NetworkMapSection";
137
+ import { NETWORK_DEVICE_METRIC_DESCRIPTIONS } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/NetworkDeviceMetricDescriptions";
138
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
139
+ import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
140
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
141
+ import NetworkEndpoint from "../../../Models/DatabaseModels/NetworkEndpoint";
142
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
143
+ import NetworkSiteStatusTimeline from "../../../Models/DatabaseModels/NetworkSiteStatusTimeline";
144
+ import { Green } from "../../../Types/BrandColors";
145
+ import NetworkDeviceMonitoringMethod from "../../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
146
+ import ObjectID from "../../../Types/ObjectID";
147
+
148
+ const DESCRIPTIONS: Record<NetworkSiteMetric, string> =
149
+ NETWORK_SITE_METRIC_DESCRIPTIONS;
150
+
151
+ // Handed out by the Navigation mock; below the imports so ObjectID exists.
152
+ const mockSiteObjectId: ObjectID = new ObjectID(SITE_ID);
153
+
154
+ const PAGE_PROPS: PageComponentProps = {} as PageComponentProps;
155
+
156
+ async function flush(): Promise<void> {
157
+ for (let i: number = 0; i < 5; i++) {
158
+ await act(async () => {
159
+ await Promise.resolve();
160
+ });
161
+ }
162
+ }
163
+
164
+ async function renderAndSettle(element: React.ReactElement): Promise<void> {
165
+ render(<MemoryRouter>{element}</MemoryRouter>);
166
+ await flush();
167
+ }
168
+
169
+ function infoButtonNames(): Array<string> {
170
+ return screen
171
+ .queryAllByRole("button", { name: /^About / })
172
+ .map((button: HTMLElement): string => {
173
+ return button.getAttribute("aria-label") || "";
174
+ });
175
+ }
176
+
177
+ async function expectExplained(label: string, text: string): Promise<void> {
178
+ const button: HTMLElement = screen.getByRole("button", {
179
+ name: `About ${label}`,
180
+ });
181
+
182
+ fireEvent.mouseEnter(button);
183
+ await act(async () => {
184
+ jest.advanceTimersByTime(200);
185
+ });
186
+
187
+ expect(
188
+ screen.getAllByRole("tooltip").map((tooltip: HTMLElement): string => {
189
+ return tooltip.textContent || "";
190
+ }),
191
+ ).toContain(text);
192
+
193
+ fireEvent.mouseLeave(button);
194
+ await act(async () => {
195
+ jest.advanceTimersByTime(400);
196
+ });
197
+ }
198
+
199
+ function status(id: string, isOperationalState: boolean): MonitorStatus {
200
+ const row: MonitorStatus = new MonitorStatus();
201
+ row._id = id;
202
+ row.name = isOperationalState ? "Operational" : "Degraded";
203
+ row.color = Green;
204
+ row.priority = isOperationalState ? 1 : 2;
205
+ row.isOperationalState = isOperationalState;
206
+ return row;
207
+ }
208
+
209
+ /*
210
+ * Healthy for the last ten days, except one full day (two to one days ago)
211
+ * spent Degraded.
212
+ */
213
+ function timelineRows(): Array<NetworkSiteStatusTimeline> {
214
+ const now: number = Date.now();
215
+ const make: (
216
+ startsAgo: number,
217
+ endsAgo: number | null,
218
+ isOperational: boolean,
219
+ ) => NetworkSiteStatusTimeline = (
220
+ startsAgo: number,
221
+ endsAgo: number | null,
222
+ isOperational: boolean,
223
+ ): NetworkSiteStatusTimeline => {
224
+ const row: NetworkSiteStatusTimeline = new NetworkSiteStatusTimeline();
225
+ row.startsAt = new Date(now - startsAgo * MS_PER_DAY);
226
+ // An open row (still the current status) has no end.
227
+ if (endsAgo !== null) {
228
+ row.endsAt = new Date(now - endsAgo * MS_PER_DAY);
229
+ }
230
+
231
+ row.monitorStatus = status(
232
+ isOperational ? OPERATIONAL_ID : DEGRADED_ID,
233
+ isOperational,
234
+ );
235
+ return row;
236
+ };
237
+
238
+ return [make(1, null, true), make(2, 1, false), make(10, 2, true)];
239
+ }
240
+
241
+ function site(): NetworkSite {
242
+ const row: NetworkSite = new NetworkSite();
243
+ row._id = SITE_ID;
244
+ row.currentMonitorStatus = status(OPERATIONAL_ID, true);
245
+ row.lastRollupAt = new Date();
246
+ return row;
247
+ }
248
+
249
+ function heroModelAPI(): void {
250
+ getItemMock.mockResolvedValue(site());
251
+ getListMock.mockImplementation((request: unknown): unknown => {
252
+ const modelType: unknown = (request as { modelType: unknown }).modelType;
253
+
254
+ if (modelType === NetworkDevice) {
255
+ return Promise.resolve({ data: [], count: 5 });
256
+ }
257
+
258
+ if (modelType === NetworkSiteStatusTimeline) {
259
+ return Promise.resolve({ data: timelineRows(), count: 3 });
260
+ }
261
+
262
+ return Promise.resolve({ data: [], count: 0 });
263
+ });
264
+ countMock.mockImplementation((request: unknown): unknown => {
265
+ const modelType: unknown = (request as { modelType: unknown }).modelType;
266
+
267
+ if (modelType === NetworkSite) {
268
+ return Promise.resolve(2);
269
+ }
270
+
271
+ if (modelType === NetworkEndpoint) {
272
+ return Promise.resolve(311);
273
+ }
274
+
275
+ return Promise.resolve(0);
276
+ });
277
+ }
278
+
279
+ beforeEach(() => {
280
+ jest.useFakeTimers();
281
+ getItemMock.mockReset();
282
+ getListMock.mockReset();
283
+ countMock.mockReset();
284
+ fetchSiteSummaryMock.mockReset();
285
+ fetchMaintenanceWindowsMock.mockReset();
286
+ modelTableMock.mockReset();
287
+
288
+ fetchMaintenanceWindowsMock.mockResolvedValue([]);
289
+ });
290
+
291
+ afterEach(() => {
292
+ cleanup();
293
+ jest.useRealTimers();
294
+ });
295
+
296
+ // ---------------------------------------------------------------- the hero
297
+
298
+ describe("SiteHeroTileTitle", () => {
299
+ test("with a description, the title gets an (i) named after it", async () => {
300
+ render(
301
+ <SiteHeroTileTitle title="Health" description={DESCRIPTIONS.health} />,
302
+ );
303
+
304
+ await expectExplained("Health", DESCRIPTIONS.health);
305
+ });
306
+
307
+ test("without one, the title stands alone", () => {
308
+ render(<SiteHeroTileTitle title="Site Type" />);
309
+
310
+ expect(screen.getByText("Site Type")).toBeInTheDocument();
311
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
312
+ });
313
+ });
314
+
315
+ describe("the site Overview hero", () => {
316
+ const HERO_TILES: Array<[string, NetworkSiteMetric]> = [
317
+ ["Health", "health"],
318
+ ["Uptime (24h)", "uptime24h"],
319
+ ["Uptime (30d)", "uptime30d"],
320
+ ["Devices", "devices"],
321
+ ["Child Sites", "childSites"],
322
+ ["Endpoints", "endpoints"],
323
+ ];
324
+
325
+ test("the six metric tiles, and not Site Type, carry an (i)", async () => {
326
+ heroModelAPI();
327
+
328
+ await renderAndSettle(<SiteStatusHero modelId={new ObjectID(SITE_ID)} />);
329
+
330
+ expect(screen.getByTestId("site-status-hero")).toBeInTheDocument();
331
+ expect(screen.getByText("Site Type")).toBeInTheDocument();
332
+ expect(infoButtonNames()).toEqual(
333
+ HERO_TILES.map(([title]: [string, NetworkSiteMetric]): string => {
334
+ return `About ${title}`;
335
+ }),
336
+ );
337
+ });
338
+
339
+ test.each(HERO_TILES)(
340
+ "the %s tile's (i) reads NETWORK_SITE_METRIC_DESCRIPTIONS.%s",
341
+ async (title: string, key: NetworkSiteMetric) => {
342
+ heroModelAPI();
343
+
344
+ await renderAndSettle(<SiteStatusHero modelId={new ObjectID(SITE_ID)} />);
345
+
346
+ await expectExplained(title, DESCRIPTIONS[key]);
347
+ },
348
+ );
349
+
350
+ test("the 30-day figure the (i) explains: one Degraded day in the window is downtime", async () => {
351
+ heroModelAPI();
352
+
353
+ await renderAndSettle(<SiteStatusHero modelId={new ObjectID(SITE_ID)} />);
354
+
355
+ /*
356
+ * Ten days of history, one of them Degraded, measured over 30 days: the
357
+ * twenty days before the first rollup count as up, so the tile reads
358
+ * 29/30 = 96.7% — the "about 3.3 points" and "counts as up" the (i)
359
+ * promises.
360
+ */
361
+ expect(screen.getByText("96.7%")).toBeInTheDocument();
362
+ expect(DESCRIPTIONS.uptime30d).toContain("about 3.3 points");
363
+ expect(DESCRIPTIONS.uptime30d).toContain(
364
+ "time before the first rollup counts as up",
365
+ );
366
+ });
367
+
368
+ test("the skeleton draws no titles, and so no (i)", async () => {
369
+ getItemMock.mockReturnValue(
370
+ new Promise<NetworkSite>(() => {
371
+ // Never settles.
372
+ }),
373
+ );
374
+ getListMock.mockReturnValue(
375
+ new Promise<unknown>(() => {
376
+ // Never settles.
377
+ }),
378
+ );
379
+ countMock.mockReturnValue(
380
+ new Promise<number>(() => {
381
+ // Never settles.
382
+ }),
383
+ );
384
+
385
+ await renderAndSettle(<SiteStatusHero modelId={new ObjectID(SITE_ID)} />);
386
+
387
+ expect(screen.getByTestId("site-status-hero-skeleton")).toBeInTheDocument();
388
+ expect(infoButtonNames()).toEqual([]);
389
+ });
390
+ });
391
+
392
+ // ------------------------------------------------------ the summary strip
393
+
394
+ const SITE_COUNTS: {
395
+ totalSites: number;
396
+ unhealthySites: number;
397
+ sitesWithNoData: number;
398
+ devicesWithoutSite: number;
399
+ unhealthyStatusIds: Array<string>;
400
+ } = {
401
+ totalSites: 12,
402
+ unhealthySites: 2,
403
+ sitesWithNoData: 3,
404
+ devicesWithoutSite: 9,
405
+ unhealthyStatusIds: [DEGRADED_ID],
406
+ };
407
+
408
+ describe("the Sites list summary strip", () => {
409
+ test("every tile carries an (i), in tile order", async () => {
410
+ fetchSiteSummaryMock.mockResolvedValue(SITE_COUNTS);
411
+
412
+ await renderAndSettle(
413
+ <SiteSummaryCards facetSelections={{}} facetOperators={{}} />,
414
+ );
415
+
416
+ expect(infoButtonNames()).toEqual(
417
+ SITE_SUMMARY_TILES.map((tile: SiteSummaryTile): string => {
418
+ return `About ${tile.label}`;
419
+ }),
420
+ );
421
+ });
422
+
423
+ test.each(
424
+ SITE_SUMMARY_TILES.map((tile: SiteSummaryTile): [string, string] => {
425
+ return [tile.label, tile.description];
426
+ }),
427
+ )(
428
+ "the %s tile's (i) reads its description",
429
+ async (label: string, text: string) => {
430
+ fetchSiteSummaryMock.mockResolvedValue(SITE_COUNTS);
431
+
432
+ await renderAndSettle(
433
+ <SiteSummaryCards facetSelections={{}} facetOperators={{}} />,
434
+ );
435
+
436
+ await expectExplained(label, text);
437
+ },
438
+ );
439
+
440
+ test("asking what a tile means does not activate it", async () => {
441
+ const onTileClick: MockFunction = getJestMockFunction();
442
+ fetchSiteSummaryMock.mockResolvedValue(SITE_COUNTS);
443
+
444
+ await renderAndSettle(
445
+ <SiteSummaryCards
446
+ facetSelections={{}}
447
+ facetOperators={{}}
448
+ onTileClick={onTileClick}
449
+ />,
450
+ );
451
+
452
+ const info: HTMLElement = screen.getByRole("button", {
453
+ name: "About Unassigned Devices",
454
+ });
455
+
456
+ fireEvent.click(info);
457
+ fireEvent.keyDown(info, { key: "Enter" });
458
+ fireEvent.keyDown(info, { key: " " });
459
+
460
+ expect(onTileClick).not.toHaveBeenCalled();
461
+
462
+ /*
463
+ * The tile itself still activates. Found by its accessible name, which
464
+ * is the card's own control whichever way InfoCard draws a clickable
465
+ * card.
466
+ */
467
+ fireEvent.click(
468
+ screen.getByRole("button", { name: /^Unassigned Devices: 9\. Activate/ }),
469
+ );
470
+ expect(onTileClick).toHaveBeenCalledTimes(1);
471
+ });
472
+ });
473
+
474
+ // ------------------------------------------------- Status Timeline tab
475
+
476
+ describe("the Status Timeline tab", () => {
477
+ const CARD_TITLES: Array<string> = [
478
+ "Uptime — Last 24 Hours",
479
+ "Uptime — Last 7 Days",
480
+ "Uptime — Last 30 Days",
481
+ "Uptime — Last 90 Days",
482
+ ];
483
+
484
+ function timelineModelAPI(): void {
485
+ getListMock.mockResolvedValue({ data: timelineRows(), count: 3 });
486
+ }
487
+
488
+ test("each uptime card and the daily strip's heading carry an (i)", async () => {
489
+ timelineModelAPI();
490
+
491
+ await renderAndSettle(<NetworkSiteStatusTimelinePage {...PAGE_PROPS} />);
492
+
493
+ expect(infoButtonNames()).toEqual([
494
+ ...CARD_TITLES.map((title: string): string => {
495
+ return `About ${title}`;
496
+ }),
497
+ "About Daily Uptime — Last 30 Days",
498
+ ]);
499
+ });
500
+
501
+ test.each(CARD_TITLES)(
502
+ "the %s card's (i) reads the window text",
503
+ async (title: string) => {
504
+ timelineModelAPI();
505
+
506
+ await renderAndSettle(<NetworkSiteStatusTimelinePage {...PAGE_PROPS} />);
507
+
508
+ await expectExplained(title, DESCRIPTIONS.uptimeWindow);
509
+ },
510
+ );
511
+
512
+ test("the daily strip's (i) is its legend", async () => {
513
+ timelineModelAPI();
514
+
515
+ await renderAndSettle(<NetworkSiteStatusTimelinePage {...PAGE_PROPS} />);
516
+
517
+ expect(screen.getByTestId("site-daily-uptime-strip")).toBeInTheDocument();
518
+ await expectExplained(
519
+ "Daily Uptime — Last 30 Days",
520
+ DESCRIPTIONS.dailyUptime,
521
+ );
522
+ });
523
+
524
+ test("with no history there is nothing to measure, and no (i)", async () => {
525
+ getListMock.mockResolvedValue({ data: [], count: 0 });
526
+
527
+ await renderAndSettle(<NetworkSiteStatusTimelinePage {...PAGE_PROPS} />);
528
+
529
+ expect(infoButtonNames()).toEqual([]);
530
+ });
531
+
532
+ test("the history table's Status column is a record of states, not a metric", async () => {
533
+ timelineModelAPI();
534
+
535
+ await renderAndSettle(<NetworkSiteStatusTimelinePage {...PAGE_PROPS} />);
536
+
537
+ const props: { columns: Array<{ title: string; headerTooltip?: string }> } =
538
+ modelTableMock.mock.calls[modelTableMock.mock.calls.length - 1]![0] as {
539
+ columns: Array<{ title: string; headerTooltip?: string }>;
540
+ };
541
+
542
+ for (const column of props.columns) {
543
+ expect({ title: column.title, tooltip: column.headerTooltip }).toEqual({
544
+ title: column.title,
545
+ tooltip: undefined,
546
+ });
547
+ }
548
+ });
549
+ });
550
+
551
+ // -------------------------------------------------------- Child Sites tab
552
+
553
+ describe("the Child Sites tab", () => {
554
+ test("the Status column explains the rollup; Name and Site Type do not", async () => {
555
+ await renderAndSettle(<NetworkSiteChildSites {...PAGE_PROPS} />);
556
+
557
+ expect(modelTableMock).toHaveBeenCalled();
558
+
559
+ const props: { columns: Array<{ title: string; headerTooltip?: string }> } =
560
+ modelTableMock.mock.calls[modelTableMock.mock.calls.length - 1]![0] as {
561
+ columns: Array<{ title: string; headerTooltip?: string }>;
562
+ };
563
+ const tooltips: Record<string, string | undefined> = {};
564
+
565
+ for (const column of props.columns) {
566
+ tooltips[column.title] = column.headerTooltip;
567
+ }
568
+
569
+ expect(tooltips).toEqual({
570
+ Name: undefined,
571
+ "Site Type": undefined,
572
+ Status: DESCRIPTIONS.childSiteStatus,
573
+ });
574
+ });
575
+ });
576
+
577
+ // ------------------------------------------------------------ Devices tab
578
+
579
+ describe("the site's Devices tab", () => {
580
+ test("Status and Interfaces explain themselves; the metadata columns do not", async () => {
581
+ await renderAndSettle(<NetworkSiteDevices {...PAGE_PROPS} />);
582
+
583
+ expect(modelTableMock).toHaveBeenCalled();
584
+
585
+ const props: { columns: Array<{ title: string; headerTooltip?: string }> } =
586
+ modelTableMock.mock.calls[modelTableMock.mock.calls.length - 1]![0] as {
587
+ columns: Array<{ title: string; headerTooltip?: string }>;
588
+ };
589
+ const tooltips: Record<string, string | undefined> = {};
590
+
591
+ for (const column of props.columns) {
592
+ tooltips[column.title] = column.headerTooltip;
593
+ }
594
+
595
+ expect(tooltips).toEqual({
596
+ Status: DESCRIPTIONS.siteDeviceStatus,
597
+ Name: undefined,
598
+ Hostname: undefined,
599
+ Vendor: undefined,
600
+ "Interfaces (Up / Down)":
601
+ NETWORK_DEVICE_METRIC_DESCRIPTIONS.deviceInterfacesUpDown,
602
+ "Last Seen": undefined,
603
+ });
604
+ });
605
+
606
+ test("a monitor-backed device the tab calls Up is what its Status text promises", async () => {
607
+ await renderAndSettle(<NetworkSiteDevices {...PAGE_PROPS} />);
608
+
609
+ const props: {
610
+ columns: Array<{
611
+ title: string;
612
+ getElement?: (item: NetworkDevice) => React.ReactElement;
613
+ }>;
614
+ } = modelTableMock.mock.calls[modelTableMock.mock.calls.length - 1]![0] as {
615
+ columns: Array<{
616
+ title: string;
617
+ getElement?: (item: NetworkDevice) => React.ReactElement;
618
+ }>;
619
+ };
620
+ const statusColumn:
621
+ | {
622
+ title: string;
623
+ getElement?: (item: NetworkDevice) => React.ReactElement;
624
+ }
625
+ | undefined = props.columns.find((column: { title: string }): boolean => {
626
+ return column.title === "Status";
627
+ });
628
+
629
+ // Bound to a monitor that is Degraded: not offline, so Up here.
630
+ const device: NetworkDevice = new NetworkDevice();
631
+ device.monitoringMethod = NetworkDeviceMonitoringMethod.Monitor;
632
+ device.monitorId = new ObjectID("88888888-8888-4888-8888-888888888888");
633
+ device.currentMonitorStatus = status(DEGRADED_ID, false);
634
+
635
+ render(statusColumn!.getElement!(device));
636
+
637
+ expect(screen.getByText("Up")).toBeInTheDocument();
638
+ expect(screen.queryByText("Degraded")).not.toBeInTheDocument();
639
+ expect(DESCRIPTIONS.siteDeviceStatus).toContain(
640
+ "Down only when its bound monitor reports it offline and Up otherwise",
641
+ );
642
+ });
643
+ });
644
+
645
+ // ------------------------------------------------ Network Map Sites band
646
+
647
+ describe("the Network Map's Sites section", () => {
648
+ test("with a description, the title gets an (i) that explains the cards", async () => {
649
+ render(
650
+ <NetworkMapSection
651
+ title="Sites"
652
+ count={12}
653
+ hint="Click a site to drill in."
654
+ description={DESCRIPTIONS.siteCards}
655
+ >
656
+ <div>cards</div>
657
+ </NetworkMapSection>,
658
+ );
659
+
660
+ expect(screen.getByText("12")).toBeInTheDocument();
661
+ expect(infoButtonNames()).toEqual(["About Sites"]);
662
+ await expectExplained("Sites", DESCRIPTIONS.siteCards);
663
+ });
664
+
665
+ test("without one (the WAN links, or no cards), it stands alone", () => {
666
+ render(
667
+ <NetworkMapSection
668
+ title="WAN links"
669
+ count={3}
670
+ hint="Connections between these top-level sites."
671
+ >
672
+ <div>links</div>
673
+ </NetworkMapSection>,
674
+ );
675
+
676
+ expect(screen.getByText("WAN links")).toBeInTheDocument();
677
+ expect(infoButtonNames()).toEqual([]);
678
+ });
679
+ });