@oneuptime/common 14.0.5 → 14.0.7
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/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.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/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
test,
|
|
9
9
|
} from "@jest/globals";
|
|
10
10
|
import {
|
|
11
|
+
act,
|
|
11
12
|
cleanup,
|
|
12
13
|
fireEvent,
|
|
13
14
|
render,
|
|
@@ -17,44 +18,82 @@ import {
|
|
|
17
18
|
import React from "react";
|
|
18
19
|
import { MemoryRouter } from "react-router-dom";
|
|
19
20
|
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
-
import
|
|
21
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
22
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
21
23
|
import Route from "../../../Types/API/Route";
|
|
24
|
+
import URL from "../../../Types/API/URL";
|
|
25
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
22
26
|
import ObjectID from "../../../Types/ObjectID";
|
|
23
27
|
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
28
|
+
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
29
|
+
import TimeRange from "../../../Types/Time/TimeRange";
|
|
30
|
+
import {
|
|
31
|
+
TOPOLOGY_API_FORMAT_VERSION,
|
|
32
|
+
TopologyApiPath,
|
|
33
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
24
34
|
import TopologyPage from "../../../../App/FeatureSet/Dashboard/src/Pages/Topology/TopologyPage";
|
|
35
|
+
import {
|
|
36
|
+
InfrastructureCollection,
|
|
37
|
+
InfrastructureTotals,
|
|
38
|
+
TopologyEntity,
|
|
39
|
+
TopologyRelationship,
|
|
40
|
+
TopologyRunsOnCounts,
|
|
41
|
+
TopologyTruncation,
|
|
42
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
/*
|
|
45
|
+
* The Topology page's three tabs load independently: Service Map and
|
|
46
|
+
* Infrastructure each POST their own endpoint the first time they are
|
|
47
|
+
* opened, Network loads nothing here. These tests drive the page through a
|
|
48
|
+
* fake server behind API.post and stub the three views, so they pin what the
|
|
49
|
+
* page asks for, when, and what it hands each view.
|
|
50
|
+
*/
|
|
27
51
|
|
|
28
|
-
|
|
52
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* What the page's translations return. Identity by default; a test that
|
|
56
|
+
* needs to see which fragments were translated swaps it for a marker.
|
|
57
|
+
*/
|
|
58
|
+
const identity: (value: string) => string = (value: string): string => {
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
let translate: (value: string) => string = identity;
|
|
62
|
+
|
|
63
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
29
64
|
return {
|
|
30
65
|
__esModule: true,
|
|
31
66
|
default: {
|
|
32
|
-
|
|
33
|
-
return
|
|
67
|
+
post: (...args: Array<unknown>) => {
|
|
68
|
+
return postMock(...args);
|
|
69
|
+
},
|
|
70
|
+
getFriendlyMessage: (error: unknown): string => {
|
|
71
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
72
|
+
return String((error as { message: unknown }).message);
|
|
73
|
+
}
|
|
74
|
+
return "Unable to load topology";
|
|
34
75
|
},
|
|
35
76
|
},
|
|
36
77
|
};
|
|
37
78
|
});
|
|
38
79
|
|
|
39
|
-
jest.mock("../../../UI/Utils/
|
|
80
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
40
81
|
return {
|
|
41
82
|
__esModule: true,
|
|
42
83
|
default: {
|
|
43
|
-
|
|
44
|
-
return
|
|
84
|
+
getCommonHeaders: (): Record<string, string> => {
|
|
85
|
+
return { tenantid: "dde060c6-fe0d-49ce-b44c-4035a13bc1db" };
|
|
45
86
|
},
|
|
46
87
|
},
|
|
47
88
|
};
|
|
48
89
|
});
|
|
49
90
|
|
|
50
|
-
jest.mock("../../../UI/Utils/
|
|
91
|
+
jest.mock("../../../UI/Utils/Project", () => {
|
|
51
92
|
return {
|
|
52
93
|
__esModule: true,
|
|
53
94
|
default: {
|
|
54
|
-
|
|
55
|
-
return
|
|
56
|
-
? error.message
|
|
57
|
-
: "Unable to load topology";
|
|
95
|
+
getCurrentProjectId: () => {
|
|
96
|
+
return new ObjectID("dde060c6-fe0d-49ce-b44c-4035a13bc1db");
|
|
58
97
|
},
|
|
59
98
|
},
|
|
60
99
|
};
|
|
@@ -65,7 +104,7 @@ jest.mock("react-i18next", () => {
|
|
|
65
104
|
useTranslation: () => {
|
|
66
105
|
return {
|
|
67
106
|
t: (value: string): string => {
|
|
68
|
-
return value;
|
|
107
|
+
return translate(value);
|
|
69
108
|
},
|
|
70
109
|
};
|
|
71
110
|
},
|
|
@@ -86,39 +125,89 @@ jest.mock(
|
|
|
86
125
|
() => {
|
|
87
126
|
return {
|
|
88
127
|
__esModule: true,
|
|
89
|
-
default: (
|
|
128
|
+
default: (props: {
|
|
129
|
+
onChange: (value: RangeStartAndEndDateTime) => void;
|
|
130
|
+
}): React.ReactElement => {
|
|
90
131
|
return (
|
|
91
|
-
<div data-testid="topology-time-picker">
|
|
132
|
+
<div data-testid="topology-time-picker">
|
|
133
|
+
Connection time range
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
props.onChange({ range: TimeRange.PAST_ONE_HOUR });
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
Pick the past hour
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
92
143
|
);
|
|
93
144
|
},
|
|
94
145
|
};
|
|
95
146
|
},
|
|
96
147
|
);
|
|
97
148
|
|
|
149
|
+
function names(entities: Array<TopologyEntity>): string {
|
|
150
|
+
return entities
|
|
151
|
+
.map((entity: TopologyEntity): string => {
|
|
152
|
+
return entity.displayName || "";
|
|
153
|
+
})
|
|
154
|
+
.join(",");
|
|
155
|
+
}
|
|
156
|
+
|
|
98
157
|
jest.mock(
|
|
99
158
|
"../../../../App/FeatureSet/Dashboard/src/Components/Topology/ServiceMapGraph",
|
|
100
159
|
() => {
|
|
101
160
|
return {
|
|
102
161
|
__esModule: true,
|
|
103
162
|
default: (props: {
|
|
104
|
-
entities: Array<
|
|
163
|
+
entities: Array<TopologyEntity>;
|
|
164
|
+
relationships: Array<TopologyRelationship>;
|
|
165
|
+
runsOnCounts?: TopologyRunsOnCounts;
|
|
105
166
|
includeInactive?: boolean;
|
|
106
167
|
rangeStart?: Date | null;
|
|
168
|
+
metricsWindowSeconds: number;
|
|
169
|
+
timeRange: RangeStartAndEndDateTime;
|
|
107
170
|
onOpenInfrastructure?: (key: string) => void;
|
|
108
171
|
}): React.ReactElement => {
|
|
109
172
|
return (
|
|
110
173
|
<div data-testid="topology-services">
|
|
111
|
-
Services:{
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
Services: {names(props.entities)}
|
|
175
|
+
<span data-testid="services-relationships">
|
|
176
|
+
{props.relationships
|
|
177
|
+
.map((relationship: TopologyRelationship): string => {
|
|
178
|
+
return `${relationship.fromEntityKey}>${relationship.toEntityKey}`;
|
|
179
|
+
})
|
|
180
|
+
.join(",")}
|
|
181
|
+
</span>
|
|
182
|
+
<span data-testid="services-runs-on">
|
|
183
|
+
{Array.from(props.runsOnCounts?.entries() || [])
|
|
184
|
+
.map(
|
|
185
|
+
([service, counts]: [
|
|
186
|
+
string,
|
|
187
|
+
Array<{ entityType: string; active: number }>,
|
|
188
|
+
]): string => {
|
|
189
|
+
return `${service}:${counts
|
|
190
|
+
.map(
|
|
191
|
+
(count: {
|
|
192
|
+
entityType: string;
|
|
193
|
+
active: number;
|
|
194
|
+
}): string => {
|
|
195
|
+
return `${count.active} ${count.entityType}`;
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
.join("+")}`;
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
.join(",")}
|
|
202
|
+
</span>
|
|
117
203
|
<span data-testid="services-inactive">
|
|
118
204
|
{String(Boolean(props.includeInactive))}
|
|
119
205
|
</span>
|
|
120
206
|
<span data-testid="services-range-start">
|
|
121
|
-
{props.rangeStart ?
|
|
207
|
+
{props.rangeStart ? props.rangeStart.toISOString() : "unset"}
|
|
208
|
+
</span>
|
|
209
|
+
<span data-testid="services-metrics-window">
|
|
210
|
+
{props.metricsWindowSeconds}
|
|
122
211
|
</span>
|
|
123
212
|
<button
|
|
124
213
|
type="button"
|
|
@@ -141,21 +230,48 @@ jest.mock(
|
|
|
141
230
|
return {
|
|
142
231
|
__esModule: true,
|
|
143
232
|
default: (props: {
|
|
144
|
-
entities: Array<
|
|
233
|
+
entities: Array<TopologyEntity>;
|
|
234
|
+
relationships: Array<TopologyRelationship>;
|
|
235
|
+
collections?: Array<InfrastructureCollection>;
|
|
236
|
+
totals?: InfrastructureTotals;
|
|
237
|
+
truncation?: TopologyTruncation | null;
|
|
145
238
|
includeInactive?: boolean;
|
|
239
|
+
rangeStart?: Date | null;
|
|
146
240
|
onOpenServiceMap?: (key: string) => void;
|
|
147
241
|
}): React.ReactElement => {
|
|
148
242
|
return (
|
|
149
243
|
<div data-testid="topology-infrastructure">
|
|
150
|
-
Infrastructure:{
|
|
151
|
-
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
244
|
+
Infrastructure: {names(props.entities)}
|
|
245
|
+
<span data-testid="infrastructure-relationships">
|
|
246
|
+
{props.relationships
|
|
247
|
+
.map((relationship: TopologyRelationship): string => {
|
|
248
|
+
return `${relationship.fromEntityKey}-${relationship.relationshipType}->${relationship.toEntityKey}`;
|
|
249
|
+
})
|
|
250
|
+
.join(",")}
|
|
251
|
+
</span>
|
|
252
|
+
<span data-testid="infrastructure-collections">
|
|
253
|
+
{(props.collections || [])
|
|
254
|
+
.map((collection: InfrastructureCollection): string => {
|
|
255
|
+
return `${collection.entityType}:${collection.active}/${collection.total}`;
|
|
256
|
+
})
|
|
257
|
+
.join(",")}
|
|
258
|
+
</span>
|
|
259
|
+
<span data-testid="infrastructure-totals">
|
|
260
|
+
{props.totals
|
|
261
|
+
? `${props.totals.activeResources}/${props.totals.resources}`
|
|
262
|
+
: "none"}
|
|
263
|
+
</span>
|
|
264
|
+
<span data-testid="infrastructure-truncation">
|
|
265
|
+
{props.truncation
|
|
266
|
+
? `${props.truncation.shown}/${props.truncation.total}`
|
|
267
|
+
: "none"}
|
|
268
|
+
</span>
|
|
156
269
|
<span data-testid="infrastructure-inactive">
|
|
157
270
|
{String(Boolean(props.includeInactive))}
|
|
158
271
|
</span>
|
|
272
|
+
<span data-testid="infrastructure-range-start">
|
|
273
|
+
{props.rangeStart ? props.rangeStart.toISOString() : "unset"}
|
|
274
|
+
</span>
|
|
159
275
|
<button
|
|
160
276
|
type="button"
|
|
161
277
|
onClick={() => {
|
|
@@ -183,6 +299,159 @@ jest.mock(
|
|
|
183
299
|
},
|
|
184
300
|
);
|
|
185
301
|
|
|
302
|
+
interface PostOptions {
|
|
303
|
+
url: URL;
|
|
304
|
+
data: JSONObject;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface RecordedRequest {
|
|
308
|
+
path: string;
|
|
309
|
+
rangeStart: string;
|
|
310
|
+
/* True when the request asked the server to bypass its response cache. */
|
|
311
|
+
fresh: boolean;
|
|
312
|
+
respond: (payload: JSONObject) => void;
|
|
313
|
+
fail: (statusCode: number, message: string) => void;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let requests: Array<RecordedRequest> = [];
|
|
317
|
+
/*
|
|
318
|
+
* How the fake server answers a request: a payload, an HTTP failure, or
|
|
319
|
+
* "hold" (the test answers it later).
|
|
320
|
+
*/
|
|
321
|
+
type Answer = JSONObject | { status: number; message: string } | "hold";
|
|
322
|
+
let answer: (request: RecordedRequest) => Answer;
|
|
323
|
+
|
|
324
|
+
const ECHOED_RANGE_START: string = "2026-09-20T10:15:00.000Z";
|
|
325
|
+
|
|
326
|
+
function requestsTo(path: TopologyApiPath): Array<RecordedRequest> {
|
|
327
|
+
return requests.filter((request: RecordedRequest): boolean => {
|
|
328
|
+
return request.path === path;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function serviceMapPayload(extra: JSONObject = {}): JSONObject {
|
|
333
|
+
return {
|
|
334
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
335
|
+
rangeStart: ECHOED_RANGE_START,
|
|
336
|
+
generatedAt: "2026-09-21T10:15:42.000Z",
|
|
337
|
+
entities: [
|
|
338
|
+
{
|
|
339
|
+
key: "checkout",
|
|
340
|
+
type: EntityType.Service,
|
|
341
|
+
name: "Checkout API",
|
|
342
|
+
source: "discovered",
|
|
343
|
+
lastSeenAt: null,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
key: "orders-db",
|
|
347
|
+
type: EntityType.Database,
|
|
348
|
+
name: "Orders DB",
|
|
349
|
+
source: "discovered",
|
|
350
|
+
lastSeenAt: null,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
dependencies: [
|
|
354
|
+
{
|
|
355
|
+
from: "checkout",
|
|
356
|
+
to: "orders-db",
|
|
357
|
+
callCount: 10,
|
|
358
|
+
errorCount: 0,
|
|
359
|
+
avgDurationMs: 4,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
runsOn: [
|
|
363
|
+
{
|
|
364
|
+
service: "checkout",
|
|
365
|
+
type: EntityType.KubernetesPod,
|
|
366
|
+
active: 3,
|
|
367
|
+
total: 4,
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
entityTruncation: null,
|
|
371
|
+
dependencyTruncation: null,
|
|
372
|
+
...extra,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function infrastructurePayload(extra: JSONObject = {}): JSONObject {
|
|
377
|
+
return {
|
|
378
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
379
|
+
rangeStart: ECHOED_RANGE_START,
|
|
380
|
+
generatedAt: "2026-09-21T10:15:42.000Z",
|
|
381
|
+
nodes: [
|
|
382
|
+
{
|
|
383
|
+
key: "node-a",
|
|
384
|
+
type: EntityType.KubernetesNode,
|
|
385
|
+
name: "Node A",
|
|
386
|
+
source: "discovered",
|
|
387
|
+
lastSeenAt: null,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
key: "pod-1",
|
|
391
|
+
type: EntityType.KubernetesPod,
|
|
392
|
+
name: "Pod 1",
|
|
393
|
+
source: "discovered",
|
|
394
|
+
lastSeenAt: null,
|
|
395
|
+
parent: 0,
|
|
396
|
+
parentVia: "runs-on",
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
services: [{ key: "checkout", name: "Checkout API" }],
|
|
400
|
+
placements: [[0, 1]],
|
|
401
|
+
collections: [
|
|
402
|
+
{
|
|
403
|
+
type: EntityType.IoTDevice,
|
|
404
|
+
total: 25000,
|
|
405
|
+
active: 24000,
|
|
406
|
+
lastSeenAt: null,
|
|
407
|
+
activeLastSeenAt: null,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
totals: { resources: 25002, activeResources: 24002 },
|
|
411
|
+
truncation: null,
|
|
412
|
+
...extra,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function defaultAnswer(request: RecordedRequest): Answer {
|
|
417
|
+
return request.path === TopologyApiPath.ServiceMap
|
|
418
|
+
? serviceMapPayload()
|
|
419
|
+
: infrastructurePayload();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function installFakeServer(): void {
|
|
423
|
+
postMock.mockImplementation((...args: Array<unknown>) => {
|
|
424
|
+
const options: PostOptions = args[0] as PostOptions;
|
|
425
|
+
return new Promise(
|
|
426
|
+
(
|
|
427
|
+
resolve: (value: HTTPResponse<JSONObject> | HTTPErrorResponse) => void,
|
|
428
|
+
): void => {
|
|
429
|
+
const request: RecordedRequest = {
|
|
430
|
+
path: options.url.toString().replace(/^.*\/api(?=\/)/, ""),
|
|
431
|
+
rangeStart: String(options.data["rangeStart"]),
|
|
432
|
+
fresh: options.data["fresh"] === true,
|
|
433
|
+
respond: (payload: JSONObject): void => {
|
|
434
|
+
resolve(new HTTPResponse<JSONObject>(200, payload, {}));
|
|
435
|
+
},
|
|
436
|
+
fail: (statusCode: number, message: string): void => {
|
|
437
|
+
resolve(new HTTPErrorResponse(statusCode, { message }, {}));
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
requests.push(request);
|
|
441
|
+
const reply: Answer = answer(request);
|
|
442
|
+
if (reply === "hold") {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if ("status" in reply && typeof reply["status"] === "number") {
|
|
446
|
+
request.fail(reply["status"], String(reply["message"]));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
request.respond(reply as JSONObject);
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
186
455
|
function renderPage(): void {
|
|
187
456
|
render(
|
|
188
457
|
<MemoryRouter>
|
|
@@ -195,9 +464,16 @@ function renderPage(): void {
|
|
|
195
464
|
);
|
|
196
465
|
}
|
|
197
466
|
|
|
467
|
+
function tab(name: string): HTMLElement {
|
|
468
|
+
return screen.getByRole("tab", { name });
|
|
469
|
+
}
|
|
470
|
+
|
|
198
471
|
beforeEach(() => {
|
|
199
|
-
|
|
200
|
-
|
|
472
|
+
requests = [];
|
|
473
|
+
answer = defaultAnswer;
|
|
474
|
+
translate = identity;
|
|
475
|
+
postMock.mockReset();
|
|
476
|
+
installFakeServer();
|
|
201
477
|
window.history.replaceState({}, "", "/dashboard/project/topology/overview");
|
|
202
478
|
});
|
|
203
479
|
|
|
@@ -206,96 +482,157 @@ afterEach(() => {
|
|
|
206
482
|
jest.restoreAllMocks();
|
|
207
483
|
});
|
|
208
484
|
|
|
209
|
-
describe("
|
|
210
|
-
test("
|
|
211
|
-
|
|
485
|
+
describe("what each tab loads", () => {
|
|
486
|
+
test("the Service Map loads its own endpoint once, behind a labelled loader", async () => {
|
|
487
|
+
answer = (): Answer => {
|
|
488
|
+
return "hold";
|
|
489
|
+
};
|
|
212
490
|
renderPage();
|
|
213
|
-
|
|
214
|
-
expect(
|
|
491
|
+
const loader: HTMLElement = await screen.findByText("Loading service map…");
|
|
492
|
+
expect(loader.closest('[role="status"]')).not.toBeNull();
|
|
215
493
|
expect(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
494
|
+
requests.map((request: RecordedRequest): string => {
|
|
495
|
+
return request.path;
|
|
496
|
+
}),
|
|
497
|
+
).toEqual([TopologyApiPath.ServiceMap]);
|
|
498
|
+
await act(async () => {
|
|
499
|
+
requests[0]!.respond(serviceMapPayload());
|
|
500
|
+
});
|
|
501
|
+
expect(screen.getByTestId("topology-services")).toHaveTextContent(
|
|
502
|
+
"Checkout API,Orders DB",
|
|
220
503
|
);
|
|
504
|
+
expect(screen.queryByText("Loading service map…")).not.toBeInTheDocument();
|
|
505
|
+
expect(requests).toHaveLength(1);
|
|
221
506
|
});
|
|
222
507
|
|
|
223
|
-
test("
|
|
224
|
-
|
|
225
|
-
|
|
508
|
+
test("the Service Map receives entities, dependencies, runs-on counts and the echoed range start", async () => {
|
|
509
|
+
renderPage();
|
|
510
|
+
await screen.findByTestId("topology-services");
|
|
511
|
+
expect(screen.getByTestId("services-relationships")).toHaveTextContent(
|
|
512
|
+
"checkout>orders-db",
|
|
513
|
+
);
|
|
514
|
+
expect(screen.getByTestId("services-runs-on")).toHaveTextContent(
|
|
515
|
+
`checkout:3 ${EntityType.KubernetesPod}`,
|
|
516
|
+
);
|
|
517
|
+
expect(screen.getByTestId("services-range-start")).toHaveTextContent(
|
|
518
|
+
ECHOED_RANGE_START,
|
|
226
519
|
);
|
|
520
|
+
expect(screen.getByTestId("services-metrics-window")).toHaveTextContent(
|
|
521
|
+
"900",
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("Infrastructure receives resources, containment, collections, totals and the echoed range start", async () => {
|
|
526
|
+
window.history.replaceState({}, "", "?tab=Infrastructure");
|
|
227
527
|
renderPage();
|
|
228
|
-
await screen.
|
|
229
|
-
fireEvent.click(screen.getByRole("tab", { name: "Network" }));
|
|
230
|
-
expect(screen.getByTestId("topology-network")).toBeVisible();
|
|
528
|
+
await screen.findByTestId("topology-infrastructure");
|
|
231
529
|
expect(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
240
|
-
expect(await screen.findByTestId("topology-infrastructure")).toBeVisible();
|
|
530
|
+
requests.map((request: RecordedRequest): string => {
|
|
531
|
+
return request.path;
|
|
532
|
+
}),
|
|
533
|
+
).toEqual([TopologyApiPath.Infrastructure]);
|
|
534
|
+
expect(screen.getByTestId("topology-infrastructure")).toHaveTextContent(
|
|
535
|
+
"Node A,Pod 1,Checkout API",
|
|
536
|
+
);
|
|
241
537
|
expect(
|
|
242
|
-
screen.
|
|
243
|
-
).
|
|
538
|
+
screen.getByTestId("infrastructure-relationships"),
|
|
539
|
+
).toHaveTextContent("pod-1-runs-on->node-a,checkout-runs-on->pod-1");
|
|
540
|
+
expect(screen.getByTestId("infrastructure-collections")).toHaveTextContent(
|
|
541
|
+
`${EntityType.IoTDevice}:24000/25000`,
|
|
542
|
+
);
|
|
543
|
+
expect(screen.getByTestId("infrastructure-totals")).toHaveTextContent(
|
|
544
|
+
"24002/25002",
|
|
545
|
+
);
|
|
546
|
+
expect(screen.getByTestId("infrastructure-truncation")).toHaveTextContent(
|
|
547
|
+
"none",
|
|
548
|
+
);
|
|
549
|
+
expect(screen.getByTestId("infrastructure-range-start")).toHaveTextContent(
|
|
550
|
+
ECHOED_RANGE_START,
|
|
551
|
+
);
|
|
244
552
|
});
|
|
245
553
|
|
|
246
|
-
test("
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
getListMock.mockResolvedValueOnce({
|
|
252
|
-
data: [entity],
|
|
253
|
-
count: 1,
|
|
254
|
-
skip: 0,
|
|
255
|
-
limit: 1000,
|
|
256
|
-
});
|
|
257
|
-
getListMock.mockResolvedValueOnce({
|
|
258
|
-
data: [],
|
|
259
|
-
count: 0,
|
|
260
|
-
skip: 0,
|
|
261
|
-
limit: 1000,
|
|
262
|
-
});
|
|
554
|
+
test("Infrastructure shows its own loader while it loads", async () => {
|
|
555
|
+
window.history.replaceState({}, "", "?tab=Infrastructure");
|
|
556
|
+
answer = (): Answer => {
|
|
557
|
+
return "hold";
|
|
558
|
+
};
|
|
263
559
|
renderPage();
|
|
264
|
-
|
|
265
|
-
"
|
|
560
|
+
const loader: HTMLElement = await screen.findByText(
|
|
561
|
+
"Loading infrastructure…",
|
|
266
562
|
);
|
|
267
|
-
|
|
268
|
-
expect(screen.
|
|
563
|
+
expect(loader.closest('[role="status"]')).not.toBeNull();
|
|
564
|
+
expect(screen.queryByText("Loading service map…")).not.toBeInTheDocument();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("switching tabs loads each telemetry tab once and never refetches", async () => {
|
|
568
|
+
renderPage();
|
|
569
|
+
expect(await screen.findByTestId("topology-services")).toHaveTextContent(
|
|
269
570
|
"Checkout API",
|
|
270
571
|
);
|
|
271
|
-
fireEvent.click(
|
|
572
|
+
fireEvent.click(tab("Infrastructure"));
|
|
573
|
+
expect(
|
|
574
|
+
await screen.findByTestId("topology-infrastructure"),
|
|
575
|
+
).toHaveTextContent("Node A");
|
|
576
|
+
fireEvent.click(tab("Network"));
|
|
577
|
+
fireEvent.click(tab("Service Map"));
|
|
272
578
|
expect(screen.getByTestId("topology-services")).toHaveTextContent(
|
|
273
579
|
"Checkout API",
|
|
274
580
|
);
|
|
275
|
-
|
|
581
|
+
fireEvent.click(tab("Infrastructure"));
|
|
582
|
+
expect(screen.getByTestId("topology-infrastructure")).toBeVisible();
|
|
583
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
584
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
585
|
+
/* Both tabs were requested for the same pinned range start. */
|
|
586
|
+
expect(requests[0]!.rangeStart).toBe(requests[1]!.rangeStart);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("opens live Network while telemetry is still pending, and loads nothing for it", async () => {
|
|
590
|
+
answer = (): Answer => {
|
|
591
|
+
return "hold";
|
|
592
|
+
};
|
|
593
|
+
renderPage();
|
|
594
|
+
fireEvent.click(await screen.findByRole("tab", { name: "Network" }));
|
|
595
|
+
expect(screen.getByTestId("topology-network")).toBeVisible();
|
|
596
|
+
expect(
|
|
597
|
+
screen.queryByTestId("topology-time-picker"),
|
|
598
|
+
).not.toBeInTheDocument();
|
|
599
|
+
expect(new URLSearchParams(window.location.search).get("tab")).toBe(
|
|
600
|
+
"Network",
|
|
601
|
+
);
|
|
602
|
+
expect(
|
|
603
|
+
requests.map((request: RecordedRequest): string => {
|
|
604
|
+
return request.path;
|
|
605
|
+
}),
|
|
606
|
+
).toEqual([TopologyApiPath.ServiceMap]);
|
|
276
607
|
});
|
|
277
608
|
|
|
278
609
|
test.each(["Infrastructure", "Network"])(
|
|
279
610
|
"restores a shared %s link on the first render",
|
|
280
|
-
async (
|
|
281
|
-
window.history.replaceState(
|
|
611
|
+
async (tabName: string) => {
|
|
612
|
+
window.history.replaceState(
|
|
613
|
+
{},
|
|
614
|
+
"",
|
|
615
|
+
`?tab=${encodeURIComponent(tabName)}`,
|
|
616
|
+
);
|
|
282
617
|
renderPage();
|
|
283
618
|
await waitFor(() => {
|
|
284
|
-
expect(
|
|
285
|
-
"aria-selected",
|
|
286
|
-
"true",
|
|
287
|
-
);
|
|
619
|
+
expect(tab(tabName)).toHaveAttribute("aria-selected", "true");
|
|
288
620
|
});
|
|
289
|
-
if (
|
|
621
|
+
if (tabName === "Network") {
|
|
290
622
|
expect(screen.getByTestId("topology-network")).toBeVisible();
|
|
291
623
|
expect(
|
|
292
624
|
screen.queryByTestId("topology-time-picker"),
|
|
293
625
|
).not.toBeInTheDocument();
|
|
626
|
+
await act(async () => {
|
|
627
|
+
await Promise.resolve();
|
|
628
|
+
});
|
|
629
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
294
630
|
} else {
|
|
295
631
|
expect(
|
|
296
632
|
await screen.findByTestId("topology-infrastructure"),
|
|
297
633
|
).toBeVisible();
|
|
298
634
|
expect(screen.getByTestId("topology-time-picker")).toBeVisible();
|
|
635
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(0);
|
|
299
636
|
}
|
|
300
637
|
},
|
|
301
638
|
);
|
|
@@ -308,32 +645,396 @@ describe("topology map navigation and data isolation", () => {
|
|
|
308
645
|
);
|
|
309
646
|
renderPage();
|
|
310
647
|
expect(await screen.findByTestId("topology-services")).toBeVisible();
|
|
311
|
-
fireEvent.click(
|
|
312
|
-
|
|
648
|
+
fireEvent.click(tab("Infrastructure"));
|
|
649
|
+
await screen.findByTestId("topology-infrastructure");
|
|
650
|
+
fireEvent.click(tab("Service Map"));
|
|
313
651
|
const query: URLSearchParams = new URLSearchParams(window.location.search);
|
|
314
652
|
expect(query.has("tab")).toBe(false);
|
|
315
653
|
expect(query.get("search")).toBe("checkout");
|
|
316
654
|
expect(query.get("infraSearch")).toBe("worker");
|
|
317
655
|
});
|
|
656
|
+
});
|
|
318
657
|
|
|
658
|
+
describe("errors stay inside their tab", () => {
|
|
659
|
+
test("a Service Map failure does not block Network or Infrastructure, and Try again retries only the Service Map", async () => {
|
|
660
|
+
answer = (request: RecordedRequest): Answer => {
|
|
661
|
+
return request.path === TopologyApiPath.ServiceMap
|
|
662
|
+
? { status: 500, message: "Telemetry is temporarily unavailable" }
|
|
663
|
+
: infrastructurePayload();
|
|
664
|
+
};
|
|
665
|
+
renderPage();
|
|
666
|
+
const failure: HTMLElement = await screen.findByText(
|
|
667
|
+
"Telemetry is temporarily unavailable",
|
|
668
|
+
);
|
|
669
|
+
expect(failure.closest('[role="alert"]')).not.toBeNull();
|
|
670
|
+
|
|
671
|
+
fireEvent.click(tab("Network"));
|
|
672
|
+
expect(screen.getByTestId("topology-network")).toBeVisible();
|
|
673
|
+
expect(
|
|
674
|
+
screen.queryByText("Telemetry is temporarily unavailable"),
|
|
675
|
+
).not.toBeInTheDocument();
|
|
676
|
+
|
|
677
|
+
/* Infrastructure has its own endpoint and loads fine. */
|
|
678
|
+
fireEvent.click(tab("Infrastructure"));
|
|
679
|
+
expect(await screen.findByTestId("topology-infrastructure")).toBeVisible();
|
|
680
|
+
expect(
|
|
681
|
+
screen.queryByText("Telemetry is temporarily unavailable"),
|
|
682
|
+
).not.toBeInTheDocument();
|
|
683
|
+
|
|
684
|
+
fireEvent.click(tab("Service Map"));
|
|
685
|
+
expect(
|
|
686
|
+
screen.getByText("Telemetry is temporarily unavailable"),
|
|
687
|
+
).toBeVisible();
|
|
688
|
+
answer = defaultAnswer;
|
|
689
|
+
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
690
|
+
expect(await screen.findByTestId("topology-services")).toBeVisible();
|
|
691
|
+
expect(
|
|
692
|
+
screen.queryByText("Telemetry is temporarily unavailable"),
|
|
693
|
+
).not.toBeInTheDocument();
|
|
694
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
695
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
696
|
+
|
|
697
|
+
/* The retry did not throw Infrastructure's data away. */
|
|
698
|
+
fireEvent.click(tab("Infrastructure"));
|
|
699
|
+
expect(screen.getByTestId("topology-infrastructure")).toBeVisible();
|
|
700
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test.each([
|
|
704
|
+
["a newer payload format", "version"],
|
|
705
|
+
["a server without the endpoint", "404"],
|
|
706
|
+
])(
|
|
707
|
+
"%s asks for a page reload instead of offering a pointless retry",
|
|
708
|
+
async (_label: string, mode: string) => {
|
|
709
|
+
answer = (): Answer => {
|
|
710
|
+
return mode === "404"
|
|
711
|
+
? { status: 404, message: "Not found" }
|
|
712
|
+
: serviceMapPayload({
|
|
713
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION + 1,
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
renderPage();
|
|
717
|
+
expect(
|
|
718
|
+
await screen.findByText("Topology was updated. Reload the page."),
|
|
719
|
+
).toBeVisible();
|
|
720
|
+
expect(screen.getByRole("button", { name: "Reload page" })).toBeVisible();
|
|
721
|
+
expect(
|
|
722
|
+
screen.queryByRole("button", { name: "Try again" }),
|
|
723
|
+
).not.toBeInTheDocument();
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
/*
|
|
728
|
+
* The server caps concurrent topology work and answers 429 when it is
|
|
729
|
+
* full. That passes in a moment, so the tab says so in plain, translated
|
|
730
|
+
* words and offers "Try again" — not the outdated-bundle reload.
|
|
731
|
+
*/
|
|
732
|
+
test("a busy server (429) gets a friendly message and Try again, which recovers", async () => {
|
|
733
|
+
answer = (): Answer => {
|
|
734
|
+
return {
|
|
735
|
+
status: 429,
|
|
736
|
+
message: "Too many topology requests are running for this project.",
|
|
737
|
+
};
|
|
738
|
+
};
|
|
739
|
+
translate = (value: string): string => {
|
|
740
|
+
return value === "The topology service is busy. Try again in a moment."
|
|
741
|
+
? "[busy, translated]"
|
|
742
|
+
: value;
|
|
743
|
+
};
|
|
744
|
+
renderPage();
|
|
745
|
+
const failure: HTMLElement = await screen.findByText("[busy, translated]");
|
|
746
|
+
expect(failure.closest('[role="alert"]')).not.toBeNull();
|
|
747
|
+
expect(
|
|
748
|
+
screen.queryByText(
|
|
749
|
+
"Too many topology requests are running for this project.",
|
|
750
|
+
),
|
|
751
|
+
).not.toBeInTheDocument();
|
|
752
|
+
expect(
|
|
753
|
+
screen.queryByText("Topology was updated. Reload the page."),
|
|
754
|
+
).not.toBeInTheDocument();
|
|
755
|
+
expect(
|
|
756
|
+
screen.queryByRole("button", { name: "Reload page" }),
|
|
757
|
+
).not.toBeInTheDocument();
|
|
758
|
+
|
|
759
|
+
answer = defaultAnswer;
|
|
760
|
+
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
761
|
+
expect(await screen.findByTestId("topology-services")).toBeVisible();
|
|
762
|
+
expect(screen.queryByText("[busy, translated]")).not.toBeInTheDocument();
|
|
763
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
764
|
+
/* A retry is an ordinary load, not an explicit refresh. */
|
|
765
|
+
expect(
|
|
766
|
+
requests.map((request: RecordedRequest): boolean => {
|
|
767
|
+
return request.fresh;
|
|
768
|
+
}),
|
|
769
|
+
).toEqual([false, false]);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
describe("the refresh button and the time range", () => {
|
|
774
|
+
test("refresh is disabled while the active tab loads, then reloads only the active tab", async () => {
|
|
775
|
+
answer = (): Answer => {
|
|
776
|
+
return "hold";
|
|
777
|
+
};
|
|
778
|
+
renderPage();
|
|
779
|
+
const refresh: HTMLElement = await screen.findByRole("button", {
|
|
780
|
+
name: "Refresh topology",
|
|
781
|
+
});
|
|
782
|
+
expect(refresh).toBeDisabled();
|
|
783
|
+
expect(refresh).not.toHaveAttribute("title");
|
|
784
|
+
await act(async () => {
|
|
785
|
+
requests[0]!.respond(serviceMapPayload());
|
|
786
|
+
});
|
|
787
|
+
expect(refresh).toBeEnabled();
|
|
788
|
+
expect(refresh.getAttribute("title")).toContain("Last refreshed");
|
|
789
|
+
|
|
790
|
+
/* Infrastructure is loading: the button follows the ACTIVE tab. */
|
|
791
|
+
fireEvent.click(tab("Infrastructure"));
|
|
792
|
+
await waitFor(() => {
|
|
793
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
794
|
+
});
|
|
795
|
+
expect(
|
|
796
|
+
screen.getByRole("button", { name: "Refresh topology" }),
|
|
797
|
+
).toBeDisabled();
|
|
798
|
+
await act(async () => {
|
|
799
|
+
requestsTo(TopologyApiPath.Infrastructure)[0]!.respond(
|
|
800
|
+
infrastructurePayload(),
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
expect(
|
|
804
|
+
screen.getByRole("button", { name: "Refresh topology" }),
|
|
805
|
+
).toBeEnabled();
|
|
806
|
+
|
|
807
|
+
answer = defaultAnswer;
|
|
808
|
+
fireEvent.click(screen.getByRole("button", { name: "Refresh topology" }));
|
|
809
|
+
await screen.findByTestId("topology-infrastructure");
|
|
810
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(2);
|
|
811
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
812
|
+
|
|
813
|
+
/* The Service Map was dropped with the old generation: it loads again. */
|
|
814
|
+
fireEvent.click(tab("Service Map"));
|
|
815
|
+
await screen.findByTestId("topology-services");
|
|
816
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
/*
|
|
820
|
+
* The server keeps each map for a minute. The refresh button is the user
|
|
821
|
+
* asking for current data, so it — and nothing else — bypasses that cache,
|
|
822
|
+
* for the active tab and for the other tab's first load after it.
|
|
823
|
+
*/
|
|
824
|
+
test("refresh asks the server for fresh data; ordinary loads use its cache", async () => {
|
|
825
|
+
renderPage();
|
|
826
|
+
await screen.findByTestId("topology-services");
|
|
827
|
+
fireEvent.click(tab("Infrastructure"));
|
|
828
|
+
await screen.findByTestId("topology-infrastructure");
|
|
829
|
+
fireEvent.click(tab("Service Map"));
|
|
830
|
+
fireEvent.click(screen.getByTestId("topology-show-inactive"));
|
|
831
|
+
expect(
|
|
832
|
+
requests.map((request: RecordedRequest): boolean => {
|
|
833
|
+
return request.fresh;
|
|
834
|
+
}),
|
|
835
|
+
).toEqual([false, false]);
|
|
836
|
+
|
|
837
|
+
fireEvent.click(screen.getByRole("button", { name: "Refresh topology" }));
|
|
838
|
+
await waitFor(() => {
|
|
839
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
840
|
+
});
|
|
841
|
+
await screen.findByTestId("topology-services");
|
|
842
|
+
fireEvent.click(tab("Infrastructure"));
|
|
843
|
+
await screen.findByTestId("topology-infrastructure");
|
|
844
|
+
expect(
|
|
845
|
+
requests.map((request: RecordedRequest): string => {
|
|
846
|
+
return `${request.path}:${request.fresh ? "fresh" : "cached"}`;
|
|
847
|
+
}),
|
|
848
|
+
).toEqual([
|
|
849
|
+
`${TopologyApiPath.ServiceMap}:cached`,
|
|
850
|
+
`${TopologyApiPath.Infrastructure}:cached`,
|
|
851
|
+
`${TopologyApiPath.ServiceMap}:fresh`,
|
|
852
|
+
`${TopologyApiPath.Infrastructure}:fresh`,
|
|
853
|
+
]);
|
|
854
|
+
|
|
855
|
+
/* A new time range is an ordinary load again. */
|
|
856
|
+
fireEvent.click(screen.getByRole("button", { name: "Pick the past hour" }));
|
|
857
|
+
await waitFor(() => {
|
|
858
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(3);
|
|
859
|
+
});
|
|
860
|
+
await screen.findByTestId("topology-infrastructure");
|
|
861
|
+
expect(requests[requests.length - 1]!.fresh).toBe(false);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("a new time range reloads the active tab for that range", async () => {
|
|
865
|
+
renderPage();
|
|
866
|
+
await screen.findByTestId("topology-services");
|
|
867
|
+
const firstStart: string = requests[0]!.rangeStart;
|
|
868
|
+
fireEvent.click(screen.getByRole("button", { name: "Pick the past hour" }));
|
|
869
|
+
await waitFor(() => {
|
|
870
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(2);
|
|
871
|
+
});
|
|
872
|
+
const secondStart: string = requests[1]!.rangeStart;
|
|
873
|
+
expect(new Date(secondStart).getTime()).toBeGreaterThan(
|
|
874
|
+
new Date(firstStart).getTime(),
|
|
875
|
+
);
|
|
876
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(0);
|
|
877
|
+
expect(await screen.findByTestId("topology-services")).toBeVisible();
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
describe("the truncation banner", () => {
|
|
882
|
+
const SERVICE_MAP_NOTE: string =
|
|
883
|
+
"The map, counts and search cover what is shown.";
|
|
884
|
+
const INFRASTRUCTURE_NOTE: string =
|
|
885
|
+
"Counts are exact; the map and search cover the resources shown.";
|
|
886
|
+
|
|
887
|
+
test("appears only for the active tab's truncated payload, with exact totals", async () => {
|
|
888
|
+
answer = (request: RecordedRequest): Answer => {
|
|
889
|
+
return request.path === TopologyApiPath.ServiceMap
|
|
890
|
+
? serviceMapPayload({
|
|
891
|
+
entityTruncation: { shown: 50000, total: 61234 },
|
|
892
|
+
})
|
|
893
|
+
: infrastructurePayload();
|
|
894
|
+
};
|
|
895
|
+
renderPage();
|
|
896
|
+
await screen.findByTestId("topology-services");
|
|
897
|
+
const banner: HTMLElement = screen.getByTestId("topology-truncation");
|
|
898
|
+
expect(banner).toHaveAttribute("role", "status");
|
|
899
|
+
expect(banner).toHaveTextContent(
|
|
900
|
+
`${(50000).toLocaleString()} of ${(61234).toLocaleString()} resources shown.`,
|
|
901
|
+
);
|
|
902
|
+
/*
|
|
903
|
+
* The Service Map's tiles count what was shipped, so it must not claim
|
|
904
|
+
* its counts are exact.
|
|
905
|
+
*/
|
|
906
|
+
expect(banner).toHaveTextContent(SERVICE_MAP_NOTE);
|
|
907
|
+
expect(banner).not.toHaveTextContent("Counts are exact");
|
|
908
|
+
expect(banner).not.toHaveTextContent("connections shown.");
|
|
909
|
+
expect(banner).not.toHaveTextContent("Partial inventory");
|
|
910
|
+
|
|
911
|
+
fireEvent.click(tab("Infrastructure"));
|
|
912
|
+
await screen.findByTestId("topology-infrastructure");
|
|
913
|
+
expect(screen.queryByTestId("topology-truncation")).not.toBeInTheDocument();
|
|
914
|
+
|
|
915
|
+
fireEvent.click(tab("Network"));
|
|
916
|
+
expect(screen.queryByTestId("topology-truncation")).not.toBeInTheDocument();
|
|
917
|
+
|
|
918
|
+
fireEvent.click(tab("Service Map"));
|
|
919
|
+
expect(screen.getByTestId("topology-truncation")).toBeVisible();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
/*
|
|
923
|
+
* The dependency cap limits connection rows, not resources: calling
|
|
924
|
+
* 200,000 connections "resources" would misstate what is missing.
|
|
925
|
+
*/
|
|
926
|
+
test("a Service Map with only its connections capped says connections", async () => {
|
|
927
|
+
answer = (request: RecordedRequest): Answer => {
|
|
928
|
+
return request.path === TopologyApiPath.ServiceMap
|
|
929
|
+
? serviceMapPayload({
|
|
930
|
+
dependencyTruncation: { shown: 200000, total: 250001 },
|
|
931
|
+
})
|
|
932
|
+
: infrastructurePayload();
|
|
933
|
+
};
|
|
934
|
+
renderPage();
|
|
935
|
+
await screen.findByTestId("topology-services");
|
|
936
|
+
const banner: HTMLElement = screen.getByTestId("topology-truncation");
|
|
937
|
+
expect(banner.textContent).toBe(
|
|
938
|
+
`${(200000).toLocaleString()} of ${(250001).toLocaleString()} connections shown. ${SERVICE_MAP_NOTE}`,
|
|
939
|
+
);
|
|
940
|
+
expect(banner).not.toHaveTextContent("resources shown.");
|
|
941
|
+
expect(banner).not.toHaveTextContent("Counts are exact");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("a Service Map with both caps hit reports both", async () => {
|
|
945
|
+
answer = (): Answer => {
|
|
946
|
+
return serviceMapPayload({
|
|
947
|
+
entityTruncation: { shown: 50000, total: 50001 },
|
|
948
|
+
dependencyTruncation: { shown: 200000, total: 200002 },
|
|
949
|
+
});
|
|
950
|
+
};
|
|
951
|
+
renderPage();
|
|
952
|
+
await screen.findByTestId("topology-services");
|
|
953
|
+
expect(screen.getByTestId("topology-truncation").textContent).toBe(
|
|
954
|
+
`${(50000).toLocaleString()} of ${(50001).toLocaleString()} resources shown. ` +
|
|
955
|
+
`${(200000).toLocaleString()} of ${(200002).toLocaleString()} connections shown. ` +
|
|
956
|
+
SERVICE_MAP_NOTE,
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
/*
|
|
961
|
+
* Every word of the banner goes through translation, and no number is
|
|
962
|
+
* baked into a key: the fragments are translated, the figures are not.
|
|
963
|
+
*/
|
|
964
|
+
test("the banner is built from translated fragments with the numbers outside them", async () => {
|
|
965
|
+
translate = (value: string): string => {
|
|
966
|
+
return `«${value}»`;
|
|
967
|
+
};
|
|
968
|
+
answer = (): Answer => {
|
|
969
|
+
return serviceMapPayload({
|
|
970
|
+
entityTruncation: { shown: 50000, total: 50001 },
|
|
971
|
+
dependencyTruncation: { shown: 200000, total: 200002 },
|
|
972
|
+
});
|
|
973
|
+
};
|
|
974
|
+
renderPage();
|
|
975
|
+
await screen.findByTestId("topology-services");
|
|
976
|
+
expect(screen.getByTestId("topology-truncation").textContent).toBe(
|
|
977
|
+
`${(50000).toLocaleString()} «of» ${(50001).toLocaleString()} «resources shown.» ` +
|
|
978
|
+
`${(200000).toLocaleString()} «of» ${(200002).toLocaleString()} «connections shown.» ` +
|
|
979
|
+
`«${SERVICE_MAP_NOTE}»`,
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test("an Infrastructure cap shows on Infrastructure only, and reaches the explorer", async () => {
|
|
984
|
+
answer = (request: RecordedRequest): Answer => {
|
|
985
|
+
return request.path === TopologyApiPath.Infrastructure
|
|
986
|
+
? infrastructurePayload({
|
|
987
|
+
truncation: { shown: 200000, total: 212345 },
|
|
988
|
+
})
|
|
989
|
+
: serviceMapPayload();
|
|
990
|
+
};
|
|
991
|
+
renderPage();
|
|
992
|
+
await screen.findByTestId("topology-services");
|
|
993
|
+
expect(screen.queryByTestId("topology-truncation")).not.toBeInTheDocument();
|
|
994
|
+
fireEvent.click(tab("Infrastructure"));
|
|
995
|
+
await screen.findByTestId("topology-infrastructure");
|
|
996
|
+
expect(screen.getByTestId("topology-truncation").textContent).toBe(
|
|
997
|
+
`${(200000).toLocaleString()} of ${(212345).toLocaleString()} resources shown. ${INFRASTRUCTURE_NOTE}`,
|
|
998
|
+
);
|
|
999
|
+
/* Only Infrastructure's summary switches to the server's exact totals. */
|
|
1000
|
+
expect(screen.getByTestId("topology-truncation")).not.toHaveTextContent(
|
|
1001
|
+
SERVICE_MAP_NOTE,
|
|
1002
|
+
);
|
|
1003
|
+
expect(screen.getByTestId("infrastructure-truncation")).toHaveTextContent(
|
|
1004
|
+
"200000/212345",
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test("no banner while the tab is still loading", async () => {
|
|
1009
|
+
answer = (): Answer => {
|
|
1010
|
+
return "hold";
|
|
1011
|
+
};
|
|
1012
|
+
renderPage();
|
|
1013
|
+
await screen.findByText("Loading service map…");
|
|
1014
|
+
expect(screen.queryByTestId("topology-truncation")).not.toBeInTheDocument();
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
describe("show inactive and cross-tab links", () => {
|
|
319
1019
|
test("show inactive reaches both telemetry views and the shareable URL", async () => {
|
|
320
1020
|
renderPage();
|
|
321
1021
|
await screen.findByTestId("topology-services");
|
|
322
1022
|
expect(screen.getByTestId("services-inactive")).toHaveTextContent("false");
|
|
323
|
-
expect(screen.getByTestId("services-range-start")).toHaveTextContent("set");
|
|
324
1023
|
fireEvent.click(screen.getByTestId("topology-show-inactive"));
|
|
325
1024
|
expect(screen.getByTestId("services-inactive")).toHaveTextContent("true");
|
|
326
1025
|
expect(new URLSearchParams(window.location.search).get("inactive")).toBe(
|
|
327
1026
|
"show",
|
|
328
1027
|
);
|
|
329
|
-
fireEvent.click(
|
|
330
|
-
expect(
|
|
331
|
-
"
|
|
332
|
-
);
|
|
1028
|
+
fireEvent.click(tab("Infrastructure"));
|
|
1029
|
+
expect(
|
|
1030
|
+
await screen.findByTestId("infrastructure-inactive"),
|
|
1031
|
+
).toHaveTextContent("true");
|
|
333
1032
|
fireEvent.click(screen.getByTestId("topology-show-inactive"));
|
|
334
1033
|
expect(new URLSearchParams(window.location.search).has("inactive")).toBe(
|
|
335
1034
|
false,
|
|
336
1035
|
);
|
|
1036
|
+
/* The toggle is client-side: it never refetches. */
|
|
1037
|
+
expect(requests).toHaveLength(2);
|
|
337
1038
|
});
|
|
338
1039
|
|
|
339
1040
|
test("a shared link restores show inactive", async () => {
|
|
@@ -345,51 +1046,54 @@ describe("topology map navigation and data isolation", () => {
|
|
|
345
1046
|
expect(screen.getByTestId("topology-show-inactive")).toBeChecked();
|
|
346
1047
|
});
|
|
347
1048
|
|
|
348
|
-
test("the Service Map hands a resource to Infrastructure, and back", async () => {
|
|
1049
|
+
test("the Service Map hands a resource to Infrastructure, which loads once, and back", async () => {
|
|
349
1050
|
renderPage();
|
|
350
1051
|
await screen.findByTestId("topology-services");
|
|
351
1052
|
fireEvent.click(
|
|
352
1053
|
screen.getByRole("button", { name: "Open pod in infrastructure" }),
|
|
353
1054
|
);
|
|
354
|
-
expect(
|
|
355
|
-
"aria-selected",
|
|
356
|
-
"true",
|
|
357
|
-
);
|
|
1055
|
+
expect(tab("Infrastructure")).toHaveAttribute("aria-selected", "true");
|
|
358
1056
|
let query: URLSearchParams = new URLSearchParams(window.location.search);
|
|
359
1057
|
expect(query.get("tab")).toBe("Infrastructure");
|
|
360
1058
|
expect(query.get("infraFocus")).toBe("pod-1");
|
|
1059
|
+
expect(await screen.findByTestId("topology-infrastructure")).toBeVisible();
|
|
361
1060
|
|
|
362
1061
|
fireEvent.click(
|
|
363
1062
|
screen.getByRole("button", { name: "Open service on map" }),
|
|
364
1063
|
);
|
|
365
|
-
expect(
|
|
366
|
-
"aria-selected",
|
|
367
|
-
"true",
|
|
368
|
-
);
|
|
1064
|
+
expect(tab("Service Map")).toHaveAttribute("aria-selected", "true");
|
|
369
1065
|
query = new URLSearchParams(window.location.search);
|
|
370
1066
|
expect(query.has("tab")).toBe(false);
|
|
371
1067
|
expect(query.get("focus")).toBe("checkout");
|
|
372
1068
|
expect(query.get("serviceView")).toBe("map");
|
|
373
|
-
|
|
374
|
-
|
|
1069
|
+
expect(screen.getByTestId("topology-services")).toBeVisible();
|
|
1070
|
+
|
|
1071
|
+
/* Round trip again: still no refetch of either tab. */
|
|
1072
|
+
fireEvent.click(
|
|
1073
|
+
screen.getByRole("button", { name: "Open pod in infrastructure" }),
|
|
1074
|
+
);
|
|
1075
|
+
expect(screen.getByTestId("topology-infrastructure")).toBeVisible();
|
|
1076
|
+
expect(requestsTo(TopologyApiPath.ServiceMap)).toHaveLength(1);
|
|
1077
|
+
expect(requestsTo(TopologyApiPath.Infrastructure)).toHaveLength(1);
|
|
375
1078
|
});
|
|
376
1079
|
|
|
377
1080
|
test("keyboard navigation switches maps and updates the shareable tab", async () => {
|
|
378
1081
|
renderPage();
|
|
379
1082
|
await screen.findByTestId("topology-services");
|
|
380
|
-
const serviceTab: HTMLElement =
|
|
381
|
-
name: "Service Map",
|
|
382
|
-
});
|
|
1083
|
+
const serviceTab: HTMLElement = tab("Service Map");
|
|
383
1084
|
serviceTab.focus();
|
|
384
1085
|
fireEvent.keyDown(serviceTab, { key: "ArrowRight" });
|
|
385
1086
|
await waitFor(() => {
|
|
386
|
-
expect(
|
|
387
|
-
screen.getByRole("tab", { name: "Infrastructure" }),
|
|
388
|
-
).toHaveAttribute("aria-selected", "true");
|
|
1087
|
+
expect(tab("Infrastructure")).toHaveAttribute("aria-selected", "true");
|
|
389
1088
|
});
|
|
390
1089
|
expect(new URLSearchParams(window.location.search).get("tab")).toBe(
|
|
391
1090
|
"Infrastructure",
|
|
392
1091
|
);
|
|
393
|
-
expect(
|
|
1092
|
+
expect(tab("Infrastructure")).toHaveFocus();
|
|
1093
|
+
await screen.findByTestId("topology-infrastructure");
|
|
1094
|
+
fireEvent.keyDown(tab("Infrastructure"), { key: "End" });
|
|
1095
|
+
await waitFor(() => {
|
|
1096
|
+
expect(tab("Network")).toHaveAttribute("aria-selected", "true");
|
|
1097
|
+
});
|
|
394
1098
|
});
|
|
395
1099
|
});
|