@oneuptime/common 14.0.6 → 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/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- 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/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- 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/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -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/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -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/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/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/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- 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/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.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/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
RenderResult,
|
|
16
|
+
screen,
|
|
17
|
+
waitFor,
|
|
18
|
+
within,
|
|
19
|
+
} from "@testing-library/react";
|
|
20
|
+
import * as React from "react";
|
|
21
|
+
import type { SpyInstance } from "jest-mock";
|
|
22
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Lifecycle edges of the topology detail drawer that EntityDetailPanel.test
|
|
26
|
+
* does not reach:
|
|
27
|
+
*
|
|
28
|
+
* - React 18 StrictMode replays the drawer's mount effects in development.
|
|
29
|
+
* It must come out with one live entity request (the discarded one
|
|
30
|
+
* aborted and ignored), resolve its best-effort Service link once, and
|
|
31
|
+
* append a "Show more" page once — its state updates are replayed too.
|
|
32
|
+
* - Closing the drawer mid-"Show more" aborts the page request, and a page
|
|
33
|
+
* or a best-effort lookup that finishes afterwards changes nothing.
|
|
34
|
+
*
|
|
35
|
+
* Same seam as EntityDetailPanel.test: the real request and decoding code
|
|
36
|
+
* behind a fake API.post.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
40
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
41
|
+
|
|
42
|
+
// The arrow wrappers are load bearing: jest.mock is hoisted above the mocks.
|
|
43
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
44
|
+
return {
|
|
45
|
+
__esModule: true,
|
|
46
|
+
default: {
|
|
47
|
+
post: (...args: Array<any>) => {
|
|
48
|
+
return postMock(...args);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
54
|
+
return {
|
|
55
|
+
__esModule: true,
|
|
56
|
+
default: {
|
|
57
|
+
getList: (...args: Array<any>) => {
|
|
58
|
+
return getListMock(...args);
|
|
59
|
+
},
|
|
60
|
+
getCommonHeaders: () => {
|
|
61
|
+
return { tenantid: "project-1" };
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
jest.mock("../../../UI/Utils/Translation", () => {
|
|
67
|
+
return {
|
|
68
|
+
__esModule: true,
|
|
69
|
+
default: () => {
|
|
70
|
+
return {
|
|
71
|
+
translateString: (value: string) => {
|
|
72
|
+
return value;
|
|
73
|
+
},
|
|
74
|
+
translateValue: (value: React.ReactNode) => {
|
|
75
|
+
return value;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
jest.mock("../../../UI/Components/SideOver/SideOver", () => {
|
|
82
|
+
return {
|
|
83
|
+
__esModule: true,
|
|
84
|
+
SideOverSize: { Small: "small" },
|
|
85
|
+
default: (props: {
|
|
86
|
+
title: string;
|
|
87
|
+
description: string;
|
|
88
|
+
children: React.ReactNode;
|
|
89
|
+
}) => {
|
|
90
|
+
return (
|
|
91
|
+
<section aria-label={props.title}>
|
|
92
|
+
<p data-testid="drawer-description">{props.description}</p>
|
|
93
|
+
{props.children}
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
import EntityDetailPanel from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/EntityDetailPanel";
|
|
101
|
+
import { EntityDetailTarget } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
|
|
102
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
103
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
104
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
105
|
+
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
106
|
+
import {
|
|
107
|
+
TOPOLOGY_API_FORMAT_VERSION,
|
|
108
|
+
TopologyConnectionRowJSON,
|
|
109
|
+
TopologyConnectionSection,
|
|
110
|
+
TopologyConnectionSectionJSON,
|
|
111
|
+
TopologyEntityConnectionsResponseJSON,
|
|
112
|
+
TopologyEntityDetailJSON,
|
|
113
|
+
TopologyEntityResponseJSON,
|
|
114
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
115
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
116
|
+
|
|
117
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
118
|
+
"5b2f5b1c-0000-4000-8000-000000000001",
|
|
119
|
+
);
|
|
120
|
+
const SERVICE_ID: string = "5e7f0000-0000-4000-8000-000000000001";
|
|
121
|
+
const ITEM_ID: string = "1a1a0000-0000-4000-8000-000000000001";
|
|
122
|
+
const RANGE_START: Date = new Date("2026-09-26T10:00:00.000Z");
|
|
123
|
+
|
|
124
|
+
interface PostRequest {
|
|
125
|
+
url: { toString: () => string };
|
|
126
|
+
data: JSONObject;
|
|
127
|
+
options: { signal?: AbortSignal };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* One request the fake server received, answerable by the test. */
|
|
131
|
+
interface Pending {
|
|
132
|
+
request: PostRequest;
|
|
133
|
+
resolve: (value: JSONObject) => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let pending: Array<Pending> = [];
|
|
137
|
+
|
|
138
|
+
function asJSON(value: unknown): JSONObject {
|
|
139
|
+
return JSON.parse(JSON.stringify(value)) as JSONObject;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function entityRow(
|
|
143
|
+
overrides: Partial<TopologyEntityDetailJSON> = {},
|
|
144
|
+
): TopologyEntityDetailJSON {
|
|
145
|
+
return {
|
|
146
|
+
id: ITEM_ID,
|
|
147
|
+
key: "svc",
|
|
148
|
+
type: EntityType.Service,
|
|
149
|
+
name: "Checkout API",
|
|
150
|
+
source: "telemetry",
|
|
151
|
+
lastSeenAt: Date.parse("2026-09-26T10:14:00.000Z"),
|
|
152
|
+
firstSeenAt: Date.parse("2026-08-01T00:00:00.000Z"),
|
|
153
|
+
/* No Service pointer: the traces link is looked up by name. */
|
|
154
|
+
resourceType: null,
|
|
155
|
+
resourceId: null,
|
|
156
|
+
identifyingAttributes: { "service.name": "Checkout API" },
|
|
157
|
+
descriptiveAttributes: {},
|
|
158
|
+
...overrides,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function pod(index: number): TopologyConnectionRowJSON {
|
|
163
|
+
return {
|
|
164
|
+
relationshipType: "runs-on",
|
|
165
|
+
direction: "out",
|
|
166
|
+
otherKey: `pod-${index}`,
|
|
167
|
+
otherKnown: true,
|
|
168
|
+
otherName: `checkout-pod-${index}`,
|
|
169
|
+
otherType: EntityType.KubernetesPod,
|
|
170
|
+
callCount: null,
|
|
171
|
+
errorCount: null,
|
|
172
|
+
avgDurationMs: null,
|
|
173
|
+
lastSeenAt: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pods(from: number, to: number): Array<TopologyConnectionRowJSON> {
|
|
178
|
+
const rows: Array<TopologyConnectionRowJSON> = [];
|
|
179
|
+
for (let index: number = from; index <= to; index++) {
|
|
180
|
+
rows.push(pod(index));
|
|
181
|
+
}
|
|
182
|
+
return rows;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sectionOf(
|
|
186
|
+
rows: Array<TopologyConnectionRowJSON>,
|
|
187
|
+
overrides: Partial<TopologyConnectionSectionJSON> = {},
|
|
188
|
+
): TopologyConnectionSectionJSON {
|
|
189
|
+
return {
|
|
190
|
+
total: rows.length,
|
|
191
|
+
unknownTotal: 0,
|
|
192
|
+
rows: rows,
|
|
193
|
+
nextOffset: null,
|
|
194
|
+
...overrides,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function entityResponse(
|
|
199
|
+
entity: TopologyEntityDetailJSON | null,
|
|
200
|
+
runsOn: TopologyConnectionSectionJSON = sectionOf(pods(1, 25), {
|
|
201
|
+
total: 40,
|
|
202
|
+
nextOffset: 25,
|
|
203
|
+
}),
|
|
204
|
+
): JSONObject {
|
|
205
|
+
const empty: TopologyConnectionSectionJSON = sectionOf([]);
|
|
206
|
+
const response: TopologyEntityResponseJSON = {
|
|
207
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
208
|
+
rangeStart: RANGE_START.toISOString(),
|
|
209
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
210
|
+
entity: entity,
|
|
211
|
+
sections: {
|
|
212
|
+
calls: empty,
|
|
213
|
+
calledBy: empty,
|
|
214
|
+
runsOn: runsOn,
|
|
215
|
+
related: empty,
|
|
216
|
+
},
|
|
217
|
+
isScanLimited: false,
|
|
218
|
+
};
|
|
219
|
+
return asJSON(response);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function connectionsResponse(
|
|
223
|
+
section: TopologyConnectionSection,
|
|
224
|
+
connections: TopologyConnectionSectionJSON,
|
|
225
|
+
): JSONObject {
|
|
226
|
+
const response: TopologyEntityConnectionsResponseJSON = {
|
|
227
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
228
|
+
rangeStart: RANGE_START.toISOString(),
|
|
229
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
230
|
+
section: section,
|
|
231
|
+
connections: connections,
|
|
232
|
+
isScanLimited: false,
|
|
233
|
+
};
|
|
234
|
+
return asJSON(response);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isConnectionsRequest(request: PostRequest): boolean {
|
|
238
|
+
return request.url.toString().endsWith("/entity/connections");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function entityRequests(): Array<Pending> {
|
|
242
|
+
return pending.filter((entry: Pending): boolean => {
|
|
243
|
+
return !isConnectionsRequest(entry.request);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function pageRequests(): Array<Pending> {
|
|
248
|
+
return pending.filter((entry: Pending): boolean => {
|
|
249
|
+
return isConnectionsRequest(entry.request);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isLive(entry: Pending): boolean {
|
|
254
|
+
return !entry.request.options.signal?.aborted;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Every request waits until the test answers it. */
|
|
258
|
+
function serveOnDemand(): void {
|
|
259
|
+
postMock.mockImplementation((...args: Array<unknown>) => {
|
|
260
|
+
const request: PostRequest = args[0] as PostRequest;
|
|
261
|
+
return new Promise<HTTPResponse<JSONObject>>(
|
|
262
|
+
(resolve: (value: HTTPResponse<JSONObject>) => void): void => {
|
|
263
|
+
pending.push({
|
|
264
|
+
request,
|
|
265
|
+
resolve: (value: JSONObject): void => {
|
|
266
|
+
resolve(new HTTPResponse<JSONObject>(200, value, {}));
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function serviceLookups(): Array<Array<unknown>> {
|
|
275
|
+
return getListMock.mock.calls.filter((call: Array<unknown>): boolean => {
|
|
276
|
+
return (
|
|
277
|
+
(call[0] as { modelType: { name: string } }).modelType.name === "Service"
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function runsOnRows(): Array<HTMLElement> {
|
|
283
|
+
return within(screen.getByTestId("entity-detail-runs-on")).getAllByRole(
|
|
284
|
+
"listitem",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const CHECKOUT: EntityDetailTarget = {
|
|
289
|
+
entityKey: "svc",
|
|
290
|
+
entityType: EntityType.Service,
|
|
291
|
+
displayName: "Checkout API",
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
function panel(): React.ReactElement {
|
|
295
|
+
return (
|
|
296
|
+
<EntityDetailPanel
|
|
297
|
+
entity={CHECKOUT}
|
|
298
|
+
rangeStart={RANGE_START}
|
|
299
|
+
metricsWindowSeconds={60}
|
|
300
|
+
onClose={() => {
|
|
301
|
+
return undefined;
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function strictPanel(): React.ReactElement {
|
|
308
|
+
return <React.StrictMode>{panel()}</React.StrictMode>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let consoleError: SpyInstance<(...data: Array<unknown>) => void>;
|
|
312
|
+
|
|
313
|
+
/*
|
|
314
|
+
* console.error calls other than the testing library's one-time
|
|
315
|
+
* "ReactDOMTestUtils.act is deprecated" notice, which lands in whichever
|
|
316
|
+
* test first calls act().
|
|
317
|
+
*/
|
|
318
|
+
function unexpectedConsoleErrors(): Array<string> {
|
|
319
|
+
return consoleError.mock.calls
|
|
320
|
+
.map((call: Array<unknown>): string => {
|
|
321
|
+
return call.map(String).join(" ");
|
|
322
|
+
})
|
|
323
|
+
.filter((message: string): boolean => {
|
|
324
|
+
return !message.includes("ReactDOMTestUtils.act");
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
beforeEach(() => {
|
|
329
|
+
pending = [];
|
|
330
|
+
postMock.mockReset();
|
|
331
|
+
getListMock.mockReset();
|
|
332
|
+
getListMock.mockImplementation(async () => {
|
|
333
|
+
return { data: [{ _id: SERVICE_ID }], count: 1 };
|
|
334
|
+
});
|
|
335
|
+
serveOnDemand();
|
|
336
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
337
|
+
consoleError = jest.spyOn(console, "error");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
afterEach(() => {
|
|
341
|
+
cleanup();
|
|
342
|
+
jest.restoreAllMocks();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("StrictMode's double mount", () => {
|
|
346
|
+
test("leaves one live entity request, and the discarded one cannot land", async () => {
|
|
347
|
+
render(strictPanel());
|
|
348
|
+
|
|
349
|
+
/*
|
|
350
|
+
* React 18 replays mount effects under StrictMode in development: the
|
|
351
|
+
* drawer asked twice, and aborted the first ask.
|
|
352
|
+
*/
|
|
353
|
+
expect(entityRequests()).toHaveLength(2);
|
|
354
|
+
const [discarded, current]: Array<Pending> = entityRequests();
|
|
355
|
+
expect(isLive(discarded!)).toBe(false);
|
|
356
|
+
expect(isLive(current!)).toBe(true);
|
|
357
|
+
expect(current!.request.data).toEqual(discarded!.request.data);
|
|
358
|
+
|
|
359
|
+
await act(async () => {
|
|
360
|
+
discarded!.resolve(
|
|
361
|
+
entityResponse(entityRow({ name: "Stale checkout" }), sectionOf([])),
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
/* Still loading, still the preview's name. */
|
|
365
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
366
|
+
"Loading connections…",
|
|
367
|
+
);
|
|
368
|
+
expect(
|
|
369
|
+
screen.getByRole("region", { name: "Checkout API" }),
|
|
370
|
+
).toBeInTheDocument();
|
|
371
|
+
expect(screen.queryByText("Stale checkout")).not.toBeInTheDocument();
|
|
372
|
+
|
|
373
|
+
await act(async () => {
|
|
374
|
+
current!.resolve(entityResponse(entityRow()));
|
|
375
|
+
});
|
|
376
|
+
await waitFor(() => {
|
|
377
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
expect(runsOnRows()).toHaveLength(25);
|
|
380
|
+
expect(entityRequests()).toHaveLength(2);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("resolves the best-effort Service link once, from the full row", async () => {
|
|
384
|
+
render(strictPanel());
|
|
385
|
+
/* The preview alone never triggers a lookup, replayed or not. */
|
|
386
|
+
expect(serviceLookups()).toHaveLength(0);
|
|
387
|
+
|
|
388
|
+
await act(async () => {
|
|
389
|
+
entityRequests()[1]!.resolve(entityResponse(entityRow()));
|
|
390
|
+
});
|
|
391
|
+
const link: HTMLElement = await screen.findByText(
|
|
392
|
+
"Traces for this service",
|
|
393
|
+
);
|
|
394
|
+
expect(link.closest("a")?.getAttribute("href")).toContain(SERVICE_ID);
|
|
395
|
+
expect(serviceLookups()).toHaveLength(1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("a Show more page is requested once and appended once", async () => {
|
|
399
|
+
render(strictPanel());
|
|
400
|
+
await act(async () => {
|
|
401
|
+
entityRequests()[1]!.resolve(entityResponse(entityRow()));
|
|
402
|
+
});
|
|
403
|
+
await waitFor(() => {
|
|
404
|
+
expect(runsOnRows()).toHaveLength(25);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
408
|
+
expect(pageRequests()).toHaveLength(1);
|
|
409
|
+
|
|
410
|
+
await act(async () => {
|
|
411
|
+
pageRequests()[0]!.resolve(
|
|
412
|
+
connectionsResponse(
|
|
413
|
+
"runsOn",
|
|
414
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await waitFor(() => {
|
|
420
|
+
expect(runsOnRows()).toHaveLength(40);
|
|
421
|
+
});
|
|
422
|
+
/* Replayed state updates must not append the page twice. */
|
|
423
|
+
const keys: Array<string> = runsOnRows().map((row: HTMLElement): string => {
|
|
424
|
+
return row.textContent || "";
|
|
425
|
+
});
|
|
426
|
+
expect(new Set<string>(keys).size).toBe(40);
|
|
427
|
+
expect(pageRequests()).toHaveLength(1);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("closing the drawer", () => {
|
|
432
|
+
test("mid-Show more aborts the page request, and its late answer changes nothing", async () => {
|
|
433
|
+
const view: RenderResult = render(panel());
|
|
434
|
+
await act(async () => {
|
|
435
|
+
entityRequests()[0]!.resolve(entityResponse(entityRow()));
|
|
436
|
+
});
|
|
437
|
+
await waitFor(() => {
|
|
438
|
+
expect(runsOnRows()).toHaveLength(25);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
fireEvent.click(screen.getByRole("button", { name: "Show more: Runs on" }));
|
|
442
|
+
expect(pageRequests()).toHaveLength(1);
|
|
443
|
+
const page: Pending = pageRequests()[0]!;
|
|
444
|
+
expect(isLive(page)).toBe(true);
|
|
445
|
+
|
|
446
|
+
/* Only what happens from here on is under test. */
|
|
447
|
+
consoleError.mockClear();
|
|
448
|
+
view.unmount();
|
|
449
|
+
|
|
450
|
+
expect(isLive(page)).toBe(false);
|
|
451
|
+
await act(async () => {
|
|
452
|
+
page.resolve(
|
|
453
|
+
connectionsResponse(
|
|
454
|
+
"runsOn",
|
|
455
|
+
sectionOf(pods(26, 40), { total: 40, nextOffset: null }),
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(postMock).toHaveBeenCalledTimes(2);
|
|
461
|
+
expect(unexpectedConsoleErrors()).toEqual([]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("while the best-effort Service lookup is pending drops its answer quietly", async () => {
|
|
465
|
+
let finishLookup: (value: unknown) => void = () => {
|
|
466
|
+
return undefined;
|
|
467
|
+
};
|
|
468
|
+
getListMock.mockImplementation(() => {
|
|
469
|
+
return new Promise<unknown>((resolve: (value: unknown) => void) => {
|
|
470
|
+
finishLookup = resolve;
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const view: RenderResult = render(panel());
|
|
475
|
+
await act(async () => {
|
|
476
|
+
entityRequests()[0]!.resolve(entityResponse(entityRow()));
|
|
477
|
+
});
|
|
478
|
+
await waitFor(() => {
|
|
479
|
+
expect(serviceLookups()).toHaveLength(1);
|
|
480
|
+
});
|
|
481
|
+
expect(screen.queryByText("Traces for this service")).toBeNull();
|
|
482
|
+
|
|
483
|
+
/* Only what happens from here on is under test. */
|
|
484
|
+
consoleError.mockClear();
|
|
485
|
+
view.unmount();
|
|
486
|
+
await act(async () => {
|
|
487
|
+
finishLookup({ data: [{ _id: SERVICE_ID }], count: 1 });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(serviceLookups()).toHaveLength(1);
|
|
491
|
+
expect(unexpectedConsoleErrors()).toEqual([]);
|
|
492
|
+
});
|
|
493
|
+
});
|