@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.
- package/Models/DatabaseModels/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- 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
|
+
});
|