@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -0,0 +1,348 @@
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
+ waitFor,
17
+ } from "@testing-library/react";
18
+ import React from "react";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+ import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
21
+ import InventoryItemRelationship from "../../../Models/DatabaseModels/InventoryItemRelationship";
22
+ import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
23
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
24
+ import ListResult from "../../../Types/BaseDatabase/ListResult";
25
+ import ObjectID from "../../../Types/ObjectID";
26
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
27
+ import TimeRange from "../../../Types/Time/TimeRange";
28
+ import useTopologyData from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/UseTopologyData";
29
+
30
+ const getListMock: MockFunction = getJestMockFunction();
31
+ const getProjectIdMock: MockFunction = getJestMockFunction();
32
+
33
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
34
+ return {
35
+ __esModule: true,
36
+ default: {
37
+ getList: (...args: Array<unknown>) => {
38
+ return getListMock(...args);
39
+ },
40
+ },
41
+ };
42
+ });
43
+
44
+ jest.mock("../../../UI/Utils/Project", () => {
45
+ return {
46
+ __esModule: true,
47
+ default: {
48
+ getCurrentProjectId: () => {
49
+ return getProjectIdMock();
50
+ },
51
+ },
52
+ };
53
+ });
54
+
55
+ jest.mock("../../../UI/Utils/API/API", () => {
56
+ return {
57
+ __esModule: true,
58
+ default: {
59
+ getFriendlyMessage: (error: unknown): string => {
60
+ return error instanceof Error
61
+ ? error.message
62
+ : "Unable to load topology";
63
+ },
64
+ },
65
+ };
66
+ });
67
+
68
+ const PROJECT_ID: ObjectID = new ObjectID(
69
+ "dde060c6-fe0d-49ce-b44c-4035a13bc1db",
70
+ );
71
+ const FIRST_RANGE: RangeStartAndEndDateTime = {
72
+ range: TimeRange.CUSTOM,
73
+ startAndEndDate: new InBetween<Date>(
74
+ new Date("2026-09-01T09:15:00.000Z"),
75
+ new Date("2026-09-01T10:15:00.000Z"),
76
+ ),
77
+ };
78
+ const SECOND_RANGE: RangeStartAndEndDateTime = {
79
+ range: TimeRange.CUSTOM,
80
+ startAndEndDate: new InBetween<Date>(
81
+ new Date("2026-09-02T09:15:00.000Z"),
82
+ new Date("2026-09-02T10:15:00.000Z"),
83
+ ),
84
+ };
85
+
86
+ type TopologyListResult = ListResult<InventoryItem | InventoryItemRelationship>;
87
+
88
+ interface DeferredResult {
89
+ promise: Promise<TopologyListResult>;
90
+ resolve: (result: TopologyListResult) => void;
91
+ reject: (error: Error) => void;
92
+ }
93
+
94
+ function deferred(): DeferredResult {
95
+ let resolve!: (result: TopologyListResult) => void;
96
+ let reject!: (error: Error) => void;
97
+ const promise: Promise<TopologyListResult> = new Promise(
98
+ (
99
+ resolvePromise: (result: TopologyListResult) => void,
100
+ rejectPromise: (error: Error) => void,
101
+ ): void => {
102
+ resolve = resolvePromise;
103
+ reject = rejectPromise;
104
+ },
105
+ );
106
+ return { promise, resolve, reject };
107
+ }
108
+
109
+ function result(name?: string, count?: number): TopologyListResult {
110
+ const data: Array<InventoryItem> = [];
111
+ if (name) {
112
+ const entity: InventoryItem = new InventoryItem();
113
+ entity.entityKey = name;
114
+ entity.displayName = name;
115
+ data.push(entity);
116
+ }
117
+ return { data, count: count ?? data.length, skip: 0, limit: 1000 };
118
+ }
119
+
120
+ function Probe(props: { range: RangeStartAndEndDateTime }): React.ReactElement {
121
+ const data: ReturnType<typeof useTopologyData> = useTopologyData(props.range);
122
+ return (
123
+ <div>
124
+ <span data-testid="loading">{String(data.isLoading)}</span>
125
+ <span data-testid="error">{data.error}</span>
126
+ <span data-testid="entities">
127
+ {data.entities
128
+ .map((entity: InventoryItem) => {
129
+ return entity.displayName;
130
+ })
131
+ .join(",")}
132
+ </span>
133
+ <span data-testid="relationships">{data.relationships.length}</span>
134
+ <span data-testid="truncated">{String(data.isTruncated)}</span>
135
+ <span data-testid="updated">
136
+ {data.lastUpdatedAt?.toISOString() || ""}
137
+ </span>
138
+ <button type="button" onClick={data.reload}>
139
+ Reload topology
140
+ </button>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ beforeEach(() => {
146
+ getListMock.mockReset();
147
+ getProjectIdMock.mockReset();
148
+ getProjectIdMock.mockReturnValue(PROJECT_ID);
149
+ getListMock.mockResolvedValue(result());
150
+ });
151
+
152
+ afterEach(() => {
153
+ cleanup();
154
+ jest.restoreAllMocks();
155
+ });
156
+
157
+ describe("topology inventory loading", () => {
158
+ test("loads the complete current catalog with a full-precision connection recency bound", async () => {
159
+ render(<Probe range={FIRST_RANGE} />);
160
+ await waitFor(() => {
161
+ expect(screen.getByTestId("loading")).toHaveTextContent("false");
162
+ });
163
+ expect(getListMock).toHaveBeenCalledTimes(2);
164
+ const entityRequest: { query: Record<string, unknown> } =
165
+ getListMock.mock.calls[0]![0];
166
+ const relationshipRequest: { query: Record<string, unknown> } =
167
+ getListMock.mock.calls[1]![0];
168
+ expect(entityRequest.query["projectId"]).toEqual(PROJECT_ID);
169
+ expect(entityRequest.query["isArchived"]).toBe(false);
170
+ expect(entityRequest.query).not.toHaveProperty("lastSeenAt");
171
+ expect(relationshipRequest.query["projectId"]).toEqual(PROJECT_ID);
172
+ expect(relationshipRequest.query["lastSeenAt"]).toEqual(
173
+ new GreaterThanOrEqual<Date>(FIRST_RANGE.startAndEndDate!.startValue),
174
+ );
175
+ expect(screen.getByTestId("updated").textContent).not.toBe("");
176
+ });
177
+
178
+ test("only publishes a snapshot after inventory and relationships both finish", async () => {
179
+ const relationships: DeferredResult = deferred();
180
+ getListMock.mockResolvedValueOnce(result("Checkout API"));
181
+ getListMock.mockReturnValueOnce(relationships.promise);
182
+ render(<Probe range={FIRST_RANGE} />);
183
+ await act(async () => {
184
+ await Promise.resolve();
185
+ });
186
+ expect(screen.getByTestId("loading")).toHaveTextContent("true");
187
+ expect(screen.getByTestId("entities")).toBeEmptyDOMElement();
188
+ await act(async () => {
189
+ relationships.resolve(result());
190
+ });
191
+ expect(screen.getByTestId("entities")).toHaveTextContent("Checkout API");
192
+ expect(screen.getByTestId("loading")).toHaveTextContent("false");
193
+ });
194
+
195
+ test("an older range finishing last cannot replace the selected range", async () => {
196
+ const oldEntities: DeferredResult = deferred();
197
+ const oldRelationships: DeferredResult = deferred();
198
+ getListMock.mockReturnValueOnce(oldEntities.promise);
199
+ getListMock.mockReturnValueOnce(oldRelationships.promise);
200
+ const view: ReturnType<typeof render> = render(
201
+ <Probe range={FIRST_RANGE} />,
202
+ );
203
+ getListMock.mockResolvedValueOnce(result("Latest range"));
204
+ getListMock.mockResolvedValueOnce(result());
205
+ view.rerender(<Probe range={SECOND_RANGE} />);
206
+ await waitFor(() => {
207
+ expect(screen.getByTestId("entities")).toHaveTextContent("Latest range");
208
+ });
209
+ await act(async () => {
210
+ oldEntities.resolve(result("Obsolete range", 100));
211
+ oldRelationships.resolve(result());
212
+ });
213
+ expect(screen.getByTestId("entities")).toHaveTextContent("Latest range");
214
+ expect(screen.getByTestId("truncated")).toHaveTextContent("false");
215
+ });
216
+
217
+ test("an obsolete failure cannot clear the current loading state or show an error", async () => {
218
+ const oldEntities: DeferredResult = deferred();
219
+ const latestEntities: DeferredResult = deferred();
220
+ getListMock.mockReturnValueOnce(oldEntities.promise);
221
+ getListMock.mockResolvedValueOnce(result());
222
+ const view: ReturnType<typeof render> = render(
223
+ <Probe range={FIRST_RANGE} />,
224
+ );
225
+ getListMock.mockReturnValueOnce(latestEntities.promise);
226
+ getListMock.mockResolvedValueOnce(result());
227
+ view.rerender(<Probe range={SECOND_RANGE} />);
228
+ await act(async () => {
229
+ oldEntities.reject(new Error("An old request failed"));
230
+ });
231
+ expect(screen.getByTestId("loading")).toHaveTextContent("true");
232
+ expect(screen.getByTestId("error")).toBeEmptyDOMElement();
233
+ await act(async () => {
234
+ latestEntities.resolve(result("Current inventory"));
235
+ });
236
+ expect(screen.getByTestId("entities")).toHaveTextContent(
237
+ "Current inventory",
238
+ );
239
+ expect(screen.getByTestId("error")).toBeEmptyDOMElement();
240
+ });
241
+
242
+ test.each(["inventory", "relationships"])(
243
+ "reports a failed %s request without presenting a partial graph, then retries",
244
+ async (failedRequest: string) => {
245
+ if (failedRequest === "inventory") {
246
+ getListMock.mockRejectedValueOnce(new Error("Please retry"));
247
+ getListMock.mockResolvedValueOnce(result());
248
+ } else {
249
+ getListMock.mockResolvedValueOnce(result("Partial inventory"));
250
+ getListMock.mockRejectedValueOnce(new Error("Please retry"));
251
+ }
252
+ render(<Probe range={FIRST_RANGE} />);
253
+ await waitFor(() => {
254
+ expect(screen.getByTestId("error")).toHaveTextContent("Please retry");
255
+ });
256
+ expect(screen.getByTestId("loading")).toHaveTextContent("false");
257
+ expect(screen.getByTestId("entities")).toBeEmptyDOMElement();
258
+ expect(screen.getByTestId("updated")).toBeEmptyDOMElement();
259
+ getListMock.mockResolvedValueOnce(result("Recovered inventory"));
260
+ getListMock.mockResolvedValueOnce(result());
261
+ fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
262
+ await waitFor(() => {
263
+ expect(screen.getByTestId("entities")).toHaveTextContent(
264
+ "Recovered inventory",
265
+ );
266
+ });
267
+ expect(screen.getByTestId("error")).toBeEmptyDOMElement();
268
+ expect(getListMock).toHaveBeenCalledTimes(4);
269
+ },
270
+ );
271
+
272
+ test.each(["inventory", "relationships"])(
273
+ "flags a truncated %s response and clears the warning after a complete reload",
274
+ async (truncatedRequest: string) => {
275
+ getListMock.mockResolvedValueOnce(
276
+ result("API", truncatedRequest === "inventory" ? 2000 : 1),
277
+ );
278
+ getListMock.mockResolvedValueOnce(
279
+ result(undefined, truncatedRequest === "relationships" ? 2000 : 0),
280
+ );
281
+ render(<Probe range={FIRST_RANGE} />);
282
+ await waitFor(() => {
283
+ expect(screen.getByTestId("truncated")).toHaveTextContent("true");
284
+ });
285
+ fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
286
+ await waitFor(() => {
287
+ expect(screen.getByTestId("loading")).toHaveTextContent("false");
288
+ });
289
+ expect(screen.getByTestId("truncated")).toHaveTextContent("false");
290
+ },
291
+ );
292
+
293
+ test("changing projects refetches and ignores the previous project's response", async () => {
294
+ const oldEntities: DeferredResult = deferred();
295
+ getListMock.mockReturnValueOnce(oldEntities.promise);
296
+ getListMock.mockResolvedValueOnce(result());
297
+ const view: ReturnType<typeof render> = render(
298
+ <Probe range={FIRST_RANGE} />,
299
+ );
300
+ const nextProject: ObjectID = new ObjectID(
301
+ "22222222-2222-4222-8222-222222222222",
302
+ );
303
+ getProjectIdMock.mockReturnValue(nextProject);
304
+ getListMock.mockResolvedValueOnce(result("Project two inventory"));
305
+ getListMock.mockResolvedValueOnce(result());
306
+ view.rerender(<Probe range={FIRST_RANGE} />);
307
+ await waitFor(() => {
308
+ expect(screen.getByTestId("entities")).toHaveTextContent(
309
+ "Project two inventory",
310
+ );
311
+ });
312
+ expect(getListMock.mock.calls[2]![0].query.projectId).toEqual(nextProject);
313
+ await act(async () => {
314
+ oldEntities.resolve(result("Project one inventory"));
315
+ });
316
+ expect(screen.getByTestId("entities")).toHaveTextContent(
317
+ "Project two inventory",
318
+ );
319
+ });
320
+
321
+ test("never sends an unscoped inventory request without a project", async () => {
322
+ getProjectIdMock.mockReturnValue(null);
323
+ render(<Probe range={FIRST_RANGE} />);
324
+ await waitFor(() => {
325
+ expect(screen.getByTestId("loading")).toHaveTextContent("false");
326
+ });
327
+ expect(getListMock).not.toHaveBeenCalled();
328
+ expect(screen.getByTestId("error")).toHaveTextContent("Select a project");
329
+ });
330
+
331
+ test("a retry supersedes a still-pending request for the same range", async () => {
332
+ const firstAttempt: DeferredResult = deferred();
333
+ getListMock.mockReturnValueOnce(firstAttempt.promise);
334
+ getListMock.mockResolvedValueOnce(result());
335
+ render(<Probe range={FIRST_RANGE} />);
336
+ getListMock.mockResolvedValueOnce(result("Retry result"));
337
+ getListMock.mockResolvedValueOnce(result());
338
+ fireEvent.click(screen.getByRole("button", { name: "Reload topology" }));
339
+ await waitFor(() => {
340
+ expect(screen.getByTestId("entities")).toHaveTextContent("Retry result");
341
+ });
342
+ await act(async () => {
343
+ firstAttempt.reject(new Error("Original attempt failed"));
344
+ });
345
+ expect(screen.getByTestId("entities")).toHaveTextContent("Retry result");
346
+ expect(screen.getByTestId("error")).toBeEmptyDOMElement();
347
+ });
348
+ });
@@ -0,0 +1,331 @@
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
+ cleanup,
12
+ fireEvent,
13
+ render,
14
+ screen,
15
+ waitFor,
16
+ } from "@testing-library/react";
17
+ import React from "react";
18
+ import { MemoryRouter } from "react-router-dom";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+ import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
21
+ import Route from "../../../Types/API/Route";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import EntityType from "../../../Types/Telemetry/EntityType";
24
+ import TopologyPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Topology/TopologyPage";
25
+
26
+ const getListMock: MockFunction = getJestMockFunction();
27
+
28
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
29
+ return {
30
+ __esModule: true,
31
+ default: {
32
+ getList: (...args: Array<unknown>) => {
33
+ return getListMock(...args);
34
+ },
35
+ },
36
+ };
37
+ });
38
+
39
+ jest.mock("../../../UI/Utils/Project", () => {
40
+ return {
41
+ __esModule: true,
42
+ default: {
43
+ getCurrentProjectId: () => {
44
+ return new ObjectID("dde060c6-fe0d-49ce-b44c-4035a13bc1db");
45
+ },
46
+ },
47
+ };
48
+ });
49
+
50
+ jest.mock("../../../UI/Utils/API/API", () => {
51
+ return {
52
+ __esModule: true,
53
+ default: {
54
+ getFriendlyMessage: (error: unknown): string => {
55
+ return error instanceof Error
56
+ ? error.message
57
+ : "Unable to load topology";
58
+ },
59
+ },
60
+ };
61
+ });
62
+
63
+ jest.mock("react-i18next", () => {
64
+ return {
65
+ useTranslation: () => {
66
+ return {
67
+ t: (value: string): string => {
68
+ return value;
69
+ },
70
+ };
71
+ },
72
+ };
73
+ });
74
+
75
+ jest.mock("../../../UI/Components/Page/Page", () => {
76
+ return {
77
+ __esModule: true,
78
+ default: (props: { children: React.ReactNode }): React.ReactElement => {
79
+ return <main>{props.children}</main>;
80
+ },
81
+ };
82
+ });
83
+
84
+ jest.mock(
85
+ "../../../UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker",
86
+ () => {
87
+ return {
88
+ __esModule: true,
89
+ default: (): React.ReactElement => {
90
+ return (
91
+ <div data-testid="topology-time-picker">Connection time range</div>
92
+ );
93
+ },
94
+ };
95
+ },
96
+ );
97
+
98
+ jest.mock(
99
+ "../../../../App/FeatureSet/Dashboard/src/Components/Topology/ServiceMapGraph",
100
+ () => {
101
+ return {
102
+ __esModule: true,
103
+ default: (props: {
104
+ entities: Array<InventoryItem>;
105
+ }): React.ReactElement => {
106
+ return (
107
+ <div data-testid="topology-services">
108
+ Services:{" "}
109
+ {props.entities
110
+ .map((entity: InventoryItem): string => {
111
+ return entity.displayName || "";
112
+ })
113
+ .join(",")}
114
+ </div>
115
+ );
116
+ },
117
+ };
118
+ },
119
+ );
120
+
121
+ jest.mock(
122
+ "../../../../App/FeatureSet/Dashboard/src/Components/Topology/InfrastructureGraph",
123
+ () => {
124
+ return {
125
+ __esModule: true,
126
+ default: (props: {
127
+ entities: Array<InventoryItem>;
128
+ }): React.ReactElement => {
129
+ return (
130
+ <div data-testid="topology-infrastructure">
131
+ Infrastructure:{" "}
132
+ {props.entities
133
+ .map((entity: InventoryItem): string => {
134
+ return entity.displayName || "";
135
+ })
136
+ .join(",")}
137
+ </div>
138
+ );
139
+ },
140
+ };
141
+ },
142
+ );
143
+
144
+ jest.mock(
145
+ "../../../../App/FeatureSet/Dashboard/src/Components/Topology/InfrastructureExplorer",
146
+ () => {
147
+ return {
148
+ __esModule: true,
149
+ default: (props: {
150
+ entities: Array<InventoryItem>;
151
+ }): React.ReactElement => {
152
+ return (
153
+ <div data-testid="topology-infrastructure">
154
+ Infrastructure:{" "}
155
+ {props.entities
156
+ .map((entity: InventoryItem): string => {
157
+ return entity.displayName || "";
158
+ })
159
+ .join(",")}
160
+ </div>
161
+ );
162
+ },
163
+ };
164
+ },
165
+ { virtual: true },
166
+ );
167
+
168
+ jest.mock(
169
+ "../../../../App/FeatureSet/Dashboard/src/Components/Topology/NetworkTopologyExplorer",
170
+ () => {
171
+ return {
172
+ __esModule: true,
173
+ default: (): React.ReactElement => {
174
+ return <div data-testid="topology-network">Live network discovery</div>;
175
+ },
176
+ };
177
+ },
178
+ );
179
+
180
+ function renderPage(): void {
181
+ render(
182
+ <MemoryRouter>
183
+ <TopologyPage
184
+ pageRoute={new Route("/topology/overview")}
185
+ currentProject={null}
186
+ hasPaymentMethod={true}
187
+ />
188
+ </MemoryRouter>,
189
+ );
190
+ }
191
+
192
+ beforeEach(() => {
193
+ getListMock.mockReset();
194
+ getListMock.mockResolvedValue({ data: [], count: 0, skip: 0, limit: 1000 });
195
+ window.history.replaceState({}, "", "/dashboard/project/topology/overview");
196
+ });
197
+
198
+ afterEach(() => {
199
+ cleanup();
200
+ jest.restoreAllMocks();
201
+ });
202
+
203
+ describe("topology map navigation and data isolation", () => {
204
+ test("opens live Network while telemetry requests are still pending", async () => {
205
+ getListMock.mockReturnValue(new Promise(() => {}));
206
+ renderPage();
207
+ fireEvent.click(await screen.findByRole("tab", { name: "Network" }));
208
+ expect(screen.getByTestId("topology-network")).toBeVisible();
209
+ expect(
210
+ screen.queryByTestId("topology-time-picker"),
211
+ ).not.toBeInTheDocument();
212
+ expect(new URLSearchParams(window.location.search).get("tab")).toBe(
213
+ "Network",
214
+ );
215
+ });
216
+
217
+ test("telemetry failures stay inside their map tabs and can be retried", async () => {
218
+ getListMock.mockRejectedValue(
219
+ new Error("Telemetry is temporarily unavailable"),
220
+ );
221
+ renderPage();
222
+ await screen.findByText("Telemetry is temporarily unavailable");
223
+ fireEvent.click(screen.getByRole("tab", { name: "Network" }));
224
+ expect(screen.getByTestId("topology-network")).toBeVisible();
225
+ expect(
226
+ screen.queryByText("Telemetry is temporarily unavailable"),
227
+ ).not.toBeInTheDocument();
228
+ fireEvent.click(screen.getByRole("tab", { name: "Infrastructure" }));
229
+ expect(
230
+ screen.getByText("Telemetry is temporarily unavailable"),
231
+ ).toBeVisible();
232
+ getListMock.mockResolvedValue({ data: [], count: 0, skip: 0, limit: 1000 });
233
+ fireEvent.click(screen.getByRole("button", { name: "Try again" }));
234
+ expect(await screen.findByTestId("topology-infrastructure")).toBeVisible();
235
+ expect(
236
+ screen.queryByText("Telemetry is temporarily unavailable"),
237
+ ).not.toBeInTheDocument();
238
+ });
239
+
240
+ test("Service Map and Infrastructure share the same loaded catalog without refetching on a tab change", async () => {
241
+ const entity: InventoryItem = new InventoryItem();
242
+ entity.entityKey = "checkout";
243
+ entity.displayName = "Checkout API";
244
+ entity.entityType = EntityType.Service;
245
+ getListMock.mockResolvedValueOnce({
246
+ data: [entity],
247
+ count: 1,
248
+ skip: 0,
249
+ limit: 1000,
250
+ });
251
+ getListMock.mockResolvedValueOnce({
252
+ data: [],
253
+ count: 0,
254
+ skip: 0,
255
+ limit: 1000,
256
+ });
257
+ renderPage();
258
+ expect(await screen.findByTestId("topology-services")).toHaveTextContent(
259
+ "Checkout API",
260
+ );
261
+ fireEvent.click(screen.getByRole("tab", { name: "Infrastructure" }));
262
+ expect(screen.getByTestId("topology-infrastructure")).toHaveTextContent(
263
+ "Checkout API",
264
+ );
265
+ fireEvent.click(screen.getByRole("tab", { name: "Service Map" }));
266
+ expect(screen.getByTestId("topology-services")).toHaveTextContent(
267
+ "Checkout API",
268
+ );
269
+ expect(getListMock).toHaveBeenCalledTimes(2);
270
+ });
271
+
272
+ test.each(["Infrastructure", "Network"])(
273
+ "restores a shared %s link on the first render",
274
+ async (tab: string) => {
275
+ window.history.replaceState({}, "", `?tab=${encodeURIComponent(tab)}`);
276
+ renderPage();
277
+ await waitFor(() => {
278
+ expect(screen.getByRole("tab", { name: tab })).toHaveAttribute(
279
+ "aria-selected",
280
+ "true",
281
+ );
282
+ });
283
+ if (tab === "Network") {
284
+ expect(screen.getByTestId("topology-network")).toBeVisible();
285
+ expect(
286
+ screen.queryByTestId("topology-time-picker"),
287
+ ).not.toBeInTheDocument();
288
+ } else {
289
+ expect(
290
+ await screen.findByTestId("topology-infrastructure"),
291
+ ).toBeVisible();
292
+ expect(screen.getByTestId("topology-time-picker")).toBeVisible();
293
+ }
294
+ },
295
+ );
296
+
297
+ test("an invalid tab falls back to Service Map and preserves both maps' search state", async () => {
298
+ window.history.replaceState(
299
+ {},
300
+ "",
301
+ "?tab=Missing&search=checkout&infraSearch=worker",
302
+ );
303
+ renderPage();
304
+ expect(await screen.findByTestId("topology-services")).toBeVisible();
305
+ fireEvent.click(screen.getByRole("tab", { name: "Infrastructure" }));
306
+ fireEvent.click(screen.getByRole("tab", { name: "Service Map" }));
307
+ const query: URLSearchParams = new URLSearchParams(window.location.search);
308
+ expect(query.has("tab")).toBe(false);
309
+ expect(query.get("search")).toBe("checkout");
310
+ expect(query.get("infraSearch")).toBe("worker");
311
+ });
312
+
313
+ test("keyboard navigation switches maps and updates the shareable tab", async () => {
314
+ renderPage();
315
+ await screen.findByTestId("topology-services");
316
+ const serviceTab: HTMLElement = screen.getByRole("tab", {
317
+ name: "Service Map",
318
+ });
319
+ serviceTab.focus();
320
+ fireEvent.keyDown(serviceTab, { key: "ArrowRight" });
321
+ await waitFor(() => {
322
+ expect(
323
+ screen.getByRole("tab", { name: "Infrastructure" }),
324
+ ).toHaveAttribute("aria-selected", "true");
325
+ });
326
+ expect(new URLSearchParams(window.location.search).get("tab")).toBe(
327
+ "Infrastructure",
328
+ );
329
+ expect(screen.getByRole("tab", { name: "Infrastructure" })).toHaveFocus();
330
+ });
331
+ });