@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,439 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
screen,
|
|
16
|
+
waitFor,
|
|
17
|
+
} from "@testing-library/react";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Moving from entity to entity inside one open drawer, with the REAL
|
|
23
|
+
* SideOver. Both maps keep a single drawer mounted while the user opens a
|
|
24
|
+
* connection row or clicks another node: a remount per entity replayed the
|
|
25
|
+
* side-over's slide-in and, because the row that had focus went away with
|
|
26
|
+
* the old drawer, dropped keyboard focus to <body> — the next Tab went to
|
|
27
|
+
* the top of the page. Pinned here:
|
|
28
|
+
*
|
|
29
|
+
* - the side-over is the same element before and after the hop, and it
|
|
30
|
+
* does not slide in again;
|
|
31
|
+
* - focus that was in the drawer (or lost) lands on the drawer's focus
|
|
32
|
+
* target, which names the new entity for screen readers — also when the
|
|
33
|
+
* new entity turns out to be gone from inventory;
|
|
34
|
+
* - focus the user put outside the drawer stays there, and opening the
|
|
35
|
+
* drawer for the first entity takes no focus.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
39
|
+
|
|
40
|
+
// The arrow wrappers are load bearing: jest.mock is hoisted above the mocks.
|
|
41
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
42
|
+
return {
|
|
43
|
+
__esModule: true,
|
|
44
|
+
default: {
|
|
45
|
+
post: (...args: Array<any>) => {
|
|
46
|
+
return postMock(...args);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
52
|
+
return {
|
|
53
|
+
__esModule: true,
|
|
54
|
+
default: {
|
|
55
|
+
getList: async () => {
|
|
56
|
+
return { data: [], count: 0 };
|
|
57
|
+
},
|
|
58
|
+
getCommonHeaders: () => {
|
|
59
|
+
return { tenantid: "project-1" };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
jest.mock("../../../UI/Utils/Translation", () => {
|
|
65
|
+
return {
|
|
66
|
+
__esModule: true,
|
|
67
|
+
default: () => {
|
|
68
|
+
return {
|
|
69
|
+
translateString: (value: string) => {
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
translateValue: (value: React.ReactNode) => {
|
|
73
|
+
return value;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
import EntityDetailPanel from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/EntityDetailPanel";
|
|
81
|
+
import { EntityDetailTarget } from "../../../../App/FeatureSet/Dashboard/src/Components/Topology/TopologyData";
|
|
82
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
83
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
84
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
85
|
+
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
86
|
+
import {
|
|
87
|
+
TOPOLOGY_API_FORMAT_VERSION,
|
|
88
|
+
TopologyConnectionRowJSON,
|
|
89
|
+
TopologyConnectionSectionJSON,
|
|
90
|
+
TopologyEntityDetailJSON,
|
|
91
|
+
TopologyEntityResponseJSON,
|
|
92
|
+
} from "../../../Types/Topology/TopologyApi";
|
|
93
|
+
import { resetPageScrollLockForTesting } from "../../../UI/Utils/PageScrollLock";
|
|
94
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
95
|
+
|
|
96
|
+
const RANGE_START: Date = new Date("2026-09-26T10:00:00.000Z");
|
|
97
|
+
|
|
98
|
+
interface PostRequest {
|
|
99
|
+
url: { toString: () => string };
|
|
100
|
+
data: JSONObject;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Requests for these keys wait until the test answers them. */
|
|
104
|
+
const held: Map<string, (value: JSONObject) => void> = new Map<
|
|
105
|
+
string,
|
|
106
|
+
(value: JSONObject) => void
|
|
107
|
+
>();
|
|
108
|
+
|
|
109
|
+
function asJSON(value: unknown): JSONObject {
|
|
110
|
+
return JSON.parse(JSON.stringify(value)) as JSONObject;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sectionOf(
|
|
114
|
+
rows: Array<TopologyConnectionRowJSON>,
|
|
115
|
+
): TopologyConnectionSectionJSON {
|
|
116
|
+
return { total: rows.length, unknownTotal: 0, rows: rows, nextOffset: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function call(
|
|
120
|
+
direction: "out" | "in",
|
|
121
|
+
otherKey: string,
|
|
122
|
+
otherName: string,
|
|
123
|
+
otherType: string,
|
|
124
|
+
): TopologyConnectionRowJSON {
|
|
125
|
+
return {
|
|
126
|
+
relationshipType: "depends-on",
|
|
127
|
+
direction: direction,
|
|
128
|
+
otherKey: otherKey,
|
|
129
|
+
otherKnown: true,
|
|
130
|
+
otherName: otherName,
|
|
131
|
+
otherType: otherType,
|
|
132
|
+
callCount: null,
|
|
133
|
+
errorCount: null,
|
|
134
|
+
avgDurationMs: null,
|
|
135
|
+
lastSeenAt: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function entityRow(
|
|
140
|
+
key: string,
|
|
141
|
+
type: string,
|
|
142
|
+
name: string,
|
|
143
|
+
): TopologyEntityDetailJSON {
|
|
144
|
+
return {
|
|
145
|
+
id: new ObjectID(
|
|
146
|
+
`1a1a0000-0000-4000-8000-${String(key.length).padStart(12, "0")}`,
|
|
147
|
+
).toString(),
|
|
148
|
+
key: key,
|
|
149
|
+
type: type,
|
|
150
|
+
name: name,
|
|
151
|
+
source: "telemetry",
|
|
152
|
+
lastSeenAt: Date.parse("2026-09-26T10:14:00.000Z"),
|
|
153
|
+
firstSeenAt: null,
|
|
154
|
+
resourceType: null,
|
|
155
|
+
resourceId: null,
|
|
156
|
+
identifyingAttributes: {},
|
|
157
|
+
descriptiveAttributes: {},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function entityResponse(
|
|
162
|
+
entity: TopologyEntityDetailJSON | null,
|
|
163
|
+
calls: Array<TopologyConnectionRowJSON>,
|
|
164
|
+
calledBy: Array<TopologyConnectionRowJSON>,
|
|
165
|
+
): JSONObject {
|
|
166
|
+
const response: TopologyEntityResponseJSON = {
|
|
167
|
+
formatVersion: TOPOLOGY_API_FORMAT_VERSION,
|
|
168
|
+
rangeStart: RANGE_START.toISOString(),
|
|
169
|
+
generatedAt: "2026-09-26T10:15:00.000Z",
|
|
170
|
+
entity: entity,
|
|
171
|
+
sections: {
|
|
172
|
+
calls: sectionOf(calls),
|
|
173
|
+
calledBy: sectionOf(calledBy),
|
|
174
|
+
runsOn: sectionOf([]),
|
|
175
|
+
related: sectionOf([]),
|
|
176
|
+
},
|
|
177
|
+
isScanLimited: false,
|
|
178
|
+
};
|
|
179
|
+
return asJSON(response);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const RESPONSES: Record<string, JSONObject> = {
|
|
183
|
+
svc: entityResponse(
|
|
184
|
+
entityRow("svc", EntityType.Service, "Checkout API"),
|
|
185
|
+
[
|
|
186
|
+
call("out", "db", "Orders database", EntityType.Database),
|
|
187
|
+
/* Known when this drawer loaded; pruned before it is opened. */
|
|
188
|
+
call("out", "gone", "Old cache", EntityType.Database),
|
|
189
|
+
],
|
|
190
|
+
[call("in", "web", "Web frontend", EntityType.Service)],
|
|
191
|
+
),
|
|
192
|
+
db: entityResponse(
|
|
193
|
+
entityRow("db", EntityType.Database, "Orders database"),
|
|
194
|
+
[],
|
|
195
|
+
[call("in", "svc", "Checkout API", EntityType.Service)],
|
|
196
|
+
),
|
|
197
|
+
web: entityResponse(
|
|
198
|
+
entityRow("web", EntityType.Service, "Web frontend"),
|
|
199
|
+
[call("out", "svc", "Checkout API", EntityType.Service)],
|
|
200
|
+
[],
|
|
201
|
+
),
|
|
202
|
+
gone: entityResponse(null, [], []),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function serve(): void {
|
|
206
|
+
postMock.mockImplementation(async (...args: Array<unknown>) => {
|
|
207
|
+
const request: PostRequest = args[0] as PostRequest;
|
|
208
|
+
const key: string = request.data["entityKey"] as string;
|
|
209
|
+
const answer: JSONObject = held.has(key)
|
|
210
|
+
? await new Promise<JSONObject>(
|
|
211
|
+
(resolve: (value: JSONObject) => void) => {
|
|
212
|
+
held.set(key, resolve);
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
: RESPONSES[key]!;
|
|
216
|
+
return new HTTPResponse<JSONObject>(200, answer, {});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const CHECKOUT: EntityDetailTarget = {
|
|
221
|
+
entityKey: "svc",
|
|
222
|
+
entityType: EntityType.Service,
|
|
223
|
+
displayName: "Checkout API",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
* A map in miniature, wired the way ServiceMapGraph and the Infrastructure
|
|
228
|
+
* explorer wire the drawer: one selection, rows select in place, and no
|
|
229
|
+
* `key` on the drawer. The "map" buttons stand in for nodes outside it.
|
|
230
|
+
*/
|
|
231
|
+
const Host: React.FunctionComponent<{
|
|
232
|
+
initial: EntityDetailTarget | null;
|
|
233
|
+
}> = (props: { initial: EntityDetailTarget | null }): React.ReactElement => {
|
|
234
|
+
const [target, setTarget] = React.useState<EntityDetailTarget | null>(
|
|
235
|
+
props.initial,
|
|
236
|
+
);
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={() => {
|
|
242
|
+
setTarget(CHECKOUT);
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
Map node Checkout API
|
|
246
|
+
</button>
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => {
|
|
250
|
+
setTarget({ entityKey: "web", displayName: "Web frontend" });
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
Map node Web frontend
|
|
254
|
+
</button>
|
|
255
|
+
{target && (
|
|
256
|
+
<EntityDetailPanel
|
|
257
|
+
entity={target}
|
|
258
|
+
rangeStart={RANGE_START}
|
|
259
|
+
metricsWindowSeconds={60}
|
|
260
|
+
onSelectEntity={setTarget}
|
|
261
|
+
onClose={() => {
|
|
262
|
+
setTarget(null);
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
async function flushEntranceFrame(): Promise<void> {
|
|
271
|
+
await act(async () => {
|
|
272
|
+
await new Promise<void>((resolve: () => void) => {
|
|
273
|
+
requestAnimationFrame(() => {
|
|
274
|
+
resolve();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function sideOverPanel(): HTMLElement {
|
|
281
|
+
return screen.getByTestId("side-over-layer").firstElementChild as HTMLElement;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function focusTarget(): HTMLElement {
|
|
285
|
+
return screen.getByTestId("entity-detail-focus-target");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Focus a button the way a keyboard user reaches it, then activate it. */
|
|
289
|
+
function activate(button: HTMLElement): void {
|
|
290
|
+
button.focus();
|
|
291
|
+
expect(document.activeElement).toBe(button);
|
|
292
|
+
fireEvent.click(button);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function openCheckout(): Promise<HTMLElement> {
|
|
296
|
+
render(<Host initial={CHECKOUT} />);
|
|
297
|
+
await screen.findByRole("button", {
|
|
298
|
+
name: "View details for Orders database",
|
|
299
|
+
});
|
|
300
|
+
await flushEntranceFrame();
|
|
301
|
+
const sideOver: HTMLElement = screen.getByTestId("side-over");
|
|
302
|
+
expect(sideOverPanel()).not.toHaveClass("translate-x-full");
|
|
303
|
+
return sideOver;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
beforeEach(() => {
|
|
307
|
+
held.clear();
|
|
308
|
+
postMock.mockReset();
|
|
309
|
+
serve();
|
|
310
|
+
resetPageScrollLockForTesting();
|
|
311
|
+
jest
|
|
312
|
+
.spyOn(ProjectUtil, "getCurrentProjectId")
|
|
313
|
+
.mockReturnValue(new ObjectID("5b2f5b1c-0000-4000-8000-000000000001"));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
afterEach(() => {
|
|
317
|
+
cleanup();
|
|
318
|
+
jest.restoreAllMocks();
|
|
319
|
+
resetPageScrollLockForTesting();
|
|
320
|
+
document.body.style.overflow = "";
|
|
321
|
+
document.body.style.paddingRight = "";
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("opening a connection row in the open drawer", () => {
|
|
325
|
+
test("keeps the same side-over, without replaying its slide-in", async () => {
|
|
326
|
+
const sideOver: HTMLElement = await openCheckout();
|
|
327
|
+
|
|
328
|
+
activate(
|
|
329
|
+
screen.getByRole("button", { name: "View details for Orders database" }),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(screen.getByTestId("side-over")).toBe(sideOver);
|
|
333
|
+
expect(sideOverPanel()).not.toHaveClass("translate-x-full");
|
|
334
|
+
expect(screen.getByTestId("side-over-title")).toHaveTextContent(
|
|
335
|
+
"Orders database",
|
|
336
|
+
);
|
|
337
|
+
await screen.findByRole("button", {
|
|
338
|
+
name: "View details for Checkout API",
|
|
339
|
+
});
|
|
340
|
+
expect(screen.getByTestId("side-over")).toBe(sideOver);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("keeps keyboard focus in the drawer, on a target that names the new entity", async () => {
|
|
344
|
+
const sideOver: HTMLElement = await openCheckout();
|
|
345
|
+
|
|
346
|
+
activate(
|
|
347
|
+
screen.getByRole("button", { name: "View details for Orders database" }),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
await waitFor(() => {
|
|
351
|
+
expect(document.activeElement).toBe(focusTarget());
|
|
352
|
+
});
|
|
353
|
+
expect(sideOver).toContainElement(focusTarget());
|
|
354
|
+
expect(focusTarget()).toHaveTextContent("Orders database, Database");
|
|
355
|
+
/* The next Tab starts inside the drawer, not at the top of the page. */
|
|
356
|
+
expect(focusTarget()).toHaveAttribute("tabindex", "-1");
|
|
357
|
+
expect(document.activeElement).not.toBe(document.body);
|
|
358
|
+
|
|
359
|
+
/* And it stays there once the new entity's connections arrive. */
|
|
360
|
+
const back: HTMLElement = await screen.findByRole("button", {
|
|
361
|
+
name: "View details for Checkout API",
|
|
362
|
+
});
|
|
363
|
+
expect(document.activeElement).toBe(focusTarget());
|
|
364
|
+
|
|
365
|
+
/* Hop again, back where we came from. */
|
|
366
|
+
activate(back);
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
expect(focusTarget()).toHaveTextContent("Checkout API, Service");
|
|
369
|
+
});
|
|
370
|
+
expect(document.activeElement).toBe(focusTarget());
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("an entity that turns out to be gone keeps focus on the same target", async () => {
|
|
374
|
+
held.set("gone", () => {
|
|
375
|
+
return undefined;
|
|
376
|
+
});
|
|
377
|
+
const sideOver: HTMLElement = await openCheckout();
|
|
378
|
+
|
|
379
|
+
activate(
|
|
380
|
+
screen.getByRole("button", { name: "View details for Old cache" }),
|
|
381
|
+
);
|
|
382
|
+
await waitFor(() => {
|
|
383
|
+
expect(document.activeElement).toBe(focusTarget());
|
|
384
|
+
});
|
|
385
|
+
const target: HTMLElement = focusTarget();
|
|
386
|
+
expect(target).toHaveTextContent("Old cache, Database");
|
|
387
|
+
|
|
388
|
+
/* The server no longer has the row: the drawer says so, focus stays. */
|
|
389
|
+
await act(async () => {
|
|
390
|
+
held.get("gone")!(RESPONSES["gone"]!);
|
|
391
|
+
});
|
|
392
|
+
expect(
|
|
393
|
+
await screen.findByText("This resource is no longer in Inventory."),
|
|
394
|
+
).toBeInTheDocument();
|
|
395
|
+
expect(focusTarget()).toBe(target);
|
|
396
|
+
expect(document.activeElement).toBe(target);
|
|
397
|
+
expect(screen.getByTestId("side-over")).toBe(sideOver);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("focus the user put outside the drawer stays there", async () => {
|
|
401
|
+
const sideOver: HTMLElement = await openCheckout();
|
|
402
|
+
|
|
403
|
+
const mapNode: HTMLElement = screen.getByRole("button", {
|
|
404
|
+
name: "Map node Web frontend",
|
|
405
|
+
});
|
|
406
|
+
activate(mapNode);
|
|
407
|
+
|
|
408
|
+
await screen.findByRole("button", {
|
|
409
|
+
name: "View details for Checkout API",
|
|
410
|
+
});
|
|
411
|
+
expect(screen.getByTestId("side-over")).toBe(sideOver);
|
|
412
|
+
expect(document.activeElement).toBe(mapNode);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("opening the drawer takes no focus", async () => {
|
|
416
|
+
render(<Host initial={null} />);
|
|
417
|
+
const opener: HTMLElement = screen.getByRole("button", {
|
|
418
|
+
name: "Map node Checkout API",
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
activate(opener);
|
|
422
|
+
await screen.findByRole("button", {
|
|
423
|
+
name: "View details for Orders database",
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(document.activeElement).toBe(opener);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("opening the drawer from a click that left nothing focused takes no focus either", async () => {
|
|
430
|
+
expect(document.activeElement).toBe(document.body);
|
|
431
|
+
|
|
432
|
+
render(<Host initial={CHECKOUT} />);
|
|
433
|
+
await screen.findByRole("button", {
|
|
434
|
+
name: "View details for Orders database",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(document.activeElement).toBe(document.body);
|
|
438
|
+
});
|
|
439
|
+
});
|