@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import EventStatusPanel, {
|
|
2
2
|
ComponentProps,
|
|
3
|
+
EventPanelAction,
|
|
3
4
|
EventStateAction,
|
|
4
5
|
EventStateItem,
|
|
5
6
|
} from "../../../../App/FeatureSet/Dashboard/src/Components/EventView/EventStatusPanel";
|
|
@@ -19,6 +20,9 @@ import { Black, Red500 } from "../../../Types/BrandColors";
|
|
|
19
20
|
import Color from "../../../Types/Color";
|
|
20
21
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
21
22
|
|
|
23
|
+
// React's warning for list items without a (unique) key.
|
|
24
|
+
const KEY_WARNING_PATTERN: RegExp = /unique "key"|same key/;
|
|
25
|
+
|
|
22
26
|
jest.mock("react-i18next", () => {
|
|
23
27
|
return {
|
|
24
28
|
useTranslation: () => {
|
|
@@ -542,7 +546,7 @@ describe("EventStatusPanel disabled behavior", () => {
|
|
|
542
546
|
});
|
|
543
547
|
|
|
544
548
|
describe("EventStatusPanel action-group layout", () => {
|
|
545
|
-
test("labels one responsive wrapping group for every action control", () => {
|
|
549
|
+
test("labels one responsive wrapping group for every action control (compact layout)", () => {
|
|
546
550
|
renderPanel();
|
|
547
551
|
|
|
548
552
|
const group: HTMLElement = getActionGroup();
|
|
@@ -607,11 +611,53 @@ describe("EventStatusPanel header layouts", () => {
|
|
|
607
611
|
|
|
608
612
|
expect(identifier).toHaveTextContent("INC-42");
|
|
609
613
|
expect(identifier).toHaveClass("bg-gray-100", "uppercase");
|
|
610
|
-
expect(headerRow).toHaveClass("
|
|
614
|
+
expect(headerRow).toHaveClass("xl:items-start", "xl:justify-between");
|
|
611
615
|
expect(headerRow).toContainElement(group);
|
|
612
616
|
expect(headerRow).not.toContainElement(screen.getByTestId("pill"));
|
|
613
617
|
});
|
|
614
618
|
|
|
619
|
+
/*
|
|
620
|
+
* Beside a side menu, a header that went side by side from md squeezed the
|
|
621
|
+
* title to a few characters and stacked the actions one per row, so the
|
|
622
|
+
* titled header stacks the actions under the title until xl.
|
|
623
|
+
*/
|
|
624
|
+
test("stacks the actions under the title until xl, and puts them beside it only from xl", () => {
|
|
625
|
+
renderPanel({ title: "Database connection failures" });
|
|
626
|
+
|
|
627
|
+
const heading: HTMLElement = screen.getByRole("heading", {
|
|
628
|
+
level: 2,
|
|
629
|
+
name: "Database connection failures",
|
|
630
|
+
});
|
|
631
|
+
const headerRow: HTMLElement = (heading.parentElement as HTMLElement)
|
|
632
|
+
.parentElement as HTMLElement;
|
|
633
|
+
|
|
634
|
+
expect(headerRow).toHaveClass(
|
|
635
|
+
"flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between",
|
|
636
|
+
{ exact: true },
|
|
637
|
+
);
|
|
638
|
+
expect(headerRow).not.toHaveClass("md:flex-row");
|
|
639
|
+
expect(headerRow).not.toHaveClass("md:items-start");
|
|
640
|
+
expect(headerRow).not.toHaveClass("md:justify-between");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("keeps the titled header's action group full width until xl", () => {
|
|
644
|
+
renderPanel({ title: "Database connection failures" });
|
|
645
|
+
|
|
646
|
+
const group: HTMLElement = getActionGroup();
|
|
647
|
+
|
|
648
|
+
expect(group).toHaveClass(
|
|
649
|
+
"flex w-full flex-wrap items-center justify-end gap-2 xl:w-auto xl:shrink-0",
|
|
650
|
+
{ exact: true },
|
|
651
|
+
);
|
|
652
|
+
expect(group).not.toHaveClass("md:w-auto");
|
|
653
|
+
expect(group).toHaveAttribute("aria-label", "Event actions");
|
|
654
|
+
expect(within(group).getAllByRole("button")).toEqual([
|
|
655
|
+
screen.getByRole("button", { name: "Acknowledge" }),
|
|
656
|
+
screen.getByRole("button", { name: "Resolve" }),
|
|
657
|
+
screen.getByRole("button", { name: "More actions" }),
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
|
|
615
661
|
test("uses the compact inline metadata layout when no title is supplied", () => {
|
|
616
662
|
renderPanel();
|
|
617
663
|
|
|
@@ -628,6 +674,25 @@ describe("EventStatusPanel header layouts", () => {
|
|
|
628
674
|
expect(compactRow).toContainElement(screen.getByTestId("pill"));
|
|
629
675
|
});
|
|
630
676
|
|
|
677
|
+
// Only the titled header moved to xl; the compact layout is as it was.
|
|
678
|
+
test("keeps the compact layout side by side from md, with its action group full width until md", () => {
|
|
679
|
+
renderPanel();
|
|
680
|
+
|
|
681
|
+
const group: HTMLElement = getActionGroup();
|
|
682
|
+
const compactRow: HTMLElement = group.parentElement as HTMLElement;
|
|
683
|
+
|
|
684
|
+
expect(compactRow).toHaveClass(
|
|
685
|
+
"flex flex-col gap-3 px-4 py-4 sm:px-5 md:flex-row md:items-center md:justify-between",
|
|
686
|
+
{ exact: true },
|
|
687
|
+
);
|
|
688
|
+
expect(compactRow).not.toHaveClass("xl:flex-row");
|
|
689
|
+
expect(group).toHaveClass(
|
|
690
|
+
"flex w-full flex-wrap items-center justify-end gap-2 md:w-auto",
|
|
691
|
+
{ exact: true },
|
|
692
|
+
);
|
|
693
|
+
expect(group).not.toHaveClass("xl:w-auto");
|
|
694
|
+
});
|
|
695
|
+
|
|
631
696
|
test("falls back to black when hydrated current-state color is absent", () => {
|
|
632
697
|
renderPanel({
|
|
633
698
|
states: [
|
|
@@ -992,3 +1057,734 @@ describe("EventStatusPanel header facts", () => {
|
|
|
992
1057
|
expect(getActionGroup()).not.toContainElement(getFacts());
|
|
993
1058
|
});
|
|
994
1059
|
});
|
|
1060
|
+
|
|
1061
|
+
/*
|
|
1062
|
+
* Secondary actions are the header's non-state buttons - "Declare Incident"
|
|
1063
|
+
* on an alert is the first. They share the state buttons' row and size but
|
|
1064
|
+
* never their primary look, never reach onActionClick (which would open a
|
|
1065
|
+
* state-change modal for a state that does not exist), never change what the
|
|
1066
|
+
* "More actions" menu offers, and stay on screen when no state action is
|
|
1067
|
+
* left. A locked one explains itself through a hoverable, focusable wrapper,
|
|
1068
|
+
* because a disabled <button> cannot trigger a tooltip.
|
|
1069
|
+
*/
|
|
1070
|
+
describe("EventStatusPanel secondary actions", () => {
|
|
1071
|
+
const DECLARE_ID: string = "alert-declare-incident-btn";
|
|
1072
|
+
const DECLARE_LABEL: string = "Declare Incident";
|
|
1073
|
+
const LOCKED_REASON: string =
|
|
1074
|
+
"You do not have permission to create this Incident.";
|
|
1075
|
+
|
|
1076
|
+
type MakeSecondaryActionFunction = (
|
|
1077
|
+
overrides?: Partial<EventPanelAction> | undefined,
|
|
1078
|
+
) => EventPanelAction;
|
|
1079
|
+
|
|
1080
|
+
const makeSecondaryAction: MakeSecondaryActionFunction = (
|
|
1081
|
+
overrides?: Partial<EventPanelAction> | undefined,
|
|
1082
|
+
): EventPanelAction => {
|
|
1083
|
+
return {
|
|
1084
|
+
id: DECLARE_ID,
|
|
1085
|
+
label: DECLARE_LABEL,
|
|
1086
|
+
icon: IconProp.Alert,
|
|
1087
|
+
onClick: (): void => {},
|
|
1088
|
+
...overrides,
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
type GetButtonFunction = (name: string) => HTMLElement;
|
|
1093
|
+
|
|
1094
|
+
const getButton: GetButtonFunction = (name: string): HTMLElement => {
|
|
1095
|
+
return screen.getByRole("button", { name: name });
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
type IsBeforeFunction = (first: Node, second: Node) => boolean;
|
|
1099
|
+
|
|
1100
|
+
const isBefore: IsBeforeFunction = (first: Node, second: Node): boolean => {
|
|
1101
|
+
return Boolean(
|
|
1102
|
+
first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1103
|
+
);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
afterEach(() => {
|
|
1107
|
+
jest.restoreAllMocks();
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
/*
|
|
1111
|
+
* First in this block on purpose: React reports a missing list key only
|
|
1112
|
+
* once per owner component, so the first render with secondary actions is
|
|
1113
|
+
* the one that would warn.
|
|
1114
|
+
*/
|
|
1115
|
+
test("keys each action by its id, so reordering moves rather than relabels them", () => {
|
|
1116
|
+
const consoleErrorSpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
|
|
1117
|
+
console,
|
|
1118
|
+
"error",
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
const first: EventPanelAction = makeSecondaryAction({
|
|
1122
|
+
id: "first-secondary-btn",
|
|
1123
|
+
label: "First",
|
|
1124
|
+
});
|
|
1125
|
+
const second: EventPanelAction = makeSecondaryAction({
|
|
1126
|
+
id: "second-secondary-btn",
|
|
1127
|
+
label: "Second",
|
|
1128
|
+
isDisabled: true,
|
|
1129
|
+
tooltip: LOCKED_REASON,
|
|
1130
|
+
});
|
|
1131
|
+
const third: EventPanelAction = makeSecondaryAction({
|
|
1132
|
+
id: "third-secondary-btn",
|
|
1133
|
+
label: "Third",
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
const baseProps: ComponentProps = {
|
|
1137
|
+
states: defaultStates(),
|
|
1138
|
+
identifier: "ALR-12",
|
|
1139
|
+
currentStateId: "created",
|
|
1140
|
+
actions: defaultActions(),
|
|
1141
|
+
onActionClick: (): void => {},
|
|
1142
|
+
onStateSelect: (): void => {},
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const { rerender } = render(
|
|
1146
|
+
<EventStatusPanel
|
|
1147
|
+
{...baseProps}
|
|
1148
|
+
secondaryActions={[first, second, third]}
|
|
1149
|
+
/>,
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const firstNode: HTMLElement = getButton("First");
|
|
1153
|
+
const secondWrapper: HTMLElement = screen.getByTestId(
|
|
1154
|
+
"second-secondary-btn-disabled-wrapper",
|
|
1155
|
+
);
|
|
1156
|
+
const thirdNode: HTMLElement = getButton("Third");
|
|
1157
|
+
|
|
1158
|
+
rerender(
|
|
1159
|
+
<EventStatusPanel
|
|
1160
|
+
{...baseProps}
|
|
1161
|
+
secondaryActions={[third, second, first]}
|
|
1162
|
+
/>,
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
// Same DOM nodes, now in the new order.
|
|
1166
|
+
expect(getButton("First")).toBe(firstNode);
|
|
1167
|
+
expect(getButton("Third")).toBe(thirdNode);
|
|
1168
|
+
expect(screen.getByTestId("second-secondary-btn-disabled-wrapper")).toBe(
|
|
1169
|
+
secondWrapper,
|
|
1170
|
+
);
|
|
1171
|
+
expect(firstNode).toHaveAttribute("id", "first-secondary-btn");
|
|
1172
|
+
expect(thirdNode).toHaveAttribute("id", "third-secondary-btn");
|
|
1173
|
+
expect(isBefore(thirdNode, secondWrapper)).toBe(true);
|
|
1174
|
+
expect(isBefore(secondWrapper, firstNode)).toBe(true);
|
|
1175
|
+
|
|
1176
|
+
const keyWarnings: Array<unknown> = consoleErrorSpy.mock.calls.filter(
|
|
1177
|
+
(call: Array<unknown>): boolean => {
|
|
1178
|
+
return KEY_WARNING_PATTERN.test(String(call[0]));
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
expect(keyWarnings).toEqual([]);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test("renders after the state buttons and before the More actions trigger", () => {
|
|
1186
|
+
renderPanel({ secondaryActions: [makeSecondaryAction()] });
|
|
1187
|
+
|
|
1188
|
+
const group: HTMLElement = getActionGroup();
|
|
1189
|
+
const acknowledge: HTMLElement = within(group).getByRole("button", {
|
|
1190
|
+
name: "Acknowledge",
|
|
1191
|
+
});
|
|
1192
|
+
const resolve: HTMLElement = within(group).getByRole("button", {
|
|
1193
|
+
name: "Resolve",
|
|
1194
|
+
});
|
|
1195
|
+
const declare: HTMLElement = within(group).getByRole("button", {
|
|
1196
|
+
name: DECLARE_LABEL,
|
|
1197
|
+
});
|
|
1198
|
+
const more: HTMLElement = within(group).getByRole("button", {
|
|
1199
|
+
name: "More actions",
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
expect(within(group).getAllByRole("button")).toEqual([
|
|
1203
|
+
acknowledge,
|
|
1204
|
+
resolve,
|
|
1205
|
+
declare,
|
|
1206
|
+
more,
|
|
1207
|
+
]);
|
|
1208
|
+
// A direct child of the group, like the state buttons - not nested.
|
|
1209
|
+
expect(declare.parentElement).toBe(group);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
test("is a native button with its id and label as title", () => {
|
|
1213
|
+
renderPanel({ secondaryActions: [makeSecondaryAction()] });
|
|
1214
|
+
|
|
1215
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1216
|
+
|
|
1217
|
+
expect(declare.tagName).toBe("BUTTON");
|
|
1218
|
+
expect(declare).toHaveAttribute("type", "button");
|
|
1219
|
+
expect(declare).toHaveAttribute("id", DECLARE_ID);
|
|
1220
|
+
expect(declare).toHaveAttribute("title", DECLARE_LABEL);
|
|
1221
|
+
expect(declare).toBeEnabled();
|
|
1222
|
+
expect(declare).toHaveAttribute("aria-disabled", "false");
|
|
1223
|
+
expect(declare.querySelector("span")).toHaveClass("truncate");
|
|
1224
|
+
expect(declare.querySelector("span")).toHaveTextContent(DECLARE_LABEL);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
test("uses the state buttons' base classes with the neutral, never the indigo, look", () => {
|
|
1228
|
+
renderPanel({ secondaryActions: [makeSecondaryAction()] });
|
|
1229
|
+
|
|
1230
|
+
const acknowledge: HTMLElement = getButton("Acknowledge");
|
|
1231
|
+
// DANGER renders neutral, so Resolve is the neutral reference.
|
|
1232
|
+
const resolve: HTMLElement = getButton("Resolve");
|
|
1233
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1234
|
+
|
|
1235
|
+
const primaryVariantClasses: Array<string> = [
|
|
1236
|
+
"border-indigo-600",
|
|
1237
|
+
"bg-indigo-600",
|
|
1238
|
+
"text-white",
|
|
1239
|
+
"hover:border-indigo-700",
|
|
1240
|
+
"hover:bg-indigo-700",
|
|
1241
|
+
];
|
|
1242
|
+
const baseClasses: Array<string> = acknowledge.className
|
|
1243
|
+
.split(/\s+/)
|
|
1244
|
+
.filter((className: string): boolean => {
|
|
1245
|
+
return (
|
|
1246
|
+
className.length > 0 && !primaryVariantClasses.includes(className)
|
|
1247
|
+
);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
expect(baseClasses).toEqual(
|
|
1251
|
+
expect.arrayContaining(["inline-flex", "h-9", "min-w-[7rem]"]),
|
|
1252
|
+
);
|
|
1253
|
+
expect(declare).toHaveClass(...baseClasses);
|
|
1254
|
+
expect(declare.className).toBe(resolve.className);
|
|
1255
|
+
expect(declare).toHaveClass(
|
|
1256
|
+
"border-gray-300",
|
|
1257
|
+
"bg-white",
|
|
1258
|
+
"text-gray-700",
|
|
1259
|
+
"hover:bg-gray-50",
|
|
1260
|
+
);
|
|
1261
|
+
expect(declare).not.toHaveClass(
|
|
1262
|
+
"bg-indigo-600",
|
|
1263
|
+
"border-indigo-600",
|
|
1264
|
+
"text-white",
|
|
1265
|
+
"pointer-events-none",
|
|
1266
|
+
"w-full",
|
|
1267
|
+
);
|
|
1268
|
+
expect(declare).not.toHaveAttribute("style");
|
|
1269
|
+
|
|
1270
|
+
// The state action stays the one primary button in the row.
|
|
1271
|
+
const primaryButtons: Array<HTMLElement> = within(getActionGroup())
|
|
1272
|
+
.getAllByRole("button")
|
|
1273
|
+
.filter((button: HTMLElement): boolean => {
|
|
1274
|
+
return button.classList.contains("bg-indigo-600");
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
expect(primaryButtons).toEqual([acknowledge]);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
test("translates its label like the state labels", () => {
|
|
1281
|
+
renderPanel({
|
|
1282
|
+
secondaryActions: [
|
|
1283
|
+
makeSecondaryAction({ label: "Resolve translation key" }),
|
|
1284
|
+
],
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
const translated: HTMLElement = getButton("Résoudre");
|
|
1288
|
+
|
|
1289
|
+
expect(translated).toHaveAttribute("id", DECLARE_ID);
|
|
1290
|
+
expect(translated).toHaveAttribute("title", "Résoudre");
|
|
1291
|
+
expect(
|
|
1292
|
+
screen.queryByRole("button", { name: "Resolve translation key" }),
|
|
1293
|
+
).not.toBeInTheDocument();
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("renders its icon at the state buttons' icon size, and none when it has no icon", () => {
|
|
1297
|
+
renderPanel({
|
|
1298
|
+
secondaryActions: [
|
|
1299
|
+
makeSecondaryAction(),
|
|
1300
|
+
makeSecondaryAction({
|
|
1301
|
+
id: "iconless-secondary-btn",
|
|
1302
|
+
label: "Iconless",
|
|
1303
|
+
icon: undefined,
|
|
1304
|
+
}),
|
|
1305
|
+
],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const declareIcon: SVGElement | null =
|
|
1309
|
+
getButton(DECLARE_LABEL).querySelector("svg");
|
|
1310
|
+
const acknowledgeIcon: SVGElement | null =
|
|
1311
|
+
getButton("Acknowledge").querySelector("svg");
|
|
1312
|
+
|
|
1313
|
+
expect(declareIcon).not.toBeNull();
|
|
1314
|
+
expect(declareIcon).toHaveClass("h-4", "w-4", "shrink-0");
|
|
1315
|
+
expect(declareIcon?.innerHTML).not.toEqual(acknowledgeIcon?.innerHTML);
|
|
1316
|
+
expect(getButton("Iconless").querySelector("svg")).toBeNull();
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test("calls its own onClick exactly once and never the state callbacks", () => {
|
|
1320
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1321
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1322
|
+
secondaryActions: [makeSecondaryAction({ onClick: onDeclare })],
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
fireEvent.click(getButton(DECLARE_LABEL));
|
|
1326
|
+
|
|
1327
|
+
expect(onDeclare).toHaveBeenCalledTimes(1);
|
|
1328
|
+
expect(onDeclare).toHaveBeenCalledWith();
|
|
1329
|
+
expect(rendered.actionClicks).toEqual([]);
|
|
1330
|
+
expect(rendered.stateSelections).toEqual([]);
|
|
1331
|
+
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
1332
|
+
|
|
1333
|
+
// The state buttons beside it still report their own states only.
|
|
1334
|
+
fireEvent.click(getButton("Acknowledge"));
|
|
1335
|
+
|
|
1336
|
+
expect(rendered.actionClicks).toEqual(["acknowledged"]);
|
|
1337
|
+
expect(onDeclare).toHaveBeenCalledTimes(1);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test("never reaches onActionClick even when its id matches a state id", () => {
|
|
1341
|
+
const onClick: MockFunction = getJestMockFunction();
|
|
1342
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1343
|
+
secondaryActions: [
|
|
1344
|
+
makeSecondaryAction({
|
|
1345
|
+
id: "resolved",
|
|
1346
|
+
label: "Look-alike",
|
|
1347
|
+
onClick: onClick,
|
|
1348
|
+
}),
|
|
1349
|
+
],
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
fireEvent.click(getButton("Look-alike"));
|
|
1353
|
+
|
|
1354
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
1355
|
+
expect(rendered.actionClicks).toEqual([]);
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
test("leaves the More actions menu exactly as it was", () => {
|
|
1359
|
+
renderPanel();
|
|
1360
|
+
|
|
1361
|
+
const withoutSecondary: Array<string> = menuChoiceNames(openMenu());
|
|
1362
|
+
|
|
1363
|
+
cleanup();
|
|
1364
|
+
|
|
1365
|
+
renderPanel({ secondaryActions: [makeSecondaryAction()] });
|
|
1366
|
+
|
|
1367
|
+
const menu: HTMLElement = openMenu();
|
|
1368
|
+
|
|
1369
|
+
expect(withoutSecondary).toEqual(["Investigating"]);
|
|
1370
|
+
expect(menuChoiceNames(menu)).toEqual(withoutSecondary);
|
|
1371
|
+
expect(within(menu).queryByText(DECLARE_LABEL)).not.toBeInTheDocument();
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test.each(["investigating", "acknowledged", "resolved", "created"])(
|
|
1375
|
+
"does not hide the %s state from the menu when an action shares its id",
|
|
1376
|
+
(sharedId: string) => {
|
|
1377
|
+
renderPanel({
|
|
1378
|
+
actions: [],
|
|
1379
|
+
secondaryActions: [
|
|
1380
|
+
makeSecondaryAction({ id: sharedId, label: "Look-alike" }),
|
|
1381
|
+
],
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
expect(menuChoiceNames(openMenu())).toEqual([
|
|
1385
|
+
"Acknowledged",
|
|
1386
|
+
"Investigating",
|
|
1387
|
+
"Resolved",
|
|
1388
|
+
]);
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
test("reports a menu selection as a state change, not a secondary click", () => {
|
|
1393
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1394
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1395
|
+
secondaryActions: [makeSecondaryAction({ onClick: onDeclare })],
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
fireEvent.click(getMenuChoice(openMenu(), "Investigating"));
|
|
1399
|
+
|
|
1400
|
+
expect(rendered.stateSelections).toEqual(["investigating"]);
|
|
1401
|
+
expect(onDeclare).not.toHaveBeenCalled();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
test("does not conjure a More actions menu when every other state already has a button", () => {
|
|
1405
|
+
renderPanel({
|
|
1406
|
+
states: [
|
|
1407
|
+
makeState("created", "Created"),
|
|
1408
|
+
makeState("acknowledged", "Acknowledged"),
|
|
1409
|
+
makeState("resolved", "Resolved"),
|
|
1410
|
+
],
|
|
1411
|
+
secondaryActions: [makeSecondaryAction()],
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
expect(
|
|
1415
|
+
screen.queryByRole("button", { name: "More actions" }),
|
|
1416
|
+
).not.toBeInTheDocument();
|
|
1417
|
+
expect(within(getActionGroup()).getAllByRole("button")).toEqual([
|
|
1418
|
+
getButton("Acknowledge"),
|
|
1419
|
+
getButton("Resolve"),
|
|
1420
|
+
getButton(DECLARE_LABEL),
|
|
1421
|
+
]);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test("stays on a resolved event, where it is the only button", () => {
|
|
1425
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1426
|
+
|
|
1427
|
+
renderPanel({
|
|
1428
|
+
currentStateId: "resolved",
|
|
1429
|
+
actions: [],
|
|
1430
|
+
secondaryActions: [makeSecondaryAction({ onClick: onDeclare })],
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
const group: HTMLElement = getActionGroup();
|
|
1434
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1435
|
+
|
|
1436
|
+
expect(within(group).getAllByRole("button")).toEqual([declare]);
|
|
1437
|
+
expect(declare).toBeEnabled();
|
|
1438
|
+
|
|
1439
|
+
fireEvent.click(declare);
|
|
1440
|
+
|
|
1441
|
+
expect(onDeclare).toHaveBeenCalledTimes(1);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
test("renders even when the panel has no states at all", () => {
|
|
1445
|
+
render(
|
|
1446
|
+
<EventStatusPanel
|
|
1447
|
+
title="Database latency"
|
|
1448
|
+
states={[]}
|
|
1449
|
+
actions={[]}
|
|
1450
|
+
onActionClick={() => {}}
|
|
1451
|
+
secondaryActions={[makeSecondaryAction()]}
|
|
1452
|
+
/>,
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
expect(within(getActionGroup()).getAllByRole("button")).toEqual([
|
|
1456
|
+
getButton(DECLARE_LABEL),
|
|
1457
|
+
]);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
test.each([
|
|
1461
|
+
["undefined", undefined],
|
|
1462
|
+
["an empty list", []],
|
|
1463
|
+
])(
|
|
1464
|
+
"renders nothing extra when secondaryActions is %s",
|
|
1465
|
+
(_label: string, secondaryActions: Array<EventPanelAction> | undefined) => {
|
|
1466
|
+
renderPanel({ secondaryActions: secondaryActions });
|
|
1467
|
+
|
|
1468
|
+
const group: HTMLElement = getActionGroup();
|
|
1469
|
+
|
|
1470
|
+
expect(within(group).getAllByRole("button")).toEqual([
|
|
1471
|
+
getButton("Acknowledge"),
|
|
1472
|
+
getButton("Resolve"),
|
|
1473
|
+
getMoreActionsTrigger(),
|
|
1474
|
+
]);
|
|
1475
|
+
expect(document.getElementById(DECLARE_ID)).toBeNull();
|
|
1476
|
+
expect(
|
|
1477
|
+
document.querySelector('[data-testid$="-disabled-wrapper"]'),
|
|
1478
|
+
).toBeNull();
|
|
1479
|
+
},
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
test("is disabled with the rest of the panel, and a click does nothing", () => {
|
|
1483
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1484
|
+
|
|
1485
|
+
renderPanel({
|
|
1486
|
+
isDisabled: true,
|
|
1487
|
+
secondaryActions: [makeSecondaryAction({ onClick: onDeclare })],
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1491
|
+
|
|
1492
|
+
expect(declare).toBeDisabled();
|
|
1493
|
+
expect(declare).toHaveAttribute("aria-disabled", "true");
|
|
1494
|
+
// No reason to show, so no wrapper and the label stays as the title.
|
|
1495
|
+
expect(screen.queryByTestId(`${DECLARE_ID}-disabled-wrapper`)).toBeNull();
|
|
1496
|
+
expect(declare).toHaveAttribute("title", DECLARE_LABEL);
|
|
1497
|
+
|
|
1498
|
+
fireEvent.click(declare);
|
|
1499
|
+
|
|
1500
|
+
expect(onDeclare).not.toHaveBeenCalled();
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
test("gets the tooltip wrapper when the panel is disabled and the action carries a reason", () => {
|
|
1504
|
+
renderPanel({
|
|
1505
|
+
isDisabled: true,
|
|
1506
|
+
secondaryActions: [makeSecondaryAction({ tooltip: LOCKED_REASON })],
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const wrapper: HTMLElement = screen.getByTestId(
|
|
1510
|
+
`${DECLARE_ID}-disabled-wrapper`,
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
expect(wrapper).toContainElement(getButton(DECLARE_LABEL));
|
|
1514
|
+
expect(getButton(DECLARE_LABEL)).toBeDisabled();
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
test.each([
|
|
1518
|
+
["no tooltip", undefined],
|
|
1519
|
+
["an empty tooltip", ""],
|
|
1520
|
+
])(
|
|
1521
|
+
"a disabled action with %s is disabled in place, with no wrapper",
|
|
1522
|
+
(_label: string, tooltip: string | undefined) => {
|
|
1523
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1524
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1525
|
+
secondaryActions: [
|
|
1526
|
+
makeSecondaryAction({
|
|
1527
|
+
isDisabled: true,
|
|
1528
|
+
tooltip: tooltip,
|
|
1529
|
+
onClick: onDeclare,
|
|
1530
|
+
}),
|
|
1531
|
+
],
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1535
|
+
|
|
1536
|
+
expect(declare).toBeDisabled();
|
|
1537
|
+
expect(declare).toHaveAttribute("aria-disabled", "true");
|
|
1538
|
+
expect(declare).toHaveAttribute("title", DECLARE_LABEL);
|
|
1539
|
+
expect(declare).not.toHaveClass("pointer-events-none", "w-full");
|
|
1540
|
+
expect(declare.parentElement).toBe(getActionGroup());
|
|
1541
|
+
expect(screen.queryByTestId(`${DECLARE_ID}-disabled-wrapper`)).toBeNull();
|
|
1542
|
+
|
|
1543
|
+
fireEvent.click(declare);
|
|
1544
|
+
|
|
1545
|
+
expect(onDeclare).not.toHaveBeenCalled();
|
|
1546
|
+
// Only this action is locked; the state controls still work.
|
|
1547
|
+
expect(getButton("Acknowledge")).toBeEnabled();
|
|
1548
|
+
expect(getButton("Resolve")).toBeEnabled();
|
|
1549
|
+
expect(getMoreActionsTrigger()).toBeEnabled();
|
|
1550
|
+
|
|
1551
|
+
fireEvent.click(getButton("Resolve"));
|
|
1552
|
+
|
|
1553
|
+
expect(rendered.actionClicks).toEqual(["resolved"]);
|
|
1554
|
+
},
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
test("a disabled action with a reason hands the pointer and focus to a wrapper", () => {
|
|
1558
|
+
renderPanel({
|
|
1559
|
+
secondaryActions: [
|
|
1560
|
+
makeSecondaryAction({ isDisabled: true, tooltip: LOCKED_REASON }),
|
|
1561
|
+
],
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
const group: HTMLElement = getActionGroup();
|
|
1565
|
+
const wrapper: HTMLElement = screen.getByTestId(
|
|
1566
|
+
`${DECLARE_ID}-disabled-wrapper`,
|
|
1567
|
+
);
|
|
1568
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1569
|
+
|
|
1570
|
+
expect(wrapper.tagName).toBe("SPAN");
|
|
1571
|
+
expect(wrapper).toHaveAttribute("tabindex", "0");
|
|
1572
|
+
expect(wrapper.tabIndex).toBe(0);
|
|
1573
|
+
expect(wrapper).toHaveClass(
|
|
1574
|
+
"inline-flex",
|
|
1575
|
+
"min-w-[7rem]",
|
|
1576
|
+
"max-w-full",
|
|
1577
|
+
"flex-auto",
|
|
1578
|
+
"rounded-md",
|
|
1579
|
+
"sm:max-w-64",
|
|
1580
|
+
"sm:flex-none",
|
|
1581
|
+
);
|
|
1582
|
+
expect(declare.parentElement).toBe(wrapper);
|
|
1583
|
+
expect(wrapper.parentElement).toBe(group);
|
|
1584
|
+
|
|
1585
|
+
// Taken out of hit-testing so the wrapper receives the hover.
|
|
1586
|
+
expect(declare).toHaveClass("pointer-events-none", "w-full");
|
|
1587
|
+
// The tooltip carries the message; a native title would compete with it.
|
|
1588
|
+
expect(declare).not.toHaveAttribute("title");
|
|
1589
|
+
expect(declare).toBeDisabled();
|
|
1590
|
+
expect(declare).toHaveAttribute("aria-disabled", "true");
|
|
1591
|
+
// Still the neutral look.
|
|
1592
|
+
expect(declare).toHaveClass("border-gray-300", "bg-white", "text-gray-700");
|
|
1593
|
+
expect(declare).not.toHaveClass("bg-indigo-600");
|
|
1594
|
+
|
|
1595
|
+
// Still between the state buttons and the menu.
|
|
1596
|
+
expect(isBefore(getButton("Resolve"), wrapper)).toBe(true);
|
|
1597
|
+
expect(isBefore(wrapper, getMoreActionsTrigger())).toBe(true);
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
test("shows the reason when the wrapper is hovered", () => {
|
|
1601
|
+
renderPanel({
|
|
1602
|
+
secondaryActions: [
|
|
1603
|
+
makeSecondaryAction({ isDisabled: true, tooltip: LOCKED_REASON }),
|
|
1604
|
+
],
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
|
1608
|
+
|
|
1609
|
+
fireEvent.mouseEnter(screen.getByTestId(`${DECLARE_ID}-disabled-wrapper`));
|
|
1610
|
+
|
|
1611
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(LOCKED_REASON);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
test("shows the reason when the wrapper is focused from the keyboard", () => {
|
|
1615
|
+
renderPanel({
|
|
1616
|
+
secondaryActions: [
|
|
1617
|
+
makeSecondaryAction({ isDisabled: true, tooltip: LOCKED_REASON }),
|
|
1618
|
+
],
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
const wrapper: HTMLElement = screen.getByTestId(
|
|
1622
|
+
`${DECLARE_ID}-disabled-wrapper`,
|
|
1623
|
+
);
|
|
1624
|
+
|
|
1625
|
+
wrapper.focus();
|
|
1626
|
+
|
|
1627
|
+
expect(document.activeElement).toBe(wrapper);
|
|
1628
|
+
|
|
1629
|
+
fireEvent.focus(wrapper);
|
|
1630
|
+
|
|
1631
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(LOCKED_REASON);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
test("translates the reason like the label", () => {
|
|
1635
|
+
renderPanel({
|
|
1636
|
+
secondaryActions: [
|
|
1637
|
+
makeSecondaryAction({
|
|
1638
|
+
isDisabled: true,
|
|
1639
|
+
tooltip: "Resolve translation key",
|
|
1640
|
+
}),
|
|
1641
|
+
],
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
fireEvent.mouseEnter(screen.getByTestId(`${DECLARE_ID}-disabled-wrapper`));
|
|
1645
|
+
|
|
1646
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent("Résoudre");
|
|
1647
|
+
expect(screen.getByRole("tooltip")).not.toHaveTextContent(
|
|
1648
|
+
"Resolve translation key",
|
|
1649
|
+
);
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
test("a click on a locked action with a reason does nothing", () => {
|
|
1653
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1654
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1655
|
+
secondaryActions: [
|
|
1656
|
+
makeSecondaryAction({
|
|
1657
|
+
isDisabled: true,
|
|
1658
|
+
tooltip: LOCKED_REASON,
|
|
1659
|
+
onClick: onDeclare,
|
|
1660
|
+
}),
|
|
1661
|
+
],
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
fireEvent.click(screen.getByTestId(`${DECLARE_ID}-disabled-wrapper`));
|
|
1665
|
+
fireEvent.click(getButton(DECLARE_LABEL));
|
|
1666
|
+
|
|
1667
|
+
expect(onDeclare).not.toHaveBeenCalled();
|
|
1668
|
+
expect(rendered.actionClicks).toEqual([]);
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
test("an enabled action ignores its tooltip and gets no wrapper", () => {
|
|
1672
|
+
renderPanel({
|
|
1673
|
+
secondaryActions: [makeSecondaryAction({ tooltip: LOCKED_REASON })],
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1677
|
+
|
|
1678
|
+
expect(screen.queryByTestId(`${DECLARE_ID}-disabled-wrapper`)).toBeNull();
|
|
1679
|
+
expect(declare.parentElement).toBe(getActionGroup());
|
|
1680
|
+
expect(declare).toBeEnabled();
|
|
1681
|
+
expect(declare).toHaveAttribute("title", DECLARE_LABEL);
|
|
1682
|
+
expect(declare).not.toHaveClass("pointer-events-none");
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
test.each([
|
|
1686
|
+
["titled", "Database latency"],
|
|
1687
|
+
["compact", undefined],
|
|
1688
|
+
])("works in the %s layout", (_layout: string, title: string | undefined) => {
|
|
1689
|
+
const onDeclare: MockFunction = getJestMockFunction();
|
|
1690
|
+
|
|
1691
|
+
renderPanel({
|
|
1692
|
+
title: title,
|
|
1693
|
+
secondaryActions: [
|
|
1694
|
+
makeSecondaryAction({ onClick: onDeclare }),
|
|
1695
|
+
makeSecondaryAction({
|
|
1696
|
+
id: "locked-secondary-btn",
|
|
1697
|
+
label: "Locked",
|
|
1698
|
+
isDisabled: true,
|
|
1699
|
+
tooltip: LOCKED_REASON,
|
|
1700
|
+
}),
|
|
1701
|
+
],
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
if (title) {
|
|
1705
|
+
expect(
|
|
1706
|
+
screen.getByRole("heading", { level: 2, name: title }),
|
|
1707
|
+
).toBeInTheDocument();
|
|
1708
|
+
} else {
|
|
1709
|
+
expect(
|
|
1710
|
+
screen.queryByRole("heading", { level: 2 }),
|
|
1711
|
+
).not.toBeInTheDocument();
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const group: HTMLElement = getActionGroup();
|
|
1715
|
+
const declare: HTMLElement = getButton(DECLARE_LABEL);
|
|
1716
|
+
const locked: HTMLElement = getButton("Locked");
|
|
1717
|
+
|
|
1718
|
+
expect(within(group).getAllByRole("button")).toEqual([
|
|
1719
|
+
getButton("Acknowledge"),
|
|
1720
|
+
getButton("Resolve"),
|
|
1721
|
+
declare,
|
|
1722
|
+
locked,
|
|
1723
|
+
getMoreActionsTrigger(),
|
|
1724
|
+
]);
|
|
1725
|
+
expect(
|
|
1726
|
+
within(group).getByTestId("locked-secondary-btn-disabled-wrapper"),
|
|
1727
|
+
).toContainElement(locked);
|
|
1728
|
+
|
|
1729
|
+
fireEvent.click(declare);
|
|
1730
|
+
|
|
1731
|
+
expect(onDeclare).toHaveBeenCalledTimes(1);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
test("keeps several secondary actions in caller order, each with its own handler", () => {
|
|
1735
|
+
const onFirst: MockFunction = getJestMockFunction();
|
|
1736
|
+
const onSecond: MockFunction = getJestMockFunction();
|
|
1737
|
+
const onThird: MockFunction = getJestMockFunction();
|
|
1738
|
+
|
|
1739
|
+
const rendered: RenderedPanel = renderPanel({
|
|
1740
|
+
secondaryActions: [
|
|
1741
|
+
makeSecondaryAction({
|
|
1742
|
+
id: "first-secondary-btn",
|
|
1743
|
+
label: "First",
|
|
1744
|
+
onClick: onFirst,
|
|
1745
|
+
}),
|
|
1746
|
+
makeSecondaryAction({
|
|
1747
|
+
id: "second-secondary-btn",
|
|
1748
|
+
label: "Second",
|
|
1749
|
+
onClick: onSecond,
|
|
1750
|
+
isDisabled: true,
|
|
1751
|
+
tooltip: LOCKED_REASON,
|
|
1752
|
+
}),
|
|
1753
|
+
makeSecondaryAction({
|
|
1754
|
+
id: "third-secondary-btn",
|
|
1755
|
+
label: "Third",
|
|
1756
|
+
onClick: onThird,
|
|
1757
|
+
}),
|
|
1758
|
+
],
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
const group: HTMLElement = getActionGroup();
|
|
1762
|
+
const first: HTMLElement = getButton("First");
|
|
1763
|
+
const second: HTMLElement = getButton("Second");
|
|
1764
|
+
const third: HTMLElement = getButton("Third");
|
|
1765
|
+
|
|
1766
|
+
expect(within(group).getAllByRole("button")).toEqual([
|
|
1767
|
+
getButton("Acknowledge"),
|
|
1768
|
+
getButton("Resolve"),
|
|
1769
|
+
first,
|
|
1770
|
+
second,
|
|
1771
|
+
third,
|
|
1772
|
+
getMoreActionsTrigger(),
|
|
1773
|
+
]);
|
|
1774
|
+
expect(first).toHaveAttribute("id", "first-secondary-btn");
|
|
1775
|
+
expect(second).toHaveAttribute("id", "second-secondary-btn");
|
|
1776
|
+
expect(third).toHaveAttribute("id", "third-secondary-btn");
|
|
1777
|
+
|
|
1778
|
+
fireEvent.click(third);
|
|
1779
|
+
fireEvent.click(second);
|
|
1780
|
+
fireEvent.click(first);
|
|
1781
|
+
|
|
1782
|
+
expect(onFirst).toHaveBeenCalledTimes(1);
|
|
1783
|
+
expect(onSecond).not.toHaveBeenCalled();
|
|
1784
|
+
expect(onThird).toHaveBeenCalledTimes(1);
|
|
1785
|
+
expect(onThird.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1786
|
+
onFirst.mock.invocationCallOrder[0]!,
|
|
1787
|
+
);
|
|
1788
|
+
expect(rendered.actionClicks).toEqual([]);
|
|
1789
|
+
});
|
|
1790
|
+
});
|