@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,1611 @@
|
|
|
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
|
+
cleanup,
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
screen,
|
|
15
|
+
waitFor,
|
|
16
|
+
} from "@testing-library/react";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import { MemoryRouter } from "react-router-dom";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Declaring an incident from alerts, and acknowledging those alerts as it is
|
|
23
|
+
* declared. The create-incident page reads the project's alert states and
|
|
24
|
+
* the alerts' current states, offers a box - ticked by default - to
|
|
25
|
+
* acknowledge the alerts that are not acknowledged yet, and sends
|
|
26
|
+
* miscDataProps[acknowledgeAlertsToLink] = true only when that box is on
|
|
27
|
+
* screen, allowed and ticked. When there are alerts that will keep escalating
|
|
28
|
+
* (box unticked, locked, or hidden while permissions are unknown) the banner
|
|
29
|
+
* says so. It also hints when an alert is already linked to an incident.
|
|
30
|
+
*
|
|
31
|
+
* The real page is rendered. ModelForm is stubbed to capture the props it is
|
|
32
|
+
* handed (as IncidentCreateFromAlerts.test.tsx does) - onBeforeCreate and the
|
|
33
|
+
* On-Call step's getSummaryElement are closures over the page's state, so
|
|
34
|
+
* they are always read from the LATEST render. ModelAPI answers per model
|
|
35
|
+
* type, and permissions are mocked as BulkIncidentLinkActions.test.tsx does.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
jest.mock("react-i18next", () => {
|
|
39
|
+
return {
|
|
40
|
+
useTranslation: () => {
|
|
41
|
+
return {
|
|
42
|
+
t: (key: string, opts?: { defaultValue?: string }): string => {
|
|
43
|
+
return opts?.defaultValue ?? key;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
type CapturedField = {
|
|
51
|
+
field?: Record<string, unknown> | undefined;
|
|
52
|
+
stepId?: string | undefined;
|
|
53
|
+
getSummaryElement?:
|
|
54
|
+
| ((item: Record<string, unknown>) => React.ReactElement)
|
|
55
|
+
| undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type CapturedFormProps = {
|
|
59
|
+
initialValues: Record<string, unknown>;
|
|
60
|
+
onBeforeCreate?:
|
|
61
|
+
| ((
|
|
62
|
+
item: unknown,
|
|
63
|
+
miscDataProps: Record<string, unknown>,
|
|
64
|
+
) => Promise<unknown>)
|
|
65
|
+
| undefined;
|
|
66
|
+
fields: Array<CapturedField>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let capturedForms: Array<CapturedFormProps> = [];
|
|
70
|
+
|
|
71
|
+
jest.mock("../../../UI/Components/Forms/ModelForm", () => {
|
|
72
|
+
// Only the component is stubbed: the page imports FormType from here too.
|
|
73
|
+
const actual: Record<string, unknown> = jest.requireActual(
|
|
74
|
+
"../../../UI/Components/Forms/ModelForm",
|
|
75
|
+
) as Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
__esModule: true,
|
|
79
|
+
...actual,
|
|
80
|
+
default: (props: CapturedFormProps): React.ReactElement => {
|
|
81
|
+
capturedForms.push(props);
|
|
82
|
+
return <div data-testid="model-form" />;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
88
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
89
|
+
|
|
90
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
91
|
+
return {
|
|
92
|
+
__esModule: true,
|
|
93
|
+
default: {
|
|
94
|
+
getList: (...args: Array<any>) => {
|
|
95
|
+
return getListMock(...args);
|
|
96
|
+
},
|
|
97
|
+
getItem: (...args: Array<any>) => {
|
|
98
|
+
return getItemMock(...args);
|
|
99
|
+
},
|
|
100
|
+
create: async (): Promise<null> => {
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
import IncidentCreate from "../../../../App/FeatureSet/Dashboard/src/Pages/Incidents/Create";
|
|
108
|
+
import { ACKNOWLEDGED_ALERTS_NO_ON_CALL_NOTE } from "../../../../App/FeatureSet/Dashboard/src/Components/Incident/AcknowledgeAlertsOnDeclare";
|
|
109
|
+
import FetchOnCallDutyPolicies from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/FetchOnCallPolicies";
|
|
110
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
111
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
112
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
113
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
114
|
+
import IncidentAlert from "../../../Models/DatabaseModels/IncidentAlert";
|
|
115
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
116
|
+
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
|
117
|
+
import OnCallDutyPolicy from "../../../Models/DatabaseModels/OnCallDutyPolicy";
|
|
118
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
119
|
+
import Route from "../../../Types/API/Route";
|
|
120
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
121
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
122
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
123
|
+
import {
|
|
124
|
+
INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
|
|
125
|
+
INCIDENT_ALERT_IDS_TO_LINK_KEY,
|
|
126
|
+
INCIDENT_CREATE_ALERT_IDS_QUERY_PARAM,
|
|
127
|
+
} from "../../../Types/Incident/IncidentAlertLink";
|
|
128
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
129
|
+
import Permission from "../../../Types/Permission";
|
|
130
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
131
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
132
|
+
import PermissionGate from "../../../UI/Utils/PermissionGate";
|
|
133
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
134
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
135
|
+
|
|
136
|
+
const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
|
|
137
|
+
const FIRST_INCIDENT_STATE_ID: string = "55555555-5555-4555-8555-000000000001";
|
|
138
|
+
|
|
139
|
+
const CREATED_STATE_ID: string = "33333333-3333-4333-8333-000000000001";
|
|
140
|
+
const ACKNOWLEDGED_STATE_ID: string = "33333333-3333-4333-8333-000000000002";
|
|
141
|
+
const RESOLVED_STATE_ID: string = "33333333-3333-4333-8333-000000000003";
|
|
142
|
+
|
|
143
|
+
const ALERT_ONE_ID: string = "22222222-2222-4222-8222-000000000001";
|
|
144
|
+
const ALERT_TWO_ID: string = "22222222-2222-4222-8222-000000000002";
|
|
145
|
+
const ALERT_THREE_ID: string = "22222222-2222-4222-8222-000000000003";
|
|
146
|
+
const ALERT_FOUR_ID: string = "22222222-2222-4222-8222-000000000004";
|
|
147
|
+
|
|
148
|
+
const INCIDENT_SEVEN_ID: string = "44444444-4444-4444-8444-000000000007";
|
|
149
|
+
const INCIDENT_EIGHT_ID: string = "44444444-4444-4444-8444-000000000008";
|
|
150
|
+
const INCIDENT_NINE_ID: string = "44444444-4444-4444-8444-000000000009";
|
|
151
|
+
|
|
152
|
+
const POLICY_ID: string = "99999999-9999-4999-8999-000000000001";
|
|
153
|
+
|
|
154
|
+
const CHECKBOX_TEST_ID: string = "incident-create-acknowledge-alerts-checkbox";
|
|
155
|
+
const CHECKBOX_WRAPPER_TEST_ID: string = "incident-create-acknowledge-alerts";
|
|
156
|
+
const KEEP_ESCALATING_TEST_ID: string =
|
|
157
|
+
"incident-create-alerts-keep-escalating";
|
|
158
|
+
const BANNER_TEST_ID: string = "incident-create-alerts-to-link";
|
|
159
|
+
const ALREADY_LINKED_TEST_ID: string = "incident-create-alert-already-linked";
|
|
160
|
+
const ALREADY_LINKED_NOTE_TEST_ID: string =
|
|
161
|
+
"incident-create-alerts-already-linked-note";
|
|
162
|
+
|
|
163
|
+
const SINGULAR_DESCRIPTION: string =
|
|
164
|
+
"It is acknowledged as you when the incident is declared, and its own on-call escalation stops within a minute. Pages that already went out are not recalled.";
|
|
165
|
+
const PLURAL_DESCRIPTION: string =
|
|
166
|
+
"They are acknowledged as you when the incident is declared, and their own on-call escalation stops within a minute. Pages that already went out are not recalled.";
|
|
167
|
+
|
|
168
|
+
const TIMELINE_CREATE_REASON: string =
|
|
169
|
+
"You do not have permission to acknowledge this alert. You need one of these permissions: Project Owner, Project Admin, Project Member, Alert Admin, Alert Member, Create Alert State Timeline.";
|
|
170
|
+
const ALERT_UPDATE_REASON: string =
|
|
171
|
+
"You do not have permission to acknowledge this alert. You need one of these permissions: Project Owner, Project Admin, Project Member, Alert Admin, Alert Member, Edit Alert.";
|
|
172
|
+
|
|
173
|
+
const NO_POLICIES_TEXT: string =
|
|
174
|
+
"No on-call policies will be executed when this incident is created.";
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* The note under the alerts when some of them already have an incident.
|
|
178
|
+
* With every alert linked there is nothing left to link, so it points at
|
|
179
|
+
* that incident; with only some, the others can still be linked to it.
|
|
180
|
+
*/
|
|
181
|
+
const SINGLE_ALERT_ALREADY_LINKED_NOTE: string =
|
|
182
|
+
"This alert is already linked to an incident. If it is the same problem, update that incident instead of declaring another one.";
|
|
183
|
+
const EVERY_ALERT_ALREADY_LINKED_NOTE: string =
|
|
184
|
+
"These alerts are already linked to incidents. If it is the same problem, update that incident instead of declaring another one.";
|
|
185
|
+
const SOME_ALERTS_ALREADY_LINKED_NOTE: string =
|
|
186
|
+
"Some of these alerts are already linked to an incident. If it is the same problem, link the other alerts to that incident from the alerts list instead of declaring another one.";
|
|
187
|
+
|
|
188
|
+
const SINGLE_ALERT_KEEPS_ESCALATING: string =
|
|
189
|
+
"Declaring the incident does not acknowledge the alert: it keeps escalating until it is acknowledged.";
|
|
190
|
+
const EVERY_ALERT_KEEPS_ESCALATING: string =
|
|
191
|
+
"Declaring the incident does not acknowledge these alerts: they keep escalating until they are acknowledged.";
|
|
192
|
+
|
|
193
|
+
type MakeAlertFunction = (
|
|
194
|
+
id: string,
|
|
195
|
+
alertNumber: number,
|
|
196
|
+
currentAlertStateId: string | null,
|
|
197
|
+
) => Alert;
|
|
198
|
+
|
|
199
|
+
const makeAlert: MakeAlertFunction = (
|
|
200
|
+
id: string,
|
|
201
|
+
alertNumber: number,
|
|
202
|
+
currentAlertStateId: string | null,
|
|
203
|
+
): Alert => {
|
|
204
|
+
const alert: Alert = new Alert();
|
|
205
|
+
alert._id = id;
|
|
206
|
+
alert.title = `Alert ${alertNumber} title`;
|
|
207
|
+
alert.description = `Alert ${alertNumber} description`;
|
|
208
|
+
alert.alertNumber = alertNumber;
|
|
209
|
+
alert.isPrivate = false;
|
|
210
|
+
alert.hosts = [];
|
|
211
|
+
alert.labels = [];
|
|
212
|
+
|
|
213
|
+
if (currentAlertStateId) {
|
|
214
|
+
alert.currentAlertStateId = new ObjectID(currentAlertStateId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return alert;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type MakeAlertStateFunction = (data: {
|
|
221
|
+
id: string;
|
|
222
|
+
name: string;
|
|
223
|
+
order?: number | undefined;
|
|
224
|
+
isAcknowledgedState?: boolean | undefined;
|
|
225
|
+
isResolvedState?: boolean | undefined;
|
|
226
|
+
}) => AlertState;
|
|
227
|
+
|
|
228
|
+
const makeAlertState: MakeAlertStateFunction = (data: {
|
|
229
|
+
id: string;
|
|
230
|
+
name: string;
|
|
231
|
+
order?: number | undefined;
|
|
232
|
+
isAcknowledgedState?: boolean | undefined;
|
|
233
|
+
isResolvedState?: boolean | undefined;
|
|
234
|
+
}): AlertState => {
|
|
235
|
+
const state: AlertState = new AlertState();
|
|
236
|
+
state._id = data.id;
|
|
237
|
+
state.name = data.name;
|
|
238
|
+
|
|
239
|
+
if (data.order !== undefined) {
|
|
240
|
+
state.order = data.order;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
state.isAcknowledgedState = Boolean(data.isAcknowledgedState);
|
|
244
|
+
state.isResolvedState = Boolean(data.isResolvedState);
|
|
245
|
+
|
|
246
|
+
return state;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
type DefaultAlertStatesFunction = () => Array<AlertState>;
|
|
250
|
+
|
|
251
|
+
// The project defaults: Created, Acknowledged, Resolved.
|
|
252
|
+
const defaultAlertStates: DefaultAlertStatesFunction =
|
|
253
|
+
(): Array<AlertState> => {
|
|
254
|
+
return [
|
|
255
|
+
makeAlertState({ id: CREATED_STATE_ID, name: "Created", order: 1 }),
|
|
256
|
+
makeAlertState({
|
|
257
|
+
id: ACKNOWLEDGED_STATE_ID,
|
|
258
|
+
name: "Acknowledged",
|
|
259
|
+
order: 2,
|
|
260
|
+
isAcknowledgedState: true,
|
|
261
|
+
}),
|
|
262
|
+
makeAlertState({
|
|
263
|
+
id: RESOLVED_STATE_ID,
|
|
264
|
+
name: "Resolved",
|
|
265
|
+
order: 3,
|
|
266
|
+
isResolvedState: true,
|
|
267
|
+
}),
|
|
268
|
+
];
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
type MakeLinkFunction = (data: {
|
|
272
|
+
alertId: string;
|
|
273
|
+
incidentId?: string | undefined;
|
|
274
|
+
incidentNumber?: number | undefined;
|
|
275
|
+
incidentNumberWithPrefix?: string | undefined;
|
|
276
|
+
}) => IncidentAlert;
|
|
277
|
+
|
|
278
|
+
const makeLink: MakeLinkFunction = (data: {
|
|
279
|
+
alertId: string;
|
|
280
|
+
incidentId?: string | undefined;
|
|
281
|
+
incidentNumber?: number | undefined;
|
|
282
|
+
incidentNumberWithPrefix?: string | undefined;
|
|
283
|
+
}): IncidentAlert => {
|
|
284
|
+
const incident: Incident = new Incident();
|
|
285
|
+
|
|
286
|
+
if (data.incidentId) {
|
|
287
|
+
incident._id = data.incidentId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (data.incidentNumber !== undefined) {
|
|
291
|
+
incident.incidentNumber = data.incidentNumber;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (data.incidentNumberWithPrefix !== undefined) {
|
|
295
|
+
incident.incidentNumberWithPrefix = data.incidentNumberWithPrefix;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
299
|
+
link.alertId = new ObjectID(data.alertId);
|
|
300
|
+
link.incident = incident;
|
|
301
|
+
|
|
302
|
+
return link;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
type ListResultFunction = (data: Array<unknown>) => {
|
|
306
|
+
data: Array<unknown>;
|
|
307
|
+
count: number;
|
|
308
|
+
skip: number;
|
|
309
|
+
limit: number;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const listResult: ListResultFunction = (data: Array<unknown>) => {
|
|
313
|
+
return { data: data, count: data.length, skip: 0, limit: data.length };
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
interface ApiFixture {
|
|
317
|
+
alerts: Array<Alert>;
|
|
318
|
+
alertStates: Array<AlertState>;
|
|
319
|
+
links: Array<IncidentAlert>;
|
|
320
|
+
failAlertStates?: boolean;
|
|
321
|
+
failLinks?: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let fixture: ApiFixture = { alerts: [], alertStates: [], links: [] };
|
|
325
|
+
let queryParams: Record<string, string> = {};
|
|
326
|
+
let permissionsForTest: Array<Permission> = [];
|
|
327
|
+
|
|
328
|
+
type IdsInFunction = (includes: unknown) => Array<string>;
|
|
329
|
+
|
|
330
|
+
const idsIn: IdsInFunction = (includes: unknown): Array<string> => {
|
|
331
|
+
return (includes as Includes).values.map((value: unknown): string => {
|
|
332
|
+
return String(value);
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
type AnswerListFunction = (request: any) => Promise<unknown>;
|
|
337
|
+
|
|
338
|
+
const answerList: AnswerListFunction = async (
|
|
339
|
+
request: any,
|
|
340
|
+
): Promise<unknown> => {
|
|
341
|
+
if (request.modelType === IncidentState) {
|
|
342
|
+
const state: IncidentState = new IncidentState();
|
|
343
|
+
state._id = FIRST_INCIDENT_STATE_ID;
|
|
344
|
+
return listResult([state]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (request.modelType === Alert) {
|
|
348
|
+
const ids: Array<string> = idsIn(request.query._id);
|
|
349
|
+
|
|
350
|
+
return listResult(
|
|
351
|
+
fixture.alerts.filter((alert: Alert): boolean => {
|
|
352
|
+
return ids.includes(alert._id || "");
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (request.modelType === AlertState) {
|
|
358
|
+
if (fixture.failAlertStates) {
|
|
359
|
+
throw new Error("Could not read the alert states.");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return listResult(fixture.alertStates);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (request.modelType === IncidentAlert) {
|
|
366
|
+
if (fixture.failLinks) {
|
|
367
|
+
throw new Error("You do not have permission to read incident alerts.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const ids: Array<string> = idsIn(request.query.alertId);
|
|
371
|
+
|
|
372
|
+
return listResult(
|
|
373
|
+
fixture.links.filter((link: IncidentAlert): boolean => {
|
|
374
|
+
return ids.includes(link.alertId?.toString() || "");
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (request.modelType === OnCallDutyPolicy) {
|
|
380
|
+
const policy: OnCallDutyPolicy = new OnCallDutyPolicy();
|
|
381
|
+
policy._id = POLICY_ID;
|
|
382
|
+
policy.name = "Primary on-call";
|
|
383
|
+
return listResult([policy]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
request.modelType === AlertSeverity ||
|
|
388
|
+
request.modelType === IncidentSeverity
|
|
389
|
+
) {
|
|
390
|
+
return listResult([]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The template owner lists.
|
|
394
|
+
return listResult([]);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
type RequestsForFunction = (modelType: unknown) => Array<any>;
|
|
398
|
+
|
|
399
|
+
const requestsFor: RequestsForFunction = (modelType: unknown): Array<any> => {
|
|
400
|
+
return getListMock.mock.calls
|
|
401
|
+
.map((call: Array<any>): any => {
|
|
402
|
+
return call[0];
|
|
403
|
+
})
|
|
404
|
+
.filter((request: any): boolean => {
|
|
405
|
+
return request.modelType === modelType;
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
type DeclareFromFunction = (alertIds: Array<string>) => void;
|
|
410
|
+
|
|
411
|
+
const declareFrom: DeclareFromFunction = (alertIds: Array<string>): void => {
|
|
412
|
+
queryParams = { [INCIDENT_CREATE_ALERT_IDS_QUERY_PARAM]: alertIds.join(",") };
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
type OpenPageFunction = () => Promise<CapturedFormProps>;
|
|
416
|
+
|
|
417
|
+
const openPage: OpenPageFunction = async (): Promise<CapturedFormProps> => {
|
|
418
|
+
render(
|
|
419
|
+
<MemoryRouter>
|
|
420
|
+
<IncidentCreate
|
|
421
|
+
pageRoute={new Route("/dashboard/incidents/create")}
|
|
422
|
+
currentProject={new Project()}
|
|
423
|
+
hasPaymentMethod={true}
|
|
424
|
+
/>
|
|
425
|
+
</MemoryRouter>,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await waitFor(() => {
|
|
429
|
+
expect(capturedForms.length).toBeGreaterThan(0);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return latestForm();
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
type LatestFormFunction = () => CapturedFormProps;
|
|
436
|
+
|
|
437
|
+
// onBeforeCreate and getSummaryElement close over the latest render's state.
|
|
438
|
+
const latestForm: LatestFormFunction = (): CapturedFormProps => {
|
|
439
|
+
return capturedForms[capturedForms.length - 1]!;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
type MiscDataFunction = () => Promise<Record<string, unknown>>;
|
|
443
|
+
|
|
444
|
+
// What the form would send as miscDataProps, after the page's onBeforeCreate.
|
|
445
|
+
const miscDataSent: MiscDataFunction = async (): Promise<
|
|
446
|
+
Record<string, unknown>
|
|
447
|
+
> => {
|
|
448
|
+
const form: CapturedFormProps = latestForm();
|
|
449
|
+
const miscDataProps: Record<string, unknown> = {};
|
|
450
|
+
const item: Incident = new Incident();
|
|
451
|
+
|
|
452
|
+
expect(form.onBeforeCreate).toBeDefined();
|
|
453
|
+
|
|
454
|
+
const returned: unknown = await form.onBeforeCreate!(item, miscDataProps);
|
|
455
|
+
|
|
456
|
+
expect(returned).toBe(item);
|
|
457
|
+
|
|
458
|
+
return miscDataProps;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
type CheckboxFunction = () => HTMLInputElement;
|
|
462
|
+
|
|
463
|
+
const acknowledgeCheckbox: CheckboxFunction = (): HTMLInputElement => {
|
|
464
|
+
return screen.getByTestId(CHECKBOX_TEST_ID) as HTMLInputElement;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
type ToggleFunction = () => Promise<void>;
|
|
468
|
+
|
|
469
|
+
/*
|
|
470
|
+
* The shared Checkbox keeps its own checked state and copies `value` into it
|
|
471
|
+
* from an effect, so the box only shows ticked once that effect has run.
|
|
472
|
+
* Wait for it before clicking - a click before then would tick it, not
|
|
473
|
+
* untick it - and for the click to land.
|
|
474
|
+
*/
|
|
475
|
+
const untick: ToggleFunction = async (): Promise<void> => {
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(acknowledgeCheckbox()).toBeChecked();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
fireEvent.click(acknowledgeCheckbox());
|
|
481
|
+
|
|
482
|
+
await waitFor(() => {
|
|
483
|
+
expect(acknowledgeCheckbox()).not.toBeChecked();
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const tick: ToggleFunction = async (): Promise<void> => {
|
|
488
|
+
expect(acknowledgeCheckbox()).not.toBeChecked();
|
|
489
|
+
|
|
490
|
+
fireEvent.click(acknowledgeCheckbox());
|
|
491
|
+
|
|
492
|
+
await waitFor(() => {
|
|
493
|
+
expect(acknowledgeCheckbox()).toBeChecked();
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
type TextFunction = (element: HTMLInputElement) => string;
|
|
498
|
+
|
|
499
|
+
// The visible label beside the box.
|
|
500
|
+
const labelOf: TextFunction = (element: HTMLInputElement): string => {
|
|
501
|
+
const label: HTMLLabelElement | null = document.querySelector(
|
|
502
|
+
`label[for="${element.id}"]`,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
expect(label).not.toBeNull();
|
|
506
|
+
|
|
507
|
+
return label!.textContent || "";
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// The description under the label, which the box points at.
|
|
511
|
+
const descriptionOf: TextFunction = (element: HTMLInputElement): string => {
|
|
512
|
+
const describedBy: string | null = element.getAttribute("aria-describedby");
|
|
513
|
+
|
|
514
|
+
expect(describedBy).toBeTruthy();
|
|
515
|
+
|
|
516
|
+
return document.getElementById(describedBy!)?.textContent || "";
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
type RenderedSummary = {
|
|
520
|
+
element: React.ReactElement;
|
|
521
|
+
container: HTMLDivElement;
|
|
522
|
+
text: string;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
type OnCallSummaryFunction = (item: Record<string, unknown>) => RenderedSummary;
|
|
526
|
+
|
|
527
|
+
// The On-Call step's summary, from the latest render, rendered on its own.
|
|
528
|
+
const onCallSummary: OnCallSummaryFunction = (
|
|
529
|
+
item: Record<string, unknown>,
|
|
530
|
+
): RenderedSummary => {
|
|
531
|
+
const field: CapturedField | undefined = latestForm().fields.find(
|
|
532
|
+
(candidate: CapturedField): boolean => {
|
|
533
|
+
return Boolean(
|
|
534
|
+
candidate.field && "onCallDutyPolicies" in candidate.field,
|
|
535
|
+
);
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(field).toBeDefined();
|
|
540
|
+
expect(field!.stepId).toBe("on-call");
|
|
541
|
+
expect(field!.getSummaryElement).toBeDefined();
|
|
542
|
+
|
|
543
|
+
const element: React.ReactElement = field!.getSummaryElement!(item);
|
|
544
|
+
const container: HTMLDivElement = document.createElement("div");
|
|
545
|
+
document.body.appendChild(container);
|
|
546
|
+
|
|
547
|
+
render(<MemoryRouter>{element}</MemoryRouter>, { container: container });
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
element: element,
|
|
551
|
+
container: container,
|
|
552
|
+
text: container.textContent || "",
|
|
553
|
+
};
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
describe("acknowledging the alerts an incident is declared from", () => {
|
|
557
|
+
beforeEach(() => {
|
|
558
|
+
capturedForms = [];
|
|
559
|
+
fixture = {
|
|
560
|
+
alerts: [
|
|
561
|
+
makeAlert(ALERT_ONE_ID, 1, CREATED_STATE_ID),
|
|
562
|
+
makeAlert(ALERT_TWO_ID, 2, CREATED_STATE_ID),
|
|
563
|
+
makeAlert(ALERT_THREE_ID, 3, CREATED_STATE_ID),
|
|
564
|
+
makeAlert(ALERT_FOUR_ID, 4, CREATED_STATE_ID),
|
|
565
|
+
],
|
|
566
|
+
alertStates: defaultAlertStates(),
|
|
567
|
+
links: [],
|
|
568
|
+
};
|
|
569
|
+
queryParams = {};
|
|
570
|
+
permissionsForTest = [Permission.ProjectMember];
|
|
571
|
+
getListMock.mockReset();
|
|
572
|
+
getItemMock.mockReset();
|
|
573
|
+
getListMock.mockImplementation(answerList);
|
|
574
|
+
PermissionGate.clearPermissionPropsCache();
|
|
575
|
+
|
|
576
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockImplementation(() => {
|
|
577
|
+
return permissionsForTest;
|
|
578
|
+
});
|
|
579
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
580
|
+
jest
|
|
581
|
+
.spyOn(ProjectUtil, "getCurrentProjectId")
|
|
582
|
+
.mockReturnValue(new ObjectID(PROJECT_ID));
|
|
583
|
+
jest
|
|
584
|
+
.spyOn(Navigation, "getQueryStringByName")
|
|
585
|
+
.mockImplementation((name: string): string | null => {
|
|
586
|
+
return queryParams[name] ?? null;
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
afterEach(() => {
|
|
591
|
+
cleanup();
|
|
592
|
+
jest.restoreAllMocks();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("uses the shared miscDataProps key", () => {
|
|
596
|
+
expect(INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY).toBe(
|
|
597
|
+
"acknowledgeAlertsToLink",
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("reads the alerts' current states, the project's alert states and the alerts' existing links", async () => {
|
|
602
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
603
|
+
|
|
604
|
+
await openPage();
|
|
605
|
+
|
|
606
|
+
const alertRequests: Array<any> = requestsFor(Alert);
|
|
607
|
+
expect(alertRequests).toHaveLength(1);
|
|
608
|
+
expect(alertRequests[0].select).toEqual(
|
|
609
|
+
expect.objectContaining({ _id: true, currentAlertStateId: true }),
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const stateRequests: Array<any> = requestsFor(AlertState);
|
|
613
|
+
expect(stateRequests).toHaveLength(1);
|
|
614
|
+
expect(stateRequests[0].query).toEqual({});
|
|
615
|
+
expect(stateRequests[0].select).toEqual({
|
|
616
|
+
_id: true,
|
|
617
|
+
order: true,
|
|
618
|
+
isAcknowledgedState: true,
|
|
619
|
+
});
|
|
620
|
+
expect(stateRequests[0].sort).toEqual({ order: SortOrder.Ascending });
|
|
621
|
+
expect(stateRequests[0].limit).toBe(LIMIT_PER_PROJECT);
|
|
622
|
+
expect(stateRequests[0].skip).toBe(0);
|
|
623
|
+
|
|
624
|
+
const linkRequests: Array<any> = requestsFor(IncidentAlert);
|
|
625
|
+
expect(linkRequests).toHaveLength(1);
|
|
626
|
+
expect(idsIn(linkRequests[0].query.alertId)).toEqual([
|
|
627
|
+
ALERT_ONE_ID,
|
|
628
|
+
ALERT_TWO_ID,
|
|
629
|
+
]);
|
|
630
|
+
expect(linkRequests[0].select).toEqual({
|
|
631
|
+
alertId: true,
|
|
632
|
+
incident: {
|
|
633
|
+
_id: true,
|
|
634
|
+
incidentNumber: true,
|
|
635
|
+
incidentNumberWithPrefix: true,
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
expect(linkRequests[0].sort).toEqual({ createdAt: SortOrder.Ascending });
|
|
639
|
+
expect(linkRequests[0].limit).toBe(LIMIT_PER_PROJECT);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe("offering the box", () => {
|
|
643
|
+
test("offers to acknowledge one Created alert, ticked by default", async () => {
|
|
644
|
+
declareFrom([ALERT_ONE_ID]);
|
|
645
|
+
|
|
646
|
+
await openPage();
|
|
647
|
+
|
|
648
|
+
const banner: HTMLElement = screen.getByTestId(BANNER_TEST_ID);
|
|
649
|
+
const wrapper: HTMLElement = screen.getByTestId(CHECKBOX_WRAPPER_TEST_ID);
|
|
650
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
651
|
+
|
|
652
|
+
expect(banner).toContainElement(wrapper);
|
|
653
|
+
expect(wrapper).toContainElement(checkbox);
|
|
654
|
+
await waitFor(() => {
|
|
655
|
+
expect(checkbox).toBeChecked();
|
|
656
|
+
});
|
|
657
|
+
expect(checkbox).toBeEnabled();
|
|
658
|
+
expect(checkbox).not.toHaveAttribute("title");
|
|
659
|
+
expect(labelOf(checkbox)).toBe(
|
|
660
|
+
"Acknowledge this alert to stop its escalation",
|
|
661
|
+
);
|
|
662
|
+
expect(descriptionOf(checkbox)).toBe(SINGULAR_DESCRIPTION);
|
|
663
|
+
expect(
|
|
664
|
+
screen.getByLabelText("Acknowledge this alert to stop its escalation"),
|
|
665
|
+
).toBe(checkbox);
|
|
666
|
+
expect(
|
|
667
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
668
|
+
).not.toBeInTheDocument();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("names every alert when none is acknowledged yet", async () => {
|
|
672
|
+
fixture.alerts[1] = makeAlert(ALERT_TWO_ID, 2, null);
|
|
673
|
+
fixture.alerts[2] = makeAlert(
|
|
674
|
+
ALERT_THREE_ID,
|
|
675
|
+
3,
|
|
676
|
+
"33333333-3333-4333-8333-0000000000ff",
|
|
677
|
+
);
|
|
678
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID, ALERT_THREE_ID]);
|
|
679
|
+
|
|
680
|
+
await openPage();
|
|
681
|
+
|
|
682
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
683
|
+
|
|
684
|
+
await waitFor(() => {
|
|
685
|
+
expect(checkbox).toBeChecked();
|
|
686
|
+
});
|
|
687
|
+
// No state, or a state that is not one of the project's, is offered.
|
|
688
|
+
expect(labelOf(checkbox)).toBe(
|
|
689
|
+
"Acknowledge these 3 alerts to stop their escalation",
|
|
690
|
+
);
|
|
691
|
+
expect(descriptionOf(checkbox)).toBe(PLURAL_DESCRIPTION);
|
|
692
|
+
expect(await miscDataSent()).toEqual({
|
|
693
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
694
|
+
ALERT_ONE_ID,
|
|
695
|
+
ALERT_TWO_ID,
|
|
696
|
+
ALERT_THREE_ID,
|
|
697
|
+
],
|
|
698
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("names only the alerts not acknowledged yet, and says how many are left as they are", async () => {
|
|
703
|
+
fixture.alerts[1] = makeAlert(ALERT_TWO_ID, 2, ACKNOWLEDGED_STATE_ID);
|
|
704
|
+
fixture.alerts[2] = makeAlert(ALERT_THREE_ID, 3, RESOLVED_STATE_ID);
|
|
705
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID, ALERT_THREE_ID, ALERT_FOUR_ID]);
|
|
706
|
+
|
|
707
|
+
await openPage();
|
|
708
|
+
|
|
709
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
710
|
+
|
|
711
|
+
await waitFor(() => {
|
|
712
|
+
expect(checkbox).toBeChecked();
|
|
713
|
+
});
|
|
714
|
+
expect(labelOf(checkbox)).toBe(
|
|
715
|
+
"Acknowledge the 2 alerts that are not acknowledged yet, to stop their escalation",
|
|
716
|
+
);
|
|
717
|
+
expect(descriptionOf(checkbox)).toBe(
|
|
718
|
+
`${PLURAL_DESCRIPTION} 2 alerts are already acknowledged or resolved and are left as they are.`,
|
|
719
|
+
);
|
|
720
|
+
// Every alert is linked; the server acknowledges only the ones not yet.
|
|
721
|
+
expect(await miscDataSent()).toEqual({
|
|
722
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
723
|
+
ALERT_ONE_ID,
|
|
724
|
+
ALERT_TWO_ID,
|
|
725
|
+
ALERT_THREE_ID,
|
|
726
|
+
ALERT_FOUR_ID,
|
|
727
|
+
],
|
|
728
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
await untick();
|
|
732
|
+
|
|
733
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
734
|
+
"Declaring the incident does not acknowledge the 2 alerts that are not acknowledged yet: they keep escalating until they are acknowledged.",
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("names the one alert not acknowledged yet among several", async () => {
|
|
739
|
+
fixture.alerts[1] = makeAlert(ALERT_TWO_ID, 2, ACKNOWLEDGED_STATE_ID);
|
|
740
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
741
|
+
|
|
742
|
+
await openPage();
|
|
743
|
+
|
|
744
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
745
|
+
|
|
746
|
+
expect(labelOf(checkbox)).toBe(
|
|
747
|
+
"Acknowledge the 1 alert that is not acknowledged yet, to stop its escalation",
|
|
748
|
+
);
|
|
749
|
+
expect(descriptionOf(checkbox)).toBe(
|
|
750
|
+
`${SINGULAR_DESCRIPTION} 1 alert is already acknowledged or resolved and is left as it is.`,
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
await untick();
|
|
754
|
+
|
|
755
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
756
|
+
"Declaring the incident does not acknowledge the alert that is not acknowledged yet: it keeps escalating until it is acknowledged.",
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("offers nothing, and warns of nothing, when every alert is acknowledged or resolved already", async () => {
|
|
761
|
+
fixture.alerts[0] = makeAlert(ALERT_ONE_ID, 1, ACKNOWLEDGED_STATE_ID);
|
|
762
|
+
fixture.alerts[1] = makeAlert(ALERT_TWO_ID, 2, RESOLVED_STATE_ID);
|
|
763
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
764
|
+
|
|
765
|
+
await openPage();
|
|
766
|
+
|
|
767
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
768
|
+
expect(
|
|
769
|
+
screen.queryByTestId(CHECKBOX_WRAPPER_TEST_ID),
|
|
770
|
+
).not.toBeInTheDocument();
|
|
771
|
+
expect(screen.queryByTestId(CHECKBOX_TEST_ID)).not.toBeInTheDocument();
|
|
772
|
+
expect(
|
|
773
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
774
|
+
).not.toBeInTheDocument();
|
|
775
|
+
expect(await miscDataSent()).toEqual({
|
|
776
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("leaves the offer out, but still declares from the alerts, when the alert states cannot be read", async () => {
|
|
781
|
+
fixture.failAlertStates = true;
|
|
782
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
783
|
+
|
|
784
|
+
const form: CapturedFormProps = await openPage();
|
|
785
|
+
|
|
786
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
787
|
+
expect(
|
|
788
|
+
screen.queryByTestId(CHECKBOX_WRAPPER_TEST_ID),
|
|
789
|
+
).not.toBeInTheDocument();
|
|
790
|
+
expect(
|
|
791
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
792
|
+
).not.toBeInTheDocument();
|
|
793
|
+
expect(screen.queryByText("Could not read the alert states.")).toBeNull();
|
|
794
|
+
// The prefill is still applied, before the form first renders.
|
|
795
|
+
expect(capturedForms[0]!.initialValues).toEqual(form.initialValues);
|
|
796
|
+
expect(form.initialValues).toEqual(
|
|
797
|
+
expect.objectContaining({
|
|
798
|
+
currentIncidentState: FIRST_INCIDENT_STATE_ID,
|
|
799
|
+
title: "Alert 1 title",
|
|
800
|
+
description: [
|
|
801
|
+
"- Alert #1: Alert 1 title",
|
|
802
|
+
"- Alert #2: Alert 2 title",
|
|
803
|
+
].join("\n"),
|
|
804
|
+
}),
|
|
805
|
+
);
|
|
806
|
+
expect(await miscDataSent()).toEqual({
|
|
807
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("leaves the offer out when the project has no Acknowledged alert state", async () => {
|
|
812
|
+
fixture.alertStates = [
|
|
813
|
+
makeAlertState({ id: CREATED_STATE_ID, name: "Created", order: 1 }),
|
|
814
|
+
makeAlertState({
|
|
815
|
+
id: RESOLVED_STATE_ID,
|
|
816
|
+
name: "Resolved",
|
|
817
|
+
order: 3,
|
|
818
|
+
isResolvedState: true,
|
|
819
|
+
}),
|
|
820
|
+
];
|
|
821
|
+
declareFrom([ALERT_ONE_ID]);
|
|
822
|
+
|
|
823
|
+
await openPage();
|
|
824
|
+
|
|
825
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
826
|
+
expect(
|
|
827
|
+
screen.queryByTestId(CHECKBOX_WRAPPER_TEST_ID),
|
|
828
|
+
).not.toBeInTheDocument();
|
|
829
|
+
expect(
|
|
830
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
831
|
+
).not.toBeInTheDocument();
|
|
832
|
+
expect(await miscDataSent()).toEqual({
|
|
833
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("leaves the offer out when the Acknowledged state has no order", async () => {
|
|
838
|
+
fixture.alertStates = [
|
|
839
|
+
makeAlertState({ id: CREATED_STATE_ID, name: "Created", order: 1 }),
|
|
840
|
+
makeAlertState({
|
|
841
|
+
id: ACKNOWLEDGED_STATE_ID,
|
|
842
|
+
name: "Acknowledged",
|
|
843
|
+
isAcknowledgedState: true,
|
|
844
|
+
}),
|
|
845
|
+
];
|
|
846
|
+
declareFrom([ALERT_ONE_ID]);
|
|
847
|
+
|
|
848
|
+
await openPage();
|
|
849
|
+
|
|
850
|
+
expect(
|
|
851
|
+
screen.queryByTestId(CHECKBOX_WRAPPER_TEST_ID),
|
|
852
|
+
).not.toBeInTheDocument();
|
|
853
|
+
expect(await miscDataSent()).toEqual({
|
|
854
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("does not read alert states or links, or send the key, when not declaring from alerts", async () => {
|
|
859
|
+
await openPage();
|
|
860
|
+
|
|
861
|
+
await waitFor(() => {
|
|
862
|
+
expect(latestForm().initialValues).toEqual({
|
|
863
|
+
currentIncidentState: FIRST_INCIDENT_STATE_ID,
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
expect(requestsFor(AlertState)).toHaveLength(0);
|
|
868
|
+
expect(requestsFor(IncidentAlert)).toHaveLength(0);
|
|
869
|
+
expect(requestsFor(Alert)).toHaveLength(0);
|
|
870
|
+
expect(screen.queryByTestId(BANNER_TEST_ID)).not.toBeInTheDocument();
|
|
871
|
+
expect(screen.queryByTestId(CHECKBOX_TEST_ID)).not.toBeInTheDocument();
|
|
872
|
+
expect(
|
|
873
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
874
|
+
).not.toBeInTheDocument();
|
|
875
|
+
expect(await miscDataSent()).toEqual({});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("does not read alert states or links for a parameter holding no valid ids", async () => {
|
|
879
|
+
queryParams = { [INCIDENT_CREATE_ALERT_IDS_QUERY_PARAM]: "nope,,<b>" };
|
|
880
|
+
|
|
881
|
+
await openPage();
|
|
882
|
+
|
|
883
|
+
expect(requestsFor(AlertState)).toHaveLength(0);
|
|
884
|
+
expect(requestsFor(IncidentAlert)).toHaveLength(0);
|
|
885
|
+
expect(await miscDataSent()).toEqual({});
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe("sending the choice", () => {
|
|
890
|
+
test("sends acknowledgeAlertsToLink with the alert ids while the box is ticked", async () => {
|
|
891
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
892
|
+
|
|
893
|
+
await openPage();
|
|
894
|
+
|
|
895
|
+
expect(await miscDataSent()).toEqual({
|
|
896
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
897
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("unticking drops the key and says the alert keeps escalating; ticking again brings it back", async () => {
|
|
902
|
+
declareFrom([ALERT_ONE_ID]);
|
|
903
|
+
|
|
904
|
+
await openPage();
|
|
905
|
+
|
|
906
|
+
await untick();
|
|
907
|
+
|
|
908
|
+
const note: HTMLElement = screen.getByTestId(KEEP_ESCALATING_TEST_ID);
|
|
909
|
+
|
|
910
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toContainElement(note);
|
|
911
|
+
expect(note.textContent).toBe(SINGLE_ALERT_KEEPS_ESCALATING);
|
|
912
|
+
// The box stays on screen, so it can be ticked again.
|
|
913
|
+
expect(acknowledgeCheckbox()).toBeEnabled();
|
|
914
|
+
|
|
915
|
+
const unticked: Record<string, unknown> = await miscDataSent();
|
|
916
|
+
|
|
917
|
+
expect(unticked).toEqual({
|
|
918
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
919
|
+
});
|
|
920
|
+
expect(unticked).not.toHaveProperty(
|
|
921
|
+
INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
await tick();
|
|
925
|
+
|
|
926
|
+
expect(
|
|
927
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
928
|
+
).not.toBeInTheDocument();
|
|
929
|
+
expect(await miscDataSent()).toEqual({
|
|
930
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
931
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
test("says every alert keeps escalating when several are unticked", async () => {
|
|
936
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
937
|
+
|
|
938
|
+
await openPage();
|
|
939
|
+
|
|
940
|
+
await untick();
|
|
941
|
+
|
|
942
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
943
|
+
EVERY_ALERT_KEEPS_ESCALATING,
|
|
944
|
+
);
|
|
945
|
+
expect(await miscDataSent()).toEqual({
|
|
946
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
describe("permissions", () => {
|
|
952
|
+
test.each([
|
|
953
|
+
["a viewer", [Permission.Viewer], TIMELINE_CREATE_REASON],
|
|
954
|
+
["an alert viewer", [Permission.AlertViewer], TIMELINE_CREATE_REASON],
|
|
955
|
+
[
|
|
956
|
+
"somebody who may edit alerts but not add state timeline rows",
|
|
957
|
+
[Permission.Viewer, Permission.EditAlert],
|
|
958
|
+
TIMELINE_CREATE_REASON,
|
|
959
|
+
],
|
|
960
|
+
[
|
|
961
|
+
"somebody who may add state timeline rows but not edit alerts",
|
|
962
|
+
[Permission.Viewer, Permission.CreateAlertStateTimeline],
|
|
963
|
+
ALERT_UPDATE_REASON,
|
|
964
|
+
],
|
|
965
|
+
])(
|
|
966
|
+
"locks the box, unticked, for %s and never sends the key",
|
|
967
|
+
async (
|
|
968
|
+
_who: string,
|
|
969
|
+
permissions: Array<Permission>,
|
|
970
|
+
reason: string,
|
|
971
|
+
): Promise<void> => {
|
|
972
|
+
permissionsForTest = permissions;
|
|
973
|
+
declareFrom([ALERT_ONE_ID]);
|
|
974
|
+
|
|
975
|
+
await openPage();
|
|
976
|
+
|
|
977
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
978
|
+
|
|
979
|
+
expect(checkbox).toBeDisabled();
|
|
980
|
+
expect(checkbox).not.toBeChecked();
|
|
981
|
+
expect(checkbox).toHaveAttribute("title", reason);
|
|
982
|
+
expect(labelOf(checkbox)).toBe(
|
|
983
|
+
"Acknowledge this alert to stop its escalation",
|
|
984
|
+
);
|
|
985
|
+
expect(descriptionOf(checkbox)).toContain(
|
|
986
|
+
"You do not have permission to acknowledge this alert",
|
|
987
|
+
);
|
|
988
|
+
expect(descriptionOf(checkbox)).toBe(
|
|
989
|
+
`${SINGULAR_DESCRIPTION} ${reason}`,
|
|
990
|
+
);
|
|
991
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
992
|
+
SINGLE_ALERT_KEEPS_ESCALATING,
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
const sent: Record<string, unknown> = await miscDataSent();
|
|
996
|
+
|
|
997
|
+
expect(sent).toEqual({
|
|
998
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* A browser does not deliver a click to a disabled box, but jsdom
|
|
1003
|
+
* does (and flips it). Even then the permission, not the box, decides
|
|
1004
|
+
* what is sent.
|
|
1005
|
+
*/
|
|
1006
|
+
fireEvent.click(checkbox);
|
|
1007
|
+
|
|
1008
|
+
expect(await miscDataSent()).toEqual({
|
|
1009
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
1010
|
+
});
|
|
1011
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
1012
|
+
SINGLE_ALERT_KEEPS_ESCALATING,
|
|
1013
|
+
);
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
test("hides the box while the permission snapshot is empty, but still warns the alerts keep escalating", async () => {
|
|
1018
|
+
permissionsForTest = [];
|
|
1019
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1020
|
+
|
|
1021
|
+
await openPage();
|
|
1022
|
+
|
|
1023
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
1024
|
+
expect(
|
|
1025
|
+
screen.queryByTestId(CHECKBOX_WRAPPER_TEST_ID),
|
|
1026
|
+
).not.toBeInTheDocument();
|
|
1027
|
+
expect(screen.getByTestId(KEEP_ESCALATING_TEST_ID).textContent).toBe(
|
|
1028
|
+
EVERY_ALERT_KEEPS_ESCALATING,
|
|
1029
|
+
);
|
|
1030
|
+
expect(await miscDataSent()).toEqual({
|
|
1031
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
test("offers a working, ticked box to a master admin without project permissions", async () => {
|
|
1036
|
+
permissionsForTest = [];
|
|
1037
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(true);
|
|
1038
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1039
|
+
|
|
1040
|
+
await openPage();
|
|
1041
|
+
|
|
1042
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
1043
|
+
|
|
1044
|
+
await waitFor(() => {
|
|
1045
|
+
expect(checkbox).toBeChecked();
|
|
1046
|
+
});
|
|
1047
|
+
expect(checkbox).toBeEnabled();
|
|
1048
|
+
expect(descriptionOf(checkbox)).toBe(SINGULAR_DESCRIPTION);
|
|
1049
|
+
expect(
|
|
1050
|
+
screen.queryByTestId(KEEP_ESCALATING_TEST_ID),
|
|
1051
|
+
).not.toBeInTheDocument();
|
|
1052
|
+
expect(await miscDataSent()).toEqual({
|
|
1053
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
1054
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
test("offers a working box to somebody with just the two fine-grained permissions", async () => {
|
|
1059
|
+
permissionsForTest = [
|
|
1060
|
+
Permission.Viewer,
|
|
1061
|
+
Permission.CreateAlertStateTimeline,
|
|
1062
|
+
Permission.EditAlert,
|
|
1063
|
+
];
|
|
1064
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1065
|
+
|
|
1066
|
+
await openPage();
|
|
1067
|
+
|
|
1068
|
+
const checkbox: HTMLInputElement = acknowledgeCheckbox();
|
|
1069
|
+
|
|
1070
|
+
await waitFor(() => {
|
|
1071
|
+
expect(checkbox).toBeChecked();
|
|
1072
|
+
});
|
|
1073
|
+
expect(checkbox).toBeEnabled();
|
|
1074
|
+
expect(await miscDataSent()).toEqual({
|
|
1075
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
1076
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
describe("alerts already linked to an incident", () => {
|
|
1082
|
+
test("names the incident an alert is already linked to, with a link to it", async () => {
|
|
1083
|
+
fixture.links = [
|
|
1084
|
+
makeLink({
|
|
1085
|
+
alertId: ALERT_ONE_ID,
|
|
1086
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1087
|
+
incidentNumber: 7,
|
|
1088
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1089
|
+
}),
|
|
1090
|
+
];
|
|
1091
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1092
|
+
|
|
1093
|
+
await openPage();
|
|
1094
|
+
|
|
1095
|
+
const hints: Array<HTMLElement> = screen.getAllByTestId(
|
|
1096
|
+
ALREADY_LINKED_TEST_ID,
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
expect(hints).toHaveLength(1);
|
|
1100
|
+
expect(hints[0]!.textContent).toBe("(already linked to Incident INC-7)");
|
|
1101
|
+
expect(hints[0]!.closest("li")?.textContent).toBe(
|
|
1102
|
+
"Alert #1:Alert 1 title(already linked to Incident INC-7)",
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
const anchors: Array<HTMLAnchorElement> = Array.from(
|
|
1106
|
+
hints[0]!.querySelectorAll("a"),
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
expect(anchors).toHaveLength(1);
|
|
1110
|
+
expect(anchors[0]!.getAttribute("href")).toBe(
|
|
1111
|
+
`/dashboard/${PROJECT_ID}/incidents/${INCIDENT_SEVEN_ID}`,
|
|
1112
|
+
);
|
|
1113
|
+
expect(anchors[0]!.getAttribute("target")).toBe("_blank");
|
|
1114
|
+
expect(anchors[0]!.textContent).toBe("Incident INC-7");
|
|
1115
|
+
|
|
1116
|
+
const note: HTMLElement = screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID);
|
|
1117
|
+
|
|
1118
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toContainElement(note);
|
|
1119
|
+
expect(note.textContent).toBe(SINGLE_ALERT_ALREADY_LINKED_NOTE);
|
|
1120
|
+
|
|
1121
|
+
// A hint, never a block: the box is still offered and the key sent.
|
|
1122
|
+
expect(acknowledgeCheckbox()).toBeEnabled();
|
|
1123
|
+
expect(await miscDataSent()).toEqual({
|
|
1124
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
1125
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("hints only on the alert that is linked, with the note for some alerts linked", async () => {
|
|
1130
|
+
fixture.links = [
|
|
1131
|
+
makeLink({
|
|
1132
|
+
alertId: ALERT_TWO_ID,
|
|
1133
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1134
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1135
|
+
}),
|
|
1136
|
+
];
|
|
1137
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1138
|
+
|
|
1139
|
+
await openPage();
|
|
1140
|
+
|
|
1141
|
+
const items: Array<HTMLLIElement> = Array.from(
|
|
1142
|
+
screen.getByTestId(BANNER_TEST_ID).querySelectorAll("li"),
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
expect(items).toHaveLength(2);
|
|
1146
|
+
expect(
|
|
1147
|
+
items[0]!.querySelector(`[data-testid="${ALREADY_LINKED_TEST_ID}"]`),
|
|
1148
|
+
).toBeNull();
|
|
1149
|
+
expect(
|
|
1150
|
+
items[1]!.querySelector(`[data-testid="${ALREADY_LINKED_TEST_ID}"]`)
|
|
1151
|
+
?.textContent,
|
|
1152
|
+
).toBe("(already linked to Incident INC-7)");
|
|
1153
|
+
expect(screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID).textContent).toBe(
|
|
1154
|
+
SOME_ALERTS_ALREADY_LINKED_NOTE,
|
|
1155
|
+
);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("says every alert is linked when several are, each to its own incident", async () => {
|
|
1159
|
+
fixture.links = [
|
|
1160
|
+
makeLink({
|
|
1161
|
+
alertId: ALERT_ONE_ID,
|
|
1162
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1163
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1164
|
+
}),
|
|
1165
|
+
makeLink({
|
|
1166
|
+
alertId: ALERT_TWO_ID,
|
|
1167
|
+
incidentId: INCIDENT_EIGHT_ID,
|
|
1168
|
+
incidentNumberWithPrefix: "INC-8",
|
|
1169
|
+
}),
|
|
1170
|
+
];
|
|
1171
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1172
|
+
|
|
1173
|
+
await openPage();
|
|
1174
|
+
|
|
1175
|
+
const items: Array<HTMLLIElement> = Array.from(
|
|
1176
|
+
screen.getByTestId(BANNER_TEST_ID).querySelectorAll("li"),
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
expect(
|
|
1180
|
+
items.map((item: HTMLLIElement): string => {
|
|
1181
|
+
return item.textContent || "";
|
|
1182
|
+
}),
|
|
1183
|
+
).toEqual([
|
|
1184
|
+
"Alert #1:Alert 1 title(already linked to Incident INC-7)",
|
|
1185
|
+
"Alert #2:Alert 2 title(already linked to Incident INC-8)",
|
|
1186
|
+
]);
|
|
1187
|
+
|
|
1188
|
+
const notes: Array<HTMLElement> = screen.getAllByTestId(
|
|
1189
|
+
ALREADY_LINKED_NOTE_TEST_ID,
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
expect(notes).toHaveLength(1);
|
|
1193
|
+
expect(notes[0]!.textContent).toBe(EVERY_ALERT_ALREADY_LINKED_NOTE);
|
|
1194
|
+
|
|
1195
|
+
// Still a hint, never a block.
|
|
1196
|
+
expect(acknowledgeCheckbox()).toBeEnabled();
|
|
1197
|
+
expect(await miscDataSent()).toEqual({
|
|
1198
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID, ALERT_TWO_ID],
|
|
1199
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
test("says every alert is linked when several are linked to the same incident", async () => {
|
|
1204
|
+
fixture.links = [
|
|
1205
|
+
makeLink({
|
|
1206
|
+
alertId: ALERT_ONE_ID,
|
|
1207
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1208
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1209
|
+
}),
|
|
1210
|
+
makeLink({
|
|
1211
|
+
alertId: ALERT_TWO_ID,
|
|
1212
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1213
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1214
|
+
}),
|
|
1215
|
+
makeLink({
|
|
1216
|
+
alertId: ALERT_THREE_ID,
|
|
1217
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1218
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1219
|
+
}),
|
|
1220
|
+
];
|
|
1221
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID, ALERT_THREE_ID]);
|
|
1222
|
+
|
|
1223
|
+
await openPage();
|
|
1224
|
+
|
|
1225
|
+
expect(screen.getAllByTestId(ALREADY_LINKED_TEST_ID)).toHaveLength(3);
|
|
1226
|
+
expect(screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID).textContent).toBe(
|
|
1227
|
+
EVERY_ALERT_ALREADY_LINKED_NOTE,
|
|
1228
|
+
);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test("says only some are linked when most, but not all, of several alerts are", async () => {
|
|
1232
|
+
fixture.links = [
|
|
1233
|
+
makeLink({
|
|
1234
|
+
alertId: ALERT_ONE_ID,
|
|
1235
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1236
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1237
|
+
}),
|
|
1238
|
+
makeLink({
|
|
1239
|
+
alertId: ALERT_THREE_ID,
|
|
1240
|
+
incidentId: INCIDENT_EIGHT_ID,
|
|
1241
|
+
incidentNumberWithPrefix: "INC-8",
|
|
1242
|
+
}),
|
|
1243
|
+
];
|
|
1244
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID, ALERT_THREE_ID]);
|
|
1245
|
+
|
|
1246
|
+
await openPage();
|
|
1247
|
+
|
|
1248
|
+
const items: Array<HTMLLIElement> = Array.from(
|
|
1249
|
+
screen.getByTestId(BANNER_TEST_ID).querySelectorAll("li"),
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
expect(
|
|
1253
|
+
items.map((item: HTMLLIElement): string | null => {
|
|
1254
|
+
return (
|
|
1255
|
+
item.querySelector(`[data-testid="${ALREADY_LINKED_TEST_ID}"]`)
|
|
1256
|
+
?.textContent || null
|
|
1257
|
+
);
|
|
1258
|
+
}),
|
|
1259
|
+
).toEqual([
|
|
1260
|
+
"(already linked to Incident INC-7)",
|
|
1261
|
+
null,
|
|
1262
|
+
"(already linked to Incident INC-8)",
|
|
1263
|
+
]);
|
|
1264
|
+
expect(screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID).textContent).toBe(
|
|
1265
|
+
SOME_ALERTS_ALREADY_LINKED_NOTE,
|
|
1266
|
+
);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// Such a link shows no hint, so the alert is not counted as linked either.
|
|
1270
|
+
test("does not count an alert whose only link has no readable incident as linked", async () => {
|
|
1271
|
+
fixture.links = [
|
|
1272
|
+
makeLink({
|
|
1273
|
+
alertId: ALERT_ONE_ID,
|
|
1274
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1275
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1276
|
+
}),
|
|
1277
|
+
makeLink({ alertId: ALERT_TWO_ID }),
|
|
1278
|
+
];
|
|
1279
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1280
|
+
|
|
1281
|
+
await openPage();
|
|
1282
|
+
|
|
1283
|
+
expect(screen.getAllByTestId(ALREADY_LINKED_TEST_ID)).toHaveLength(1);
|
|
1284
|
+
expect(screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID).textContent).toBe(
|
|
1285
|
+
SOME_ALERTS_ALREADY_LINKED_NOTE,
|
|
1286
|
+
);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// The note is about the alerts the banner lists, not the ids asked for.
|
|
1290
|
+
test("words the note for the alerts listed when a declared alert could not be read", async () => {
|
|
1291
|
+
fixture.links = [
|
|
1292
|
+
makeLink({
|
|
1293
|
+
alertId: ALERT_ONE_ID,
|
|
1294
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1295
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1296
|
+
}),
|
|
1297
|
+
];
|
|
1298
|
+
declareFrom([ALERT_ONE_ID, "22222222-2222-4222-8222-000000000099"]);
|
|
1299
|
+
|
|
1300
|
+
await openPage();
|
|
1301
|
+
|
|
1302
|
+
expect(
|
|
1303
|
+
screen.getByTestId(BANNER_TEST_ID).querySelectorAll("li"),
|
|
1304
|
+
).toHaveLength(1);
|
|
1305
|
+
expect(screen.getByTestId(ALREADY_LINKED_NOTE_TEST_ID).textContent).toBe(
|
|
1306
|
+
SINGLE_ALERT_ALREADY_LINKED_NOTE,
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
/*
|
|
1311
|
+
* Leaving the create page would lose the form, so the incidents open in
|
|
1312
|
+
* a new tab: the click is left to the browser (target="_blank"), never
|
|
1313
|
+
* routed in this tab.
|
|
1314
|
+
*/
|
|
1315
|
+
test("opens every linked incident in a new tab, at the incident's page, without leaving the form", async () => {
|
|
1316
|
+
const navigateSpy: ReturnType<typeof jest.spyOn> = jest
|
|
1317
|
+
.spyOn(Navigation, "navigate")
|
|
1318
|
+
.mockImplementation((): void => {});
|
|
1319
|
+
|
|
1320
|
+
fixture.links = [
|
|
1321
|
+
makeLink({
|
|
1322
|
+
alertId: ALERT_ONE_ID,
|
|
1323
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1324
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1325
|
+
}),
|
|
1326
|
+
makeLink({
|
|
1327
|
+
alertId: ALERT_ONE_ID,
|
|
1328
|
+
incidentId: INCIDENT_EIGHT_ID,
|
|
1329
|
+
incidentNumber: 8,
|
|
1330
|
+
}),
|
|
1331
|
+
makeLink({
|
|
1332
|
+
alertId: ALERT_TWO_ID,
|
|
1333
|
+
incidentId: INCIDENT_NINE_ID,
|
|
1334
|
+
}),
|
|
1335
|
+
];
|
|
1336
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1337
|
+
|
|
1338
|
+
await openPage();
|
|
1339
|
+
|
|
1340
|
+
const anchors: Array<HTMLAnchorElement> = screen
|
|
1341
|
+
.getAllByTestId(ALREADY_LINKED_TEST_ID)
|
|
1342
|
+
.flatMap((hint: HTMLElement): Array<HTMLAnchorElement> => {
|
|
1343
|
+
return Array.from(hint.querySelectorAll("a"));
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
expect(
|
|
1347
|
+
anchors.map(
|
|
1348
|
+
(
|
|
1349
|
+
anchor: HTMLAnchorElement,
|
|
1350
|
+
): { href: string | null; target: string | null; text: string } => {
|
|
1351
|
+
return {
|
|
1352
|
+
href: anchor.getAttribute("href"),
|
|
1353
|
+
target: anchor.getAttribute("target"),
|
|
1354
|
+
text: anchor.textContent || "",
|
|
1355
|
+
};
|
|
1356
|
+
},
|
|
1357
|
+
),
|
|
1358
|
+
).toEqual([
|
|
1359
|
+
{
|
|
1360
|
+
href: `/dashboard/${PROJECT_ID}/incidents/${INCIDENT_SEVEN_ID}`,
|
|
1361
|
+
target: "_blank",
|
|
1362
|
+
text: "Incident INC-7",
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
href: `/dashboard/${PROJECT_ID}/incidents/${INCIDENT_EIGHT_ID}`,
|
|
1366
|
+
target: "_blank",
|
|
1367
|
+
text: "Incident #8",
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
href: `/dashboard/${PROJECT_ID}/incidents/${INCIDENT_NINE_ID}`,
|
|
1371
|
+
target: "_blank",
|
|
1372
|
+
text: "Incident",
|
|
1373
|
+
},
|
|
1374
|
+
]);
|
|
1375
|
+
|
|
1376
|
+
/*
|
|
1377
|
+
* Record whether the page cancelled the click once React has handled
|
|
1378
|
+
* it, then cancel it so JSDOM does not try to navigate.
|
|
1379
|
+
*/
|
|
1380
|
+
const defaultPreventedByPage: Array<boolean> = [];
|
|
1381
|
+
const recordAndCancel: (event: Event) => void = (event: Event): void => {
|
|
1382
|
+
defaultPreventedByPage.push(event.defaultPrevented);
|
|
1383
|
+
event.preventDefault();
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
document.addEventListener("click", recordAndCancel);
|
|
1387
|
+
|
|
1388
|
+
try {
|
|
1389
|
+
fireEvent.click(anchors[0]!);
|
|
1390
|
+
} finally {
|
|
1391
|
+
document.removeEventListener("click", recordAndCancel);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
expect(defaultPreventedByPage).toEqual([false]);
|
|
1395
|
+
expect(navigateSpy).not.toHaveBeenCalled();
|
|
1396
|
+
expect(screen.getByTestId("model-form")).toBeInTheDocument();
|
|
1397
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("lists every incident an alert is linked to, oldest link first, however it is numbered", async () => {
|
|
1401
|
+
fixture.links = [
|
|
1402
|
+
makeLink({
|
|
1403
|
+
alertId: ALERT_ONE_ID,
|
|
1404
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1405
|
+
incidentNumber: 7,
|
|
1406
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1407
|
+
}),
|
|
1408
|
+
makeLink({
|
|
1409
|
+
alertId: ALERT_ONE_ID,
|
|
1410
|
+
incidentId: INCIDENT_EIGHT_ID,
|
|
1411
|
+
incidentNumber: 8,
|
|
1412
|
+
}),
|
|
1413
|
+
makeLink({
|
|
1414
|
+
alertId: ALERT_ONE_ID,
|
|
1415
|
+
incidentId: INCIDENT_NINE_ID,
|
|
1416
|
+
}),
|
|
1417
|
+
];
|
|
1418
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1419
|
+
|
|
1420
|
+
await openPage();
|
|
1421
|
+
|
|
1422
|
+
const hint: HTMLElement = screen.getByTestId(ALREADY_LINKED_TEST_ID);
|
|
1423
|
+
|
|
1424
|
+
expect(hint.textContent).toBe(
|
|
1425
|
+
"(already linked to Incident INC-7, Incident #8, Incident)",
|
|
1426
|
+
);
|
|
1427
|
+
expect(
|
|
1428
|
+
Array.from(hint.querySelectorAll("a")).map(
|
|
1429
|
+
(anchor: HTMLAnchorElement): string | null => {
|
|
1430
|
+
return anchor.getAttribute("href");
|
|
1431
|
+
},
|
|
1432
|
+
),
|
|
1433
|
+
).toEqual([
|
|
1434
|
+
`/dashboard/${PROJECT_ID}/incidents/${INCIDENT_SEVEN_ID}`,
|
|
1435
|
+
`/dashboard/${PROJECT_ID}/incidents/${INCIDENT_EIGHT_ID}`,
|
|
1436
|
+
`/dashboard/${PROJECT_ID}/incidents/${INCIDENT_NINE_ID}`,
|
|
1437
|
+
]);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test("ignores a link whose incident could not be read", async () => {
|
|
1441
|
+
fixture.links = [makeLink({ alertId: ALERT_ONE_ID })];
|
|
1442
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1443
|
+
|
|
1444
|
+
await openPage();
|
|
1445
|
+
|
|
1446
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
1447
|
+
expect(
|
|
1448
|
+
screen.queryByTestId(ALREADY_LINKED_TEST_ID),
|
|
1449
|
+
).not.toBeInTheDocument();
|
|
1450
|
+
expect(
|
|
1451
|
+
screen.queryByTestId(ALREADY_LINKED_NOTE_TEST_ID),
|
|
1452
|
+
).not.toBeInTheDocument();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
test("shows no hint when there are no links", async () => {
|
|
1456
|
+
declareFrom([ALERT_ONE_ID, ALERT_TWO_ID]);
|
|
1457
|
+
|
|
1458
|
+
await openPage();
|
|
1459
|
+
|
|
1460
|
+
expect(
|
|
1461
|
+
screen.queryByTestId(ALREADY_LINKED_TEST_ID),
|
|
1462
|
+
).not.toBeInTheDocument();
|
|
1463
|
+
expect(
|
|
1464
|
+
screen.queryByTestId(ALREADY_LINKED_NOTE_TEST_ID),
|
|
1465
|
+
).not.toBeInTheDocument();
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
test("leaves the hint out, and the page working, when the links cannot be read", async () => {
|
|
1469
|
+
fixture.failLinks = true;
|
|
1470
|
+
fixture.links = [
|
|
1471
|
+
makeLink({
|
|
1472
|
+
alertId: ALERT_ONE_ID,
|
|
1473
|
+
incidentId: INCIDENT_SEVEN_ID,
|
|
1474
|
+
incidentNumberWithPrefix: "INC-7",
|
|
1475
|
+
}),
|
|
1476
|
+
];
|
|
1477
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1478
|
+
|
|
1479
|
+
const form: CapturedFormProps = await openPage();
|
|
1480
|
+
|
|
1481
|
+
expect(requestsFor(IncidentAlert)).toHaveLength(1);
|
|
1482
|
+
expect(screen.getByTestId(BANNER_TEST_ID)).toBeInTheDocument();
|
|
1483
|
+
expect(
|
|
1484
|
+
screen.queryByTestId(ALREADY_LINKED_TEST_ID),
|
|
1485
|
+
).not.toBeInTheDocument();
|
|
1486
|
+
expect(
|
|
1487
|
+
screen.queryByTestId(ALREADY_LINKED_NOTE_TEST_ID),
|
|
1488
|
+
).not.toBeInTheDocument();
|
|
1489
|
+
expect(
|
|
1490
|
+
screen.queryByText(
|
|
1491
|
+
"You do not have permission to read incident alerts.",
|
|
1492
|
+
),
|
|
1493
|
+
).toBeNull();
|
|
1494
|
+
expect(form.initialValues["title"]).toBe("Alert 1 title");
|
|
1495
|
+
expect(acknowledgeCheckbox()).toBeEnabled();
|
|
1496
|
+
expect(await miscDataSent()).toEqual({
|
|
1497
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ONE_ID],
|
|
1498
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
describe("the On-Call step's summary", () => {
|
|
1504
|
+
test("says the alerts' own escalation stops, and what may still page, when no policy runs and the box is ticked", async () => {
|
|
1505
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1506
|
+
|
|
1507
|
+
await openPage();
|
|
1508
|
+
|
|
1509
|
+
const text: string = onCallSummary({}).text;
|
|
1510
|
+
|
|
1511
|
+
expect(text).toBe(
|
|
1512
|
+
`${NO_POLICIES_TEXT} ${ACKNOWLEDGED_ALERTS_NO_ON_CALL_NOTE}`,
|
|
1513
|
+
);
|
|
1514
|
+
expect(text).toBe(
|
|
1515
|
+
`${NO_POLICIES_TEXT} The alerts it is declared from are acknowledged too, so their own escalation stops. An alert episode they belong to keeps escalating until the episode is acknowledged, and an incident on-call rule, if any, may still page.`,
|
|
1516
|
+
);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("treats an empty policy list as no policies", async () => {
|
|
1520
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1521
|
+
|
|
1522
|
+
await openPage();
|
|
1523
|
+
|
|
1524
|
+
const summary: RenderedSummary = onCallSummary({
|
|
1525
|
+
onCallDutyPolicies: [],
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
expect(summary.element.type).toBe("p");
|
|
1529
|
+
expect(summary.text).toBe(
|
|
1530
|
+
`${NO_POLICIES_TEXT} ${ACKNOWLEDGED_ALERTS_NO_ON_CALL_NOTE}`,
|
|
1531
|
+
);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test("says only that no policy runs once the box is unticked", async () => {
|
|
1535
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1536
|
+
|
|
1537
|
+
await openPage();
|
|
1538
|
+
|
|
1539
|
+
await untick();
|
|
1540
|
+
|
|
1541
|
+
expect(onCallSummary({}).text).toBe(NO_POLICIES_TEXT);
|
|
1542
|
+
expect(onCallSummary({ onCallDutyPolicies: [] }).text).toBe(
|
|
1543
|
+
NO_POLICIES_TEXT,
|
|
1544
|
+
);
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
test("says only that no policy runs when the box is locked", async () => {
|
|
1548
|
+
permissionsForTest = [Permission.Viewer];
|
|
1549
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1550
|
+
|
|
1551
|
+
await openPage();
|
|
1552
|
+
|
|
1553
|
+
expect(onCallSummary({}).text).toBe(NO_POLICIES_TEXT);
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test("says only that no policy runs when every alert is acknowledged already", async () => {
|
|
1557
|
+
fixture.alerts[0] = makeAlert(ALERT_ONE_ID, 1, ACKNOWLEDGED_STATE_ID);
|
|
1558
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1559
|
+
|
|
1560
|
+
await openPage();
|
|
1561
|
+
|
|
1562
|
+
expect(onCallSummary({}).text).toBe(NO_POLICIES_TEXT);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test("says only that no policy runs when not declaring from alerts", async () => {
|
|
1566
|
+
await openPage();
|
|
1567
|
+
|
|
1568
|
+
expect(onCallSummary({ onCallDutyPolicies: [] }).text).toBe(
|
|
1569
|
+
NO_POLICIES_TEXT,
|
|
1570
|
+
);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test("lists the chosen policies, without the note, when policies run", async () => {
|
|
1574
|
+
declareFrom([ALERT_ONE_ID]);
|
|
1575
|
+
|
|
1576
|
+
await openPage();
|
|
1577
|
+
|
|
1578
|
+
const summary: RenderedSummary = onCallSummary({
|
|
1579
|
+
onCallDutyPolicies: [POLICY_ID],
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
const child: React.ReactElement = (
|
|
1583
|
+
summary.element.props as { children: React.ReactElement }
|
|
1584
|
+
).children;
|
|
1585
|
+
|
|
1586
|
+
expect(summary.element.type).toBe("div");
|
|
1587
|
+
expect(child.type).toBe(FetchOnCallDutyPolicies);
|
|
1588
|
+
expect(
|
|
1589
|
+
(
|
|
1590
|
+
child.props as { onCallDutyPolicyIds: Array<ObjectID> }
|
|
1591
|
+
).onCallDutyPolicyIds.map((id: ObjectID): string => {
|
|
1592
|
+
return id.toString();
|
|
1593
|
+
}),
|
|
1594
|
+
).toEqual([POLICY_ID]);
|
|
1595
|
+
|
|
1596
|
+
await waitFor(() => {
|
|
1597
|
+
expect(summary.container).toHaveTextContent("Primary on-call");
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
const policyRequests: Array<any> = requestsFor(OnCallDutyPolicy);
|
|
1601
|
+
|
|
1602
|
+
expect(
|
|
1603
|
+
idsIn(policyRequests[policyRequests.length - 1].query._id),
|
|
1604
|
+
).toEqual([POLICY_ID]);
|
|
1605
|
+
expect(summary.container.textContent).not.toContain(
|
|
1606
|
+
ACKNOWLEDGED_ALERTS_NO_ON_CALL_NOTE,
|
|
1607
|
+
);
|
|
1608
|
+
expect(summary.container.textContent).not.toContain(NO_POLICIES_TEXT);
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1611
|
+
});
|