@oneuptime/common 14.0.2 → 14.0.3
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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
within,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import {
|
|
15
|
+
afterEach,
|
|
16
|
+
beforeEach,
|
|
17
|
+
describe,
|
|
18
|
+
expect,
|
|
19
|
+
jest,
|
|
20
|
+
test,
|
|
21
|
+
} from "@jest/globals";
|
|
22
|
+
import Table from "../../../UI/Components/Table/Table";
|
|
23
|
+
import TableHeader from "../../../UI/Components/Table/TableHeader";
|
|
24
|
+
import Column from "../../../UI/Components/Table/Types/Column";
|
|
25
|
+
import Columns from "../../../UI/Components/Table/Types/Columns";
|
|
26
|
+
import FieldType from "../../../UI/Components/Types/FieldType";
|
|
27
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
28
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Column.headerTooltip: an (i) beside a table header that says what the
|
|
32
|
+
* column's values mean - "CPU" summed over pods, "Restarts" since when.
|
|
33
|
+
*
|
|
34
|
+
* A sortable header is a <button>, and a button may not contain another
|
|
35
|
+
* button, so for a sortable column with a tooltip the sort button shrinks
|
|
36
|
+
* to its content and the (i) follows it inside the same cell. A column
|
|
37
|
+
* without a tooltip must render exactly as it did before. And
|
|
38
|
+
* Column.description - which ModelTable fills from the database column for
|
|
39
|
+
* other purposes - must never produce an (i).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
jest.mock("react-i18next", () => {
|
|
43
|
+
return {
|
|
44
|
+
useTranslation: () => {
|
|
45
|
+
return {
|
|
46
|
+
t: (key: string, opts?: { defaultValue?: string }): string => {
|
|
47
|
+
return opts?.defaultValue ?? key;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
interface Row {
|
|
55
|
+
_id?: string | undefined;
|
|
56
|
+
name?: string | undefined;
|
|
57
|
+
cpu?: number | undefined;
|
|
58
|
+
restarts?: number | undefined;
|
|
59
|
+
notes?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CPU_TIP: string =
|
|
63
|
+
"Processor time used by all pods of the workload, summed - 100% is one full CPU core, so it can exceed 100%.";
|
|
64
|
+
const RESTARTS_TIP: string =
|
|
65
|
+
"How many times the containers were restarted since they were created.";
|
|
66
|
+
|
|
67
|
+
type OnSort = (sortBy: keyof Row | null, sortOrder: SortOrder) => void;
|
|
68
|
+
|
|
69
|
+
interface HeaderOptions {
|
|
70
|
+
columns: Columns<Row>;
|
|
71
|
+
sortBy?: keyof Row | null | undefined;
|
|
72
|
+
sortOrder?: SortOrder | undefined;
|
|
73
|
+
onSortChanged?: OnSort | undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderHeader(options: HeaderOptions): void {
|
|
77
|
+
render(
|
|
78
|
+
<table>
|
|
79
|
+
<TableHeader<Row>
|
|
80
|
+
id="test-header"
|
|
81
|
+
columns={options.columns}
|
|
82
|
+
onSortChanged={options.onSortChanged ?? (() => {})}
|
|
83
|
+
isBulkActionsEnabled={false}
|
|
84
|
+
onAllItemsOnThePageSelected={undefined}
|
|
85
|
+
onAllItemsDeselected={undefined}
|
|
86
|
+
hasTableItems={true}
|
|
87
|
+
isAllItemsOnThePageSelected={false}
|
|
88
|
+
sortBy={options.sortBy ?? null}
|
|
89
|
+
sortOrder={options.sortOrder ?? SortOrder.Ascending}
|
|
90
|
+
/>
|
|
91
|
+
</table>,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function headerCell(title: string): HTMLElement {
|
|
96
|
+
const cells: Array<HTMLElement> = screen.getAllByRole("columnheader");
|
|
97
|
+
const cell: HTMLElement | undefined = cells.find(
|
|
98
|
+
(c: HTMLElement): boolean => {
|
|
99
|
+
return (c.textContent || "").trim() === title;
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!cell) {
|
|
104
|
+
throw new Error(`No header cell "${title}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return cell;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
111
|
+
fireEvent.mouseEnter(trigger);
|
|
112
|
+
await act(async () => {
|
|
113
|
+
jest.advanceTimersByTime(200);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const setViewportWidth: (width: number) => void = (width: number): void => {
|
|
118
|
+
Object.defineProperty(window, "innerWidth", {
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true,
|
|
121
|
+
value: width,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
jest.useFakeTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
cleanup();
|
|
131
|
+
jest.useRealTimers();
|
|
132
|
+
setViewportWidth(1024);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("a sortable column with a header tooltip", () => {
|
|
136
|
+
const columns: Columns<Row> = [
|
|
137
|
+
{ title: "Name", type: FieldType.Text, key: "name" },
|
|
138
|
+
{
|
|
139
|
+
title: "CPU",
|
|
140
|
+
type: FieldType.Number,
|
|
141
|
+
key: "cpu",
|
|
142
|
+
headerTooltip: CPU_TIP,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
test("the (i) sits in the header cell, beside the sort button and not inside it", () => {
|
|
147
|
+
renderHeader({ columns });
|
|
148
|
+
|
|
149
|
+
const cell: HTMLElement = headerCell("CPU");
|
|
150
|
+
const sortButton: HTMLElement = within(cell).getByRole("button", {
|
|
151
|
+
name: "CPU",
|
|
152
|
+
});
|
|
153
|
+
const info: HTMLElement = within(cell).getByRole("button", {
|
|
154
|
+
name: "About CPU",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(sortButton).not.toContainElement(info);
|
|
158
|
+
expect(info.parentElement?.closest("button")).toBeNull();
|
|
159
|
+
expect(sortButton.nextElementSibling).toBe(info);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the cell still announces itself as sortable", () => {
|
|
163
|
+
renderHeader({ columns });
|
|
164
|
+
|
|
165
|
+
expect(headerCell("CPU")).toHaveAttribute("aria-sort", "none");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("clicking the sort button still sorts", () => {
|
|
169
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
170
|
+
renderHeader({ columns, onSortChanged });
|
|
171
|
+
|
|
172
|
+
fireEvent.click(screen.getByRole("button", { name: "CPU" }));
|
|
173
|
+
|
|
174
|
+
expect(onSortChanged).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(onSortChanged).toHaveBeenCalledWith("cpu", SortOrder.Descending);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("the sort direction round-trips on a tooltip column", () => {
|
|
179
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
180
|
+
renderHeader({
|
|
181
|
+
columns,
|
|
182
|
+
onSortChanged,
|
|
183
|
+
sortBy: "cpu",
|
|
184
|
+
sortOrder: SortOrder.Descending,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(headerCell("CPU")).toHaveAttribute("aria-sort", "descending");
|
|
188
|
+
|
|
189
|
+
fireEvent.click(screen.getByRole("button", { name: "CPU" }));
|
|
190
|
+
|
|
191
|
+
expect(onSortChanged).toHaveBeenCalledWith("cpu", SortOrder.Ascending);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("the sort chevron stays inside the sort button, before the (i)", () => {
|
|
195
|
+
renderHeader({ columns, sortBy: "cpu", sortOrder: SortOrder.Ascending });
|
|
196
|
+
|
|
197
|
+
const cell: HTMLElement = headerCell("CPU");
|
|
198
|
+
const sortButton: HTMLElement = within(cell).getByRole("button", {
|
|
199
|
+
name: "CPU",
|
|
200
|
+
});
|
|
201
|
+
const info: HTMLElement = within(cell).getByRole("button", {
|
|
202
|
+
name: "About CPU",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(sortButton.querySelector("svg")).not.toBeNull();
|
|
206
|
+
expect(info.compareDocumentPosition(sortButton)).toBe(
|
|
207
|
+
Node.DOCUMENT_POSITION_PRECEDING,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("clicking the (i) does not sort", () => {
|
|
212
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
213
|
+
renderHeader({ columns, onSortChanged });
|
|
214
|
+
|
|
215
|
+
fireEvent.click(screen.getByRole("button", { name: "About CPU" }));
|
|
216
|
+
|
|
217
|
+
expect(onSortChanged).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test.each([
|
|
221
|
+
["Enter", "Enter"],
|
|
222
|
+
["Space", " "],
|
|
223
|
+
])("pressing %s on the (i) does not sort", (_: string, key: string) => {
|
|
224
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
225
|
+
renderHeader({ columns, onSortChanged });
|
|
226
|
+
|
|
227
|
+
fireEvent.keyDown(screen.getByRole("button", { name: "About CPU" }), {
|
|
228
|
+
key,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(onSortChanged).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a real keyboard Enter on the focused (i) does not sort, and on the sort button does", async () => {
|
|
235
|
+
const user: ReturnType<typeof userEvent.setup> = userEvent.setup({
|
|
236
|
+
advanceTimers: (ms: number) => {
|
|
237
|
+
jest.advanceTimersByTime(ms);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
241
|
+
renderHeader({ columns, onSortChanged });
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
screen.getByRole("button", { name: "About CPU" }).focus();
|
|
245
|
+
});
|
|
246
|
+
await user.keyboard("{Enter}");
|
|
247
|
+
|
|
248
|
+
expect(onSortChanged).not.toHaveBeenCalled();
|
|
249
|
+
|
|
250
|
+
act(() => {
|
|
251
|
+
screen.getByRole("button", { name: "CPU" }).focus();
|
|
252
|
+
});
|
|
253
|
+
await user.keyboard("{Enter}");
|
|
254
|
+
|
|
255
|
+
expect(onSortChanged).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("hovering the (i) shows what the column means", async () => {
|
|
259
|
+
renderHeader({ columns });
|
|
260
|
+
|
|
261
|
+
await hover(screen.getByRole("button", { name: "About CPU" }));
|
|
262
|
+
|
|
263
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(CPU_TIP);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("the sort button shrinks to its content and the pair shares the cell", () => {
|
|
267
|
+
renderHeader({ columns });
|
|
268
|
+
|
|
269
|
+
const cell: HTMLElement = headerCell("CPU");
|
|
270
|
+
const sortButton: HTMLElement = within(cell).getByRole("button", {
|
|
271
|
+
name: "CPU",
|
|
272
|
+
});
|
|
273
|
+
const wrapper: HTMLElement = sortButton.parentElement as HTMLElement;
|
|
274
|
+
|
|
275
|
+
expect(wrapper.parentElement).toBe(cell);
|
|
276
|
+
expect(wrapper).toHaveClass("flex", "w-full", "items-center");
|
|
277
|
+
expect(sortButton).toHaveClass("pl-6", "pr-1", "py-3");
|
|
278
|
+
expect(sortButton).not.toHaveClass("w-full");
|
|
279
|
+
expect(sortButton).not.toHaveClass("px-6");
|
|
280
|
+
expect(sortButton).toHaveAttribute("type", "button");
|
|
281
|
+
expect(within(cell).getByRole("button", { name: "About CPU" })).toHaveClass(
|
|
282
|
+
"mr-6",
|
|
283
|
+
"font-normal",
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("a sibling column without a tooltip is untouched", () => {
|
|
288
|
+
renderHeader({ columns });
|
|
289
|
+
|
|
290
|
+
const cell: HTMLElement = headerCell("Name");
|
|
291
|
+
|
|
292
|
+
expect(within(cell).getAllByRole("button")).toHaveLength(1);
|
|
293
|
+
expect(within(cell).getByRole("button", { name: "Name" })).toHaveClass(
|
|
294
|
+
"w-full",
|
|
295
|
+
"px-6",
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("a non-sortable column with a header tooltip", () => {
|
|
301
|
+
const NON_SORTABLE: Array<[string, Column<Row>]> = [
|
|
302
|
+
[
|
|
303
|
+
"disableSort",
|
|
304
|
+
{
|
|
305
|
+
title: "Restarts",
|
|
306
|
+
type: FieldType.Number,
|
|
307
|
+
key: "restarts",
|
|
308
|
+
disableSort: true,
|
|
309
|
+
headerTooltip: RESTARTS_TIP,
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
[
|
|
313
|
+
"no key",
|
|
314
|
+
{
|
|
315
|
+
title: "Restarts",
|
|
316
|
+
type: FieldType.Number,
|
|
317
|
+
headerTooltip: RESTARTS_TIP,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
test.each(NON_SORTABLE)(
|
|
323
|
+
"(%s) shows the (i) beside a plain title",
|
|
324
|
+
async (_: string, column: Column<Row>) => {
|
|
325
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
326
|
+
renderHeader({ columns: [column], onSortChanged });
|
|
327
|
+
|
|
328
|
+
const cell: HTMLElement = headerCell("Restarts");
|
|
329
|
+
const buttons: Array<HTMLElement> = within(cell).getAllByRole("button");
|
|
330
|
+
|
|
331
|
+
expect(buttons).toHaveLength(1);
|
|
332
|
+
expect(buttons[0]).toHaveAccessibleName("About Restarts");
|
|
333
|
+
expect(cell).not.toHaveAttribute("aria-sort");
|
|
334
|
+
|
|
335
|
+
const content: HTMLElement = buttons[0]!.parentElement as HTMLElement;
|
|
336
|
+
expect(content.className).toBe(
|
|
337
|
+
"flex w-full px-6 py-3 justify-start items-center",
|
|
338
|
+
);
|
|
339
|
+
expect(buttons[0]).toHaveClass("ml-1", "font-normal");
|
|
340
|
+
|
|
341
|
+
fireEvent.click(buttons[0]!);
|
|
342
|
+
expect(onSortChanged).not.toHaveBeenCalled();
|
|
343
|
+
|
|
344
|
+
await hover(buttons[0]!);
|
|
345
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(RESTARTS_TIP);
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
test("an actions-type column keeps its right alignment", () => {
|
|
350
|
+
renderHeader({
|
|
351
|
+
columns: [
|
|
352
|
+
{
|
|
353
|
+
title: "Actions",
|
|
354
|
+
type: FieldType.Actions,
|
|
355
|
+
headerTooltip: "What you can do with each row.",
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
361
|
+
name: "About Actions",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(info.parentElement?.className).toBe(
|
|
365
|
+
"flex w-full px-6 py-3 justify-end items-center",
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("the column header is named by its title, not by its (i)", () => {
|
|
371
|
+
const columns: Columns<Row> = [
|
|
372
|
+
{ title: "Name", type: FieldType.Text, key: "name" },
|
|
373
|
+
{
|
|
374
|
+
title: "CPU",
|
|
375
|
+
type: FieldType.Number,
|
|
376
|
+
key: "cpu",
|
|
377
|
+
headerTooltip: CPU_TIP,
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
title: "Restarts",
|
|
381
|
+
type: FieldType.Number,
|
|
382
|
+
key: "restarts",
|
|
383
|
+
disableSort: true,
|
|
384
|
+
headerTooltip: RESTARTS_TIP,
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
test.each(["CPU", "Restarts"])(
|
|
389
|
+
"a %s header with an (i) is announced as its title alone",
|
|
390
|
+
(title: string) => {
|
|
391
|
+
renderHeader({ columns });
|
|
392
|
+
|
|
393
|
+
const cell: HTMLElement = screen.getByRole("columnheader", {
|
|
394
|
+
name: title,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(cell).toBe(headerCell(title));
|
|
398
|
+
expect(
|
|
399
|
+
within(cell).getByRole("button", { name: `About ${title}` }),
|
|
400
|
+
).toBeInTheDocument();
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
test("a header without an (i) keeps the name its content gives it", () => {
|
|
405
|
+
renderHeader({ columns });
|
|
406
|
+
|
|
407
|
+
const cell: HTMLElement = headerCell("Name");
|
|
408
|
+
|
|
409
|
+
expect(cell).not.toHaveAttribute("aria-label");
|
|
410
|
+
expect(cell).toHaveAccessibleName("Name");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("columns without a header tooltip render exactly as before", () => {
|
|
415
|
+
const columns: Columns<Row> = [
|
|
416
|
+
{ title: "Name", type: FieldType.Text, key: "name" },
|
|
417
|
+
{ title: "Notes", type: FieldType.Text },
|
|
418
|
+
{ title: "Actions", type: FieldType.Actions },
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
test("no (i) anywhere", () => {
|
|
422
|
+
renderHeader({ columns });
|
|
423
|
+
|
|
424
|
+
expect(
|
|
425
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
426
|
+
).not.toBeInTheDocument();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("a sortable header is one full-width button, directly in the cell", () => {
|
|
430
|
+
renderHeader({ columns });
|
|
431
|
+
|
|
432
|
+
const cell: HTMLElement = headerCell("Name");
|
|
433
|
+
const button: HTMLElement = within(cell).getByRole("button", {
|
|
434
|
+
name: "Name",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(button.parentElement).toBe(cell);
|
|
438
|
+
expect(button.className).toContain("flex w-full px-6 py-3 justify-start");
|
|
439
|
+
expect(button).not.toHaveClass("pl-6");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("a non-sortable header is the plain content div, without the tooltip alignment", () => {
|
|
443
|
+
renderHeader({ columns });
|
|
444
|
+
|
|
445
|
+
const notes: HTMLElement = headerCell("Notes");
|
|
446
|
+
const actions: HTMLElement = headerCell("Actions");
|
|
447
|
+
|
|
448
|
+
expect(within(notes).queryByRole("button")).not.toBeInTheDocument();
|
|
449
|
+
expect((notes.firstElementChild as HTMLElement).className).toBe(
|
|
450
|
+
"flex w-full px-6 py-3 justify-start",
|
|
451
|
+
);
|
|
452
|
+
expect((actions.firstElementChild as HTMLElement).className).toBe(
|
|
453
|
+
"flex w-full px-6 py-3 justify-end",
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test.each([
|
|
458
|
+
["empty", ""],
|
|
459
|
+
["undefined", undefined],
|
|
460
|
+
])(
|
|
461
|
+
"an %s headerTooltip on a sortable column keeps the full-width sort button",
|
|
462
|
+
(_: string, headerTooltip: string | undefined) => {
|
|
463
|
+
renderHeader({
|
|
464
|
+
columns: [
|
|
465
|
+
{ title: "Name", type: FieldType.Text, key: "name", headerTooltip },
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const button: HTMLElement = screen.getByRole("button", { name: "Name" });
|
|
470
|
+
|
|
471
|
+
expect(button.className).toContain("w-full px-6");
|
|
472
|
+
expect(screen.getAllByRole("button")).toHaveLength(1);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe("Column.description is never an (i)", () => {
|
|
478
|
+
test("a description alone draws nothing, on sortable and plain columns", () => {
|
|
479
|
+
renderHeader({
|
|
480
|
+
columns: [
|
|
481
|
+
{
|
|
482
|
+
title: "Name",
|
|
483
|
+
type: FieldType.Text,
|
|
484
|
+
key: "name",
|
|
485
|
+
description: "The monitor name as stored in the database.",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
title: "Notes",
|
|
489
|
+
type: FieldType.Text,
|
|
490
|
+
description: "Free text notes.",
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(
|
|
496
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
497
|
+
).not.toBeInTheDocument();
|
|
498
|
+
expect(
|
|
499
|
+
screen.queryByText("The monitor name as stored in the database."),
|
|
500
|
+
).not.toBeInTheDocument();
|
|
501
|
+
expect(screen.queryByText("Free text notes.")).not.toBeInTheDocument();
|
|
502
|
+
// The sortable one keeps its original full-width button.
|
|
503
|
+
expect(screen.getByRole("button", { name: "Name" })).toHaveClass("w-full");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("with both, the (i) shows the headerTooltip and never the description", async () => {
|
|
507
|
+
renderHeader({
|
|
508
|
+
columns: [
|
|
509
|
+
{
|
|
510
|
+
title: "CPU",
|
|
511
|
+
type: FieldType.Number,
|
|
512
|
+
key: "cpu",
|
|
513
|
+
description: "Database column description.",
|
|
514
|
+
headerTooltip: CPU_TIP,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await hover(screen.getByRole("button", { name: "About CPU" }));
|
|
520
|
+
|
|
521
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(CPU_TIP);
|
|
522
|
+
expect(
|
|
523
|
+
screen.queryByText("Database column description."),
|
|
524
|
+
).not.toBeInTheDocument();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe("mobile", () => {
|
|
529
|
+
test("a column hidden on mobile takes its (i) with it", () => {
|
|
530
|
+
setViewportWidth(500);
|
|
531
|
+
|
|
532
|
+
renderHeader({
|
|
533
|
+
columns: [
|
|
534
|
+
{ title: "Name", type: FieldType.Text, key: "name" },
|
|
535
|
+
{
|
|
536
|
+
title: "CPU",
|
|
537
|
+
type: FieldType.Number,
|
|
538
|
+
key: "cpu",
|
|
539
|
+
headerTooltip: CPU_TIP,
|
|
540
|
+
hideOnMobile: true,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(
|
|
546
|
+
screen.queryByRole("button", { name: "About CPU" }),
|
|
547
|
+
).not.toBeInTheDocument();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe("through the full Table", () => {
|
|
552
|
+
const data: Array<Row> = [
|
|
553
|
+
{ _id: "1", name: "api", cpu: 12, restarts: 0 },
|
|
554
|
+
{ _id: "2", name: "web", cpu: 140, restarts: 3 },
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
function renderTable(onSortChanged: OnSort): void {
|
|
558
|
+
render(
|
|
559
|
+
<Table<Row>
|
|
560
|
+
id="tooltip-table"
|
|
561
|
+
data={data}
|
|
562
|
+
columns={[
|
|
563
|
+
{ title: "Name", type: FieldType.Text, key: "name" },
|
|
564
|
+
{
|
|
565
|
+
title: "CPU",
|
|
566
|
+
type: FieldType.Number,
|
|
567
|
+
key: "cpu",
|
|
568
|
+
headerTooltip: CPU_TIP,
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
title: "Restarts",
|
|
572
|
+
type: FieldType.Number,
|
|
573
|
+
key: "restarts",
|
|
574
|
+
disableSort: true,
|
|
575
|
+
headerTooltip: RESTARTS_TIP,
|
|
576
|
+
},
|
|
577
|
+
]}
|
|
578
|
+
currentPageNumber={1}
|
|
579
|
+
totalItemsCount={data.length}
|
|
580
|
+
itemsOnPage={10}
|
|
581
|
+
error=""
|
|
582
|
+
isLoading={false}
|
|
583
|
+
singularLabel="Workload"
|
|
584
|
+
pluralLabel="Workloads"
|
|
585
|
+
sortOrder={SortOrder.Ascending}
|
|
586
|
+
sortBy={null}
|
|
587
|
+
onSortChanged={onSortChanged}
|
|
588
|
+
onNavigateToPage={() => {}}
|
|
589
|
+
/>,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
test("every tooltip column gets one (i) in the header, and sorting still works", async () => {
|
|
594
|
+
const onSortChanged: MockFunction = getJestMockFunction();
|
|
595
|
+
renderTable(onSortChanged);
|
|
596
|
+
|
|
597
|
+
const infos: Array<HTMLElement> = screen.getAllByRole("button", {
|
|
598
|
+
name: /^About /,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(
|
|
602
|
+
infos.map((b: HTMLElement): string => {
|
|
603
|
+
return b.getAttribute("aria-label") || "";
|
|
604
|
+
}),
|
|
605
|
+
).toEqual(["About CPU", "About Restarts"]);
|
|
606
|
+
|
|
607
|
+
for (const info of infos) {
|
|
608
|
+
expect(info.closest("thead")).not.toBeNull();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fireEvent.click(infos[0]!);
|
|
612
|
+
expect(onSortChanged).not.toHaveBeenCalled();
|
|
613
|
+
|
|
614
|
+
fireEvent.click(screen.getByRole("button", { name: "CPU" }));
|
|
615
|
+
expect(onSortChanged).toHaveBeenCalledWith("cpu", SortOrder.Descending);
|
|
616
|
+
|
|
617
|
+
await hover(infos[1]!);
|
|
618
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(RESTARTS_TIP);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("the rows are unaffected by header tooltips", () => {
|
|
622
|
+
renderTable(() => {});
|
|
623
|
+
|
|
624
|
+
expect(screen.getAllByText("api").length).toBeGreaterThan(0);
|
|
625
|
+
expect(screen.getAllByText("140").length).toBeGreaterThan(0);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe("ModelTable hands headerTooltip through", () => {
|
|
630
|
+
/*
|
|
631
|
+
* ModelTable builds the Table's columns by spreading its own; the field
|
|
632
|
+
* exists on both Column types so a page can set it once on a ModelTable
|
|
633
|
+
* column. Pinned at the source: rendering a ModelTable needs the whole
|
|
634
|
+
* API layer.
|
|
635
|
+
*/
|
|
636
|
+
const COMMON_UI: string = path.join(__dirname, "..", "..", "..", "UI");
|
|
637
|
+
|
|
638
|
+
test("both Column types declare headerTooltip", () => {
|
|
639
|
+
for (const file of [
|
|
640
|
+
"Components/Table/Types/Column.ts",
|
|
641
|
+
"Components/ModelTable/Column.ts",
|
|
642
|
+
]) {
|
|
643
|
+
const source: string = fs.readFileSync(
|
|
644
|
+
path.join(COMMON_UI, ...file.split("/")),
|
|
645
|
+
"utf8",
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
expect(source).toContain("headerTooltip?: string | undefined;");
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("BaseModelTable spreads the column into the table column", () => {
|
|
653
|
+
const source: string = fs.readFileSync(
|
|
654
|
+
path.join(COMMON_UI, "Components", "ModelTable", "BaseModelTable.tsx"),
|
|
655
|
+
"utf8",
|
|
656
|
+
);
|
|
657
|
+
const push: number = source.indexOf("columns.push({");
|
|
658
|
+
|
|
659
|
+
expect(push).toBeGreaterThan(-1);
|
|
660
|
+
expect(source.slice(push, push + 80)).toContain("...column,");
|
|
661
|
+
});
|
|
662
|
+
});
|