@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,514 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render, screen, within } from "@testing-library/react";
3
+ import * as React from "react";
4
+ import { describe, expect, it, jest } from "@jest/globals";
5
+ import ReplayScrubber, {
6
+ ReplayScrubberProps,
7
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayScrubber";
8
+ import ReplayHeader, {
9
+ ReplayHeaderProps,
10
+ ReplayHeaderTab,
11
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayHeader";
12
+ import ReplayRailRow, {
13
+ ReplayRailRowProps,
14
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Rail/ReplayRailRow";
15
+ import { ReplayRailRowModel } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Rail/ReplayRailTabs";
16
+ import { ReplaySignal } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Rail/ReplaySignalTypes";
17
+ import {
18
+ ReplayEngineSnapshot,
19
+ derivePhase,
20
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Engine/ReplayEngineTypes";
21
+ import { REPLAY_CONTROL_HEIGHT_CLASS } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayUi";
22
+
23
+ /*
24
+ * The design decisions of the player redesign, held across components.
25
+ *
26
+ * ReplayUi.test.tsx covers the primitives in isolation. What is pinned
27
+ * HERE is the handful of structural choices that no single component
28
+ * owns and that a well-meaning edit to one file would silently undo:
29
+ * the order the player is stacked in, the fact that the shell (not each
30
+ * part) draws the card, one control height across a whole toolbar, and
31
+ * the typographic hierarchy that stops the header and the rail reading
32
+ * as undifferentiated 11px soup.
33
+ *
34
+ * These are structure, not taste. Nothing here asserts a colour or a
35
+ * padding value; every assertion names a rule that has a reason.
36
+ */
37
+
38
+ const DURATION_MS: number = 600000;
39
+
40
+ function makeSnapshot(
41
+ overrides?: Partial<ReplayEngineSnapshot>,
42
+ ): ReplayEngineSnapshot {
43
+ const buffer: ReplayEngineSnapshot["buffer"] = overrides?.buffer ?? "ok";
44
+ const intent: ReplayEngineSnapshot["intent"] = overrides?.intent ?? "paused";
45
+
46
+ return {
47
+ phase: derivePhase(buffer, intent),
48
+ intent: intent,
49
+ buffer: buffer,
50
+ currentTimeMs: 12000,
51
+ durationMs: DURATION_MS,
52
+ speed: 1,
53
+ skipInactive: false,
54
+ fedRange: { fromMs: 0, toMs: 60000 },
55
+ loadedChunkIndexes: [0, 1, 2, 3],
56
+ activeTabId: "tab-1",
57
+ recordedSize: null,
58
+ bufferingSinceMs: null,
59
+ lastGap: null,
60
+ lastIdleSkip: null,
61
+ error: null,
62
+ pendingSeekMs: null,
63
+ generation: 1,
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ function noop(): void {
69
+ /* Handlers these assertions do not exercise. */
70
+ }
71
+
72
+ function makeScrubberProps(
73
+ overrides?: Partial<ReplayScrubberProps>,
74
+ ): ReplayScrubberProps {
75
+ return {
76
+ snapshot: makeSnapshot(),
77
+ bands: [],
78
+ markers: [],
79
+ onSeek: noop,
80
+ onPlayPause: noop,
81
+ onSpeedChange: noop,
82
+ onSkipInactiveChange: noop,
83
+ ...overrides,
84
+ };
85
+ }
86
+
87
+ function makeHeaderProps(
88
+ overrides?: Partial<ReplayHeaderProps>,
89
+ ): ReplayHeaderProps {
90
+ const tab: ReplayHeaderTab = {
91
+ tabId: "tab-1",
92
+ label: "Tab 1",
93
+ durationMs: 252000,
94
+ openedAtMs: 0,
95
+ hasFootage: true,
96
+ isActive: true,
97
+ };
98
+
99
+ return {
100
+ sessionId: "a1b2c3d4e5f60718293a4b5c6d7e8f90",
101
+ backHref: "/dashboard/p/rum/a/session-replay",
102
+ onBack: jest.fn(),
103
+ identity: { label: "jane@acme.com", traits: { plan: "pro" } },
104
+ facts: [
105
+ { label: "Browser", value: "Chrome 126" },
106
+ { label: "OS", value: "macOS" },
107
+ { label: "Viewport", value: "1440x900" },
108
+ ],
109
+ startTimeUnixMs: new Date(2026, 8, 4, 10, 12, 41, 200).getTime(),
110
+ currentTimeMs: 41200,
111
+ durationMs: 252000,
112
+ isLive: false,
113
+ tabs: [tab],
114
+ onSwitchTab: jest.fn(),
115
+ isWide: false,
116
+ onToggleWide: jest.fn(),
117
+ isTheater: false,
118
+ onToggleTheater: jest.fn(),
119
+ onOpenDetails: jest.fn(),
120
+ buildMomentUrl: (): string => {
121
+ return "https://dash.example/x";
122
+ },
123
+ ...overrides,
124
+ };
125
+ }
126
+
127
+ function makeRailRowProps(
128
+ overrides?: Partial<ReplayRailRowProps>,
129
+ ): ReplayRailRowProps {
130
+ const signal: ReplaySignal = {
131
+ id: "rec:1:0",
132
+ kind: "network",
133
+ source: "recording",
134
+ offsetMs: 62300,
135
+ severity: "info",
136
+ title: "GET /api/checkout/session returned 500",
137
+ subtitle: "812ms",
138
+ links: {},
139
+ detail: {},
140
+ };
141
+
142
+ const row: ReplayRailRowModel = {
143
+ signal: signal,
144
+ repeatCount: 1,
145
+ lastOffsetMs: signal.offsetMs,
146
+ memberIds: [signal.id],
147
+ };
148
+
149
+ return {
150
+ row: row,
151
+ domId: "rail-row-1",
152
+ isActive: false,
153
+ isSelected: false,
154
+ isFuture: false,
155
+ isFocusStop: true,
156
+ onActivate: noop,
157
+ onSeek: noop,
158
+ onHover: noop,
159
+ ...overrides,
160
+ };
161
+ }
162
+
163
+ /*
164
+ * Elements are ordered as they appear in the document, so comparing two
165
+ * positions with compareDocumentPosition is how "A comes before B" is
166
+ * asserted without reaching for a snapshot.
167
+ */
168
+ function comesBefore(first: Element, second: Element): boolean {
169
+ return Boolean(
170
+ first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
171
+ );
172
+ }
173
+
174
+ describe("player stacking order", () => {
175
+ /*
176
+ * picture -> track -> transport, the order every media player has used
177
+ * since the first one. The old layout put ten buttons BETWEEN the
178
+ * picture and the timeline, so the two things a viewer moves between -
179
+ * the frame, and the position they want it at - were separated by the
180
+ * noisiest strip on the page.
181
+ */
182
+ it("draws the timeline above the transport row", () => {
183
+ render(<ReplayScrubber {...makeScrubberProps()} />);
184
+
185
+ const timeline: HTMLElement = screen.getByTestId("replay-timeline");
186
+ const controls: HTMLElement = screen.getByTestId("replay-controls");
187
+
188
+ expect(comesBefore(timeline, controls)).toBe(true);
189
+ });
190
+
191
+ /*
192
+ * The address bar, the picture and the transport are sections of ONE
193
+ * card that the shell draws; each of them drawing its own border was
194
+ * what made the player read as three unrelated widgets stacked with
195
+ * gaps between them. If the scrubber grows a border again, the shell's
196
+ * card gets a second outline nested inside it.
197
+ */
198
+ it("leaves the card to the shell instead of drawing its own", () => {
199
+ render(<ReplayScrubber {...makeScrubberProps()} />);
200
+
201
+ const scrubber: HTMLElement = screen.getByTestId("replay-scrubber");
202
+
203
+ expect(scrubber.className).not.toContain("border");
204
+ expect(scrubber.className).not.toContain("shadow");
205
+ expect(scrubber.className).not.toContain("rounded");
206
+ });
207
+ });
208
+
209
+ describe("transport row", () => {
210
+ /*
211
+ * ONE height for every control, and exactly one exception: the play
212
+ * button, which is deliberately larger and round because it is the one
213
+ * control a viewer should find without reading. Four heights in a row
214
+ * is what made the old strip look like a pile of unrelated chips - the
215
+ * worst offender being Common/UI's 24px Toggle with its text-sm label.
216
+ */
217
+ it("gives every button the shared control height, except the play button", () => {
218
+ render(<ReplayScrubber {...makeScrubberProps()} />);
219
+
220
+ const controls: HTMLElement = screen.getByTestId("replay-controls");
221
+ const buttons: Array<HTMLElement> = Array.from(
222
+ controls.querySelectorAll("button"),
223
+ );
224
+
225
+ expect(buttons.length).toBeGreaterThan(5);
226
+
227
+ buttons.forEach((button: HTMLElement): void => {
228
+ if (button.getAttribute("data-testid") === "replay-play-pause") {
229
+ /* The exception, and it is a circle. */
230
+ expect(button.className).toContain("h-9");
231
+ expect(button.className).toContain("rounded-full");
232
+ return;
233
+ }
234
+
235
+ expect(button.className).toContain(REPLAY_CONTROL_HEIGHT_CLASS);
236
+ });
237
+ });
238
+
239
+ /*
240
+ * Colour means something in this row: rose is "there is an error to
241
+ * jump to", amber is "there is a frustration signal", indigo is the
242
+ * one primary action. Everything else is a ghost. A button that draws
243
+ * its own outline at rest is back to being a chip.
244
+ */
245
+ it("draws no resting outline on any control but the play button", () => {
246
+ render(<ReplayScrubber {...makeScrubberProps()} />);
247
+
248
+ const controls: HTMLElement = screen.getByTestId("replay-controls");
249
+ const buttons: Array<HTMLElement> = Array.from(
250
+ controls.querySelectorAll("button"),
251
+ );
252
+
253
+ buttons.forEach((button: HTMLElement): void => {
254
+ /* focus-visible rings are keyboard affordances, not chrome. */
255
+ const restingClasses: string = button.className
256
+ .split(" ")
257
+ .filter((token: string): boolean => {
258
+ return !token.startsWith("focus");
259
+ })
260
+ .join(" ");
261
+
262
+ expect(restingClasses).not.toContain("ring-gray-300");
263
+ expect(restingClasses).not.toContain("ring-1 ring-inset ring-gray-");
264
+ });
265
+ });
266
+
267
+ it("groups the controls that belong together on shared tracks", () => {
268
+ render(<ReplayScrubber {...makeScrubberProps()} />);
269
+
270
+ const controls: HTMLElement = screen.getByTestId("replay-controls");
271
+
272
+ /* +-10s together, and the three signal-jump buttons together. */
273
+ expect(
274
+ within(controls).getByRole("group", { name: "Seek by ten seconds" }),
275
+ ).toBeInTheDocument();
276
+ expect(
277
+ within(controls).getByRole("group", { name: "Jump between signals" }),
278
+ ).toBeInTheDocument();
279
+ });
280
+
281
+ it("separates the clusters with decorative hairlines, not with text", () => {
282
+ render(<ReplayScrubber {...makeScrubberProps()} />);
283
+
284
+ const dividers: Array<HTMLElement> = screen.getAllByTestId(
285
+ "replay-toolbar-divider",
286
+ );
287
+
288
+ expect(dividers.length).toBeGreaterThan(0);
289
+ dividers.forEach((divider: HTMLElement): void => {
290
+ expect(divider).toHaveAttribute("aria-hidden", "true");
291
+ expect(divider.textContent).toBe("");
292
+ });
293
+ });
294
+
295
+ /*
296
+ * The speed menu opens UPWARD. The transport row is the bottom edge of
297
+ * the player card now, so a downward menu would hang off the card into
298
+ * the page - and, on the shortest viewport the player supports, off
299
+ * the screen.
300
+ */
301
+ it("opens the speed menu above its trigger", () => {
302
+ render(<ReplayScrubber {...makeScrubberProps()} />);
303
+
304
+ fireEvent.click(screen.getByTestId("replay-speed"));
305
+
306
+ expect(screen.getByTestId("replay-speed-menu").className).toContain(
307
+ "bottom-full",
308
+ );
309
+ });
310
+ });
311
+
312
+ describe("header hierarchy", () => {
313
+ /*
314
+ * Everything on the old header ran at text-xs, separated by a literal
315
+ * "|" character, so the one thing a viewer opens the page to check -
316
+ * is this the right session - shouted at exactly the same volume as
317
+ * the viewport size. The identity now leads.
318
+ */
319
+ it("sets the identity a size larger than the device facts", () => {
320
+ render(<ReplayHeader {...makeHeaderProps()} />);
321
+
322
+ expect(screen.getByTestId("replay-header-user").className).toContain(
323
+ "text-sm",
324
+ );
325
+ screen
326
+ .getAllByTestId("replay-header-fact")
327
+ .forEach((fact: HTMLElement): void => {
328
+ expect(fact.className).not.toContain("text-sm");
329
+ });
330
+ });
331
+
332
+ it("uses no typed-out pipe characters as separators", () => {
333
+ render(<ReplayHeader {...makeHeaderProps()} />);
334
+
335
+ expect(screen.getByTestId("replay-header").textContent).not.toContain("|");
336
+ });
337
+
338
+ /*
339
+ * Row 2 is a shelf under a hairline. Its actions are the only
340
+ * outlined-looking things on the header, and their accessible names
341
+ * are the words printed on them (WCAG 2.5.3), not their tooltips.
342
+ */
343
+ it("names the action buttons by the word printed on them", () => {
344
+ render(<ReplayHeader {...makeHeaderProps()} />);
345
+
346
+ ["Link", "Wide", "Theater", "Details"].forEach((label: string): void => {
347
+ expect(screen.getByRole("button", { name: label })).toHaveTextContent(
348
+ label,
349
+ );
350
+ });
351
+ });
352
+
353
+ it("puts the layout toggles on one shared track", () => {
354
+ render(<ReplayHeader {...makeHeaderProps()} />);
355
+
356
+ const group: HTMLElement = screen.getByRole("group", {
357
+ name: "Player layout",
358
+ });
359
+
360
+ expect(within(group).getByTestId("replay-toggle-wide")).toBeInTheDocument();
361
+ expect(
362
+ within(group).getByTestId("replay-toggle-theater"),
363
+ ).toBeInTheDocument();
364
+ });
365
+
366
+ /*
367
+ * The wall clock is the number a viewer lines up against a dashboard,
368
+ * so it leads its block; the offset pair is a reference underneath it.
369
+ */
370
+ it("leads the clock block with the wall-clock time", () => {
371
+ render(<ReplayHeader {...makeHeaderProps()} />);
372
+
373
+ const block: HTMLElement = screen.getByTestId("replay-header-clock");
374
+ const wallClock: HTMLElement = screen.getByTestId(
375
+ "replay-header-wall-clock",
376
+ );
377
+
378
+ expect(block).toContainElement(wallClock);
379
+ expect(wallClock.className).toContain("font-semibold");
380
+ expect(block).toHaveTextContent("0:41 / 4:12");
381
+ });
382
+
383
+ it("keeps the identity honest about the difference between hidden and anonymous", () => {
384
+ const { rerender } = render(
385
+ <ReplayHeader {...makeHeaderProps({ identity: { label: null } })} />,
386
+ );
387
+
388
+ expect(screen.getByTestId("replay-header-user")).toHaveTextContent(
389
+ "Identity hidden",
390
+ );
391
+
392
+ rerender(
393
+ <ReplayHeader {...makeHeaderProps({ identity: { label: "" } })} />,
394
+ );
395
+ expect(screen.getByTestId("replay-header-user")).toHaveTextContent(
396
+ "Anonymous",
397
+ );
398
+ });
399
+ });
400
+
401
+ describe("rail row typography", () => {
402
+ /*
403
+ * The row used to be font-mono at 11px end to end - glyph, offset,
404
+ * title, subtitle, tags - which made a few hundred rows read as a log
405
+ * dump. Monospace is right for the one column that must align down the
406
+ * list and wrong for the prose beside it.
407
+ */
408
+ it("sets the offset in a monospace column and the title in the UI face", () => {
409
+ render(<ReplayRailRow {...makeRailRowProps()} />);
410
+
411
+ const row: HTMLElement = screen.getByTestId("rail-row");
412
+
413
+ expect(row.className).not.toContain("font-mono");
414
+
415
+ const offset: HTMLElement | null = within(row).getByText("1:02.3");
416
+
417
+ expect(offset.className).toContain("font-mono");
418
+ expect(offset.className).toContain("tabular-nums");
419
+
420
+ const title: HTMLElement = within(row).getByText(
421
+ "GET /api/checkout/session returned 500",
422
+ );
423
+
424
+ expect(title.className).not.toContain("font-mono");
425
+ });
426
+
427
+ /*
428
+ * The offset column is fixed width. Without it every title started at
429
+ * a different x depending on whether the row's time had crossed into
430
+ * minutes, which is exactly the ragged left edge that makes a list
431
+ * unscannable.
432
+ */
433
+ it("reserves a fixed width for the offset so titles line up", () => {
434
+ render(<ReplayRailRow {...makeRailRowProps()} />);
435
+
436
+ expect(screen.getByText("1:02.3").className).toContain("w-12");
437
+ });
438
+
439
+ it("still reads the whole row to assistive tech, glyph and meta included", () => {
440
+ render(
441
+ <ReplayRailRow
442
+ {...makeRailRowProps({ counterpartNote: "also reported server-side" })}
443
+ />,
444
+ );
445
+
446
+ const body: HTMLElement = screen.getByRole("button", {
447
+ name: /request, info at 1:02.3/,
448
+ });
449
+
450
+ expect(body).toHaveAttribute(
451
+ "aria-label",
452
+ expect.stringContaining("also reported server-side"),
453
+ );
454
+ expect(body).toHaveAttribute(
455
+ "aria-label",
456
+ expect.stringContaining("812ms"),
457
+ );
458
+ });
459
+ });
460
+
461
+ describe("timeline lanes", () => {
462
+ /*
463
+ * The legend is rendered unconditionally with a RESERVED height:
464
+ * showing it only on hover shifted every lane down by a line the
465
+ * moment the pointer entered the track, which moved the marker the
466
+ * viewer was aiming at.
467
+ */
468
+ it("always renders the legend at a fixed height", () => {
469
+ const { rerender } = render(<ReplayScrubber {...makeScrubberProps()} />);
470
+
471
+ const legend: HTMLElement = screen.getByTestId("timeline-legend");
472
+
473
+ expect(legend.className).toContain("h-4");
474
+ expect(legend).toHaveTextContent("Loaded");
475
+ expect(legend).toHaveTextContent("Gap");
476
+
477
+ rerender(
478
+ <ReplayScrubber
479
+ {...makeScrubberProps({
480
+ snapshot: makeSnapshot({ intent: "playing" }),
481
+ })}
482
+ />,
483
+ );
484
+
485
+ expect(screen.getByTestId("timeline-legend").className).toContain("h-4");
486
+ });
487
+
488
+ it("draws every legend swatch, so no band kind is unexplained", () => {
489
+ render(<ReplayScrubber {...makeScrubberProps()} />);
490
+
491
+ const legend: HTMLElement = screen.getByTestId("timeline-legend");
492
+
493
+ [
494
+ "Loaded",
495
+ "Not yet loaded",
496
+ "Gap",
497
+ "Idle",
498
+ "Tab in background",
499
+ "Approximate",
500
+ ].forEach((label: string): void => {
501
+ expect(legend).toHaveTextContent(label);
502
+ });
503
+ });
504
+
505
+ it("keeps the seekable track a real slider with a readable value", () => {
506
+ render(<ReplayScrubber {...makeScrubberProps()} />);
507
+
508
+ const track: HTMLElement = screen.getByTestId("timeline-track");
509
+
510
+ expect(track).toHaveAttribute("role", "slider");
511
+ expect(track).toHaveAttribute("aria-valuenow", "12000");
512
+ expect(track).toHaveAttribute("aria-valuetext", "0:12 of 10:00");
513
+ });
514
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import {
3
+ ReplayClockParts,
4
+ formatReplayClock,
5
+ formatReplayOffset,
6
+ formatReplayOffsetPrecise,
7
+ splitReplayClock,
8
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayTimeFormat";
9
+
10
+ /*
11
+ * The clock, as its two halves.
12
+ *
13
+ * splitReplayClock exists because the redesigned transport row weights
14
+ * the elapsed time and mutes the total, which needs the halves
15
+ * separately - and the one thing that must not happen is the halves
16
+ * drifting from the joined string that the E2E suite and every existing
17
+ * caller still read. formatReplayClock is now defined IN TERMS of the
18
+ * split, and these tests hold the two together.
19
+ */
20
+
21
+ describe("splitReplayClock", () => {
22
+ it("agrees with formatReplayClock, character for character", () => {
23
+ const cases: Array<[number, number, boolean]> = [
24
+ [0, 0, true],
25
+ [0, 0, false],
26
+ [12_000, 600_000, false],
27
+ [12_345, 600_000, true],
28
+ [62_300, 252_000, true],
29
+ [3_661_000, 7_200_000, false],
30
+ [-5_000, 90_000, false],
31
+ [Number.NaN, 90_000, true],
32
+ ];
33
+
34
+ cases.forEach(
35
+ ([currentTimeMs, durationMs, isPaused]: [
36
+ number,
37
+ number,
38
+ boolean,
39
+ ]): void => {
40
+ const parts: ReplayClockParts = splitReplayClock(
41
+ currentTimeMs,
42
+ durationMs,
43
+ isPaused,
44
+ );
45
+
46
+ expect(`${parts.current} / ${parts.total}`).toBe(
47
+ formatReplayClock(currentTimeMs, durationMs, isPaused),
48
+ );
49
+ },
50
+ );
51
+ });
52
+
53
+ /*
54
+ * Tenths only while paused: at 1x a decimal would flicker ten times a
55
+ * second and read as noise, and while paused a viewer stepping frame
56
+ * by frame needs the readout to move at all.
57
+ */
58
+ it("carries tenths only while paused", () => {
59
+ expect(splitReplayClock(62_300, 252_000, true).current).toBe("1:02.3");
60
+ expect(splitReplayClock(62_300, 252_000, false).current).toBe("1:02");
61
+ });
62
+
63
+ it("never shows tenths on the total, paused or not", () => {
64
+ expect(splitReplayClock(62_300, 252_400, true).total).toBe("4:12");
65
+ expect(splitReplayClock(62_300, 252_400, false).total).toBe("4:12");
66
+ });
67
+
68
+ it("renders hours on both halves once a session is long enough", () => {
69
+ const parts: ReplayClockParts = splitReplayClock(
70
+ 3_661_000,
71
+ 7_200_000,
72
+ false,
73
+ );
74
+
75
+ expect(parts.current).toBe("1:01:01");
76
+ expect(parts.total).toBe("2:00:00");
77
+ });
78
+
79
+ /* A bad manifest must reach the screen as "0:00", never as "NaN:NaN". */
80
+ it("floors a garbage offset to zero instead of printing NaN", () => {
81
+ expect(splitReplayClock(Number.NaN, 90_000, false).current).toBe("0:00");
82
+ expect(splitReplayClock(-5_000, 90_000, false).current).toBe("0:00");
83
+ expect(
84
+ splitReplayClock(10_000, Number.POSITIVE_INFINITY, false).total,
85
+ ).toBe("0:00");
86
+ });
87
+
88
+ it("floors rather than rounds, so a link never lands after its event", () => {
89
+ /* 2,900ms is still inside second two on every surface of the player. */
90
+ expect(splitReplayClock(2_900, 60_000, false).current).toBe("0:02");
91
+ expect(formatReplayOffset(2_900)).toBe("0:02");
92
+ expect(formatReplayOffsetPrecise(2_900)).toBe("0:02.9");
93
+ });
94
+ });