@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,1066 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
screen,
|
|
16
|
+
within,
|
|
17
|
+
} from "@testing-library/react";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import { MemoryRouter, useLocation } from "react-router-dom";
|
|
20
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* The shared dashboard pieces that carry a metric's (i) tooltip:
|
|
24
|
+
*
|
|
25
|
+
* - ResourceOverview's golden-metric tiles (every telemetry overview page),
|
|
26
|
+
* including a linked tile, where the link is laid over the card so the
|
|
27
|
+
* (i) is never inside an anchor;
|
|
28
|
+
* - ChartCard, in each of its three states;
|
|
29
|
+
* - the Infrastructure GoldenMetricTile (Kubernetes, Proxmox, Ceph...);
|
|
30
|
+
* - WebVitalsCard, for the card and for each vital, whose tooltip adds the
|
|
31
|
+
* Good / Poor limits the rating chip uses;
|
|
32
|
+
* - ResourceOverviewTab's summary fields;
|
|
33
|
+
* - ResourceTable's header tooltips, and the Kubernetes wrapper that passes
|
|
34
|
+
* them through.
|
|
35
|
+
*
|
|
36
|
+
* Tippy under jsdom (see InfoTooltip.test.tsx): the popup is portalled to
|
|
37
|
+
* document.body and never finishes animating, so a shown tooltip is found
|
|
38
|
+
* through `screen` and its presence is asserted.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const lineChartMock: MockFunction = getJestMockFunction();
|
|
42
|
+
|
|
43
|
+
jest.mock("react-i18next", () => {
|
|
44
|
+
return {
|
|
45
|
+
useTranslation: () => {
|
|
46
|
+
return {
|
|
47
|
+
t: (key: string, opts?: { defaultValue?: string }): string => {
|
|
48
|
+
return opts?.defaultValue ?? key;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// recharts draws nothing in a 0x0 jsdom box; the card is checked by what it hands the chart.
|
|
56
|
+
jest.mock("../../../UI/Components/Charts/Line/LineChart", () => {
|
|
57
|
+
return {
|
|
58
|
+
__esModule: true,
|
|
59
|
+
default: (props: { data: Array<unknown> }) => {
|
|
60
|
+
lineChartMock(props);
|
|
61
|
+
return (
|
|
62
|
+
<div data-testid="line-chart">{`series:${props.data.length}`}</div>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
import ResourceOverview, {
|
|
69
|
+
ResourceOverviewTile,
|
|
70
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/ResourceOverview";
|
|
71
|
+
import ChartCard from "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/ChartCard";
|
|
72
|
+
import WebVitalsCard, {
|
|
73
|
+
describeWebVital,
|
|
74
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/WebVitalsCard";
|
|
75
|
+
import { WebVital } from "../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/telemetryMetrics";
|
|
76
|
+
import InfraGoldenMetricTile from "../../../../App/FeatureSet/Dashboard/src/Components/Infrastructure/GoldenMetricTile";
|
|
77
|
+
import ResourceOverviewTab from "../../../../App/FeatureSet/Dashboard/src/Components/Infrastructure/ResourceOverviewTab";
|
|
78
|
+
import ResourceTable, {
|
|
79
|
+
InfrastructureResource,
|
|
80
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Infrastructure/ResourceTable";
|
|
81
|
+
import KubernetesResourceTable from "../../../../App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable";
|
|
82
|
+
import Route from "../../../Types/API/Route";
|
|
83
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
84
|
+
import SeriesPoint from "../../../UI/Components/Charts/Types/SeriesPoints";
|
|
85
|
+
import {
|
|
86
|
+
describeWebVitalThresholds,
|
|
87
|
+
WebVitalDefinition,
|
|
88
|
+
WebVitalDefinitions,
|
|
89
|
+
} from "../../../Types/Rum/WebVitals";
|
|
90
|
+
|
|
91
|
+
const P95_TEXT: string =
|
|
92
|
+
"p95: 95% of requests finished faster than this, and the slowest 5% took longer.";
|
|
93
|
+
const ERROR_TEXT: string =
|
|
94
|
+
"The share of requests in the selected range that failed.";
|
|
95
|
+
const CLIENTS_TEXT: string =
|
|
96
|
+
"Distinct platforms this app has been used on so far.";
|
|
97
|
+
|
|
98
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
99
|
+
fireEvent.mouseEnter(trigger);
|
|
100
|
+
await act(async () => {
|
|
101
|
+
jest.advanceTimersByTime(200);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function expectTooltip(
|
|
106
|
+
trigger: HTMLElement,
|
|
107
|
+
text: string,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
await hover(trigger);
|
|
110
|
+
|
|
111
|
+
const describedBy: string | null = trigger.getAttribute("aria-describedby");
|
|
112
|
+
|
|
113
|
+
expect(describedBy).toBeTruthy();
|
|
114
|
+
expect(document.getElementById(describedBy as string)).toHaveTextContent(
|
|
115
|
+
text,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
jest.useFakeTimers();
|
|
121
|
+
lineChartMock.mockReset();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
cleanup();
|
|
126
|
+
jest.useRealTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/* ------------------------------------------------------------------ */
|
|
130
|
+
/* ResourceOverview golden-metric tiles */
|
|
131
|
+
/* ------------------------------------------------------------------ */
|
|
132
|
+
|
|
133
|
+
function LocationProbe(): React.ReactElement {
|
|
134
|
+
const location: { pathname: string } = useLocation();
|
|
135
|
+
return <div data-testid="location">{location.pathname}</div>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderOverview(
|
|
139
|
+
tiles: Array<ResourceOverviewTile>,
|
|
140
|
+
options: { tilesLoading?: boolean } = {},
|
|
141
|
+
): void {
|
|
142
|
+
render(
|
|
143
|
+
<MemoryRouter initialEntries={["/dashboard/overview"]}>
|
|
144
|
+
<ResourceOverview
|
|
145
|
+
icon={IconProp.Globe}
|
|
146
|
+
title="Checkout Web"
|
|
147
|
+
identifier="checkout-web"
|
|
148
|
+
identifierLabel="service.name"
|
|
149
|
+
status="connected"
|
|
150
|
+
lastSeenAt={undefined}
|
|
151
|
+
chips={[]}
|
|
152
|
+
tiles={tiles}
|
|
153
|
+
tilesLoading={options.tilesLoading}
|
|
154
|
+
detailRows={[]}
|
|
155
|
+
/>
|
|
156
|
+
<LocationProbe />
|
|
157
|
+
</MemoryRouter>,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function tileRoot(title: string): HTMLElement {
|
|
162
|
+
const titleSpan: HTMLElement = screen.getByText(title, {
|
|
163
|
+
selector: "span.uppercase",
|
|
164
|
+
});
|
|
165
|
+
return titleSpan.closest("[aria-busy]") as HTMLElement;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const P95_TILE: ResourceOverviewTile = {
|
|
169
|
+
title: "p95 duration",
|
|
170
|
+
value: "123 ms",
|
|
171
|
+
icon: IconProp.Clock,
|
|
172
|
+
iconColor: "violet",
|
|
173
|
+
sublabel: "selected range",
|
|
174
|
+
description: P95_TEXT,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
describe("ResourceOverview tiles", () => {
|
|
178
|
+
test("a tile with a description has an (i) whose tooltip is the description", async () => {
|
|
179
|
+
renderOverview([P95_TILE]);
|
|
180
|
+
|
|
181
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
182
|
+
name: "About p95 duration",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(tileRoot("p95 duration")).toContainElement(info);
|
|
186
|
+
await expectTooltip(info, P95_TEXT);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("the value and sublabel still render beside the (i)", () => {
|
|
190
|
+
renderOverview([P95_TILE]);
|
|
191
|
+
|
|
192
|
+
const root: HTMLElement = tileRoot("p95 duration");
|
|
193
|
+
|
|
194
|
+
expect(within(root).getByText("123 ms")).toBeInTheDocument();
|
|
195
|
+
expect(within(root).getByText("selected range")).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a tile without a description has no (i)", () => {
|
|
199
|
+
renderOverview([{ ...P95_TILE, description: undefined }]);
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
203
|
+
).not.toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a whitespace description has no (i)", () => {
|
|
207
|
+
renderOverview([{ ...P95_TILE, description: " " }]);
|
|
208
|
+
|
|
209
|
+
expect(
|
|
210
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
211
|
+
).not.toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("each tile explains itself with its own text", async () => {
|
|
215
|
+
renderOverview([
|
|
216
|
+
P95_TILE,
|
|
217
|
+
{
|
|
218
|
+
title: "Error rate",
|
|
219
|
+
value: "2.0%",
|
|
220
|
+
icon: IconProp.Alert,
|
|
221
|
+
iconColor: "rose",
|
|
222
|
+
percent: 2,
|
|
223
|
+
thresholds: { warn: 1, danger: 5 },
|
|
224
|
+
description: ERROR_TEXT,
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
await expectTooltip(
|
|
229
|
+
screen.getByRole("button", { name: "About Error rate" }),
|
|
230
|
+
ERROR_TEXT,
|
|
231
|
+
);
|
|
232
|
+
await expectTooltip(
|
|
233
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
234
|
+
P95_TEXT,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("a tile that is still loading already shows its (i)", async () => {
|
|
239
|
+
renderOverview([{ ...P95_TILE, loading: true }]);
|
|
240
|
+
|
|
241
|
+
const root: HTMLElement = tileRoot("p95 duration");
|
|
242
|
+
|
|
243
|
+
expect(root).toHaveAttribute("aria-busy", "true");
|
|
244
|
+
expect(within(root).getByText("Loading p95 duration")).toBeInTheDocument();
|
|
245
|
+
expect(within(root).queryByText("123 ms")).not.toBeInTheDocument();
|
|
246
|
+
|
|
247
|
+
await expectTooltip(
|
|
248
|
+
within(root).getByRole("button", { name: "About p95 duration" }),
|
|
249
|
+
P95_TEXT,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("tiles loaded as a group (tilesLoading) keep their (i)", () => {
|
|
254
|
+
renderOverview([P95_TILE], { tilesLoading: true });
|
|
255
|
+
|
|
256
|
+
expect(tileRoot("p95 duration")).toHaveAttribute("aria-busy", "true");
|
|
257
|
+
expect(
|
|
258
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
259
|
+
).toBeInTheDocument();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("an unlinked tile has no link at all", () => {
|
|
263
|
+
renderOverview([P95_TILE]);
|
|
264
|
+
|
|
265
|
+
expect(within(tileRoot("p95 duration")).queryByRole("link")).toBeNull();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("a linked ResourceOverview tile", () => {
|
|
270
|
+
const CLIENTS: ResourceOverviewTile = {
|
|
271
|
+
title: "Clients",
|
|
272
|
+
value: "4",
|
|
273
|
+
icon: IconProp.Window,
|
|
274
|
+
iconColor: "amber",
|
|
275
|
+
sublabel: "platforms seen",
|
|
276
|
+
to: new Route("/dashboard/project/rum/app/clients"),
|
|
277
|
+
description: CLIENTS_TEXT,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
test("the link is an overlay anchor with the tile's route and a 'View <title>' name", () => {
|
|
281
|
+
renderOverview([CLIENTS]);
|
|
282
|
+
|
|
283
|
+
const link: HTMLElement = screen.getByRole("link", {
|
|
284
|
+
name: "View Clients",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(link).toHaveAttribute("href", "/dashboard/project/rum/app/clients");
|
|
288
|
+
expect(link).toHaveClass("absolute", "inset-0");
|
|
289
|
+
expect(tileRoot("Clients")).toHaveClass("relative");
|
|
290
|
+
expect(tileRoot("Clients")).toContainElement(link);
|
|
291
|
+
|
|
292
|
+
const srOnly: HTMLElement = within(link).getByText("View Clients");
|
|
293
|
+
expect(srOnly).toHaveClass("sr-only");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("the link does not wrap the tile's content", () => {
|
|
297
|
+
renderOverview([CLIENTS]);
|
|
298
|
+
|
|
299
|
+
const link: HTMLElement = screen.getByRole("link", {
|
|
300
|
+
name: "View Clients",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(link.textContent).toBe("View Clients");
|
|
304
|
+
expect(within(link).queryByText("4")).toBeNull();
|
|
305
|
+
expect(link.parentElement).toBe(tileRoot("Clients"));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("the (i) is not a descendant of any anchor and is lifted above the overlay", () => {
|
|
309
|
+
renderOverview([CLIENTS]);
|
|
310
|
+
|
|
311
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
312
|
+
name: "About Clients",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(info.closest("a")).toBeNull();
|
|
316
|
+
for (const anchor of Array.from(document.querySelectorAll("a"))) {
|
|
317
|
+
expect(anchor).not.toContainElement(info);
|
|
318
|
+
}
|
|
319
|
+
expect(info).toHaveClass("relative", "z-10");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("clicking the (i) does not navigate; clicking the overlay does", () => {
|
|
323
|
+
renderOverview([CLIENTS]);
|
|
324
|
+
|
|
325
|
+
expect(screen.getByTestId("location")).toHaveTextContent(
|
|
326
|
+
"/dashboard/overview",
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
fireEvent.click(screen.getByRole("button", { name: "About Clients" }));
|
|
330
|
+
expect(screen.getByTestId("location")).toHaveTextContent(
|
|
331
|
+
"/dashboard/overview",
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
fireEvent.click(screen.getByRole("link", { name: "View Clients" }));
|
|
335
|
+
expect(screen.getByTestId("location")).toHaveTextContent(
|
|
336
|
+
"/dashboard/project/rum/app/clients",
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("hovering the (i) on a linked tile shows the text", async () => {
|
|
341
|
+
renderOverview([CLIENTS]);
|
|
342
|
+
|
|
343
|
+
await expectTooltip(
|
|
344
|
+
screen.getByRole("button", { name: "About Clients" }),
|
|
345
|
+
CLIENTS_TEXT,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("a linked tile without a description still links, with no (i)", () => {
|
|
350
|
+
renderOverview([{ ...CLIENTS, description: undefined }]);
|
|
351
|
+
|
|
352
|
+
expect(
|
|
353
|
+
screen.getByRole("link", { name: "View Clients" }),
|
|
354
|
+
).toBeInTheDocument();
|
|
355
|
+
expect(
|
|
356
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
357
|
+
).not.toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a linked tile that is loading keeps its link and its (i)", () => {
|
|
361
|
+
renderOverview([{ ...CLIENTS, loading: true }]);
|
|
362
|
+
|
|
363
|
+
expect(
|
|
364
|
+
screen.getByRole("link", { name: "View Clients" }),
|
|
365
|
+
).toBeInTheDocument();
|
|
366
|
+
expect(
|
|
367
|
+
screen.getByRole("button", { name: "About Clients" }).closest("a"),
|
|
368
|
+
).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
/* ------------------------------------------------------------------ */
|
|
373
|
+
/* ChartCard */
|
|
374
|
+
/* ------------------------------------------------------------------ */
|
|
375
|
+
|
|
376
|
+
const WINDOW_START: Date = new Date("2026-09-24T10:00:00.000Z");
|
|
377
|
+
const WINDOW_END: Date = new Date("2026-09-24T11:00:00.000Z");
|
|
378
|
+
|
|
379
|
+
const DATA: Array<SeriesPoint> = [
|
|
380
|
+
{
|
|
381
|
+
seriesName: "p95",
|
|
382
|
+
data: [{ x: WINDOW_START, y: 120 }],
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
interface ChartCardOptions {
|
|
387
|
+
loading?: boolean;
|
|
388
|
+
series?: Array<SeriesPoint>;
|
|
389
|
+
windowStart?: Date | null;
|
|
390
|
+
description?: string | undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renderChartCard(options: ChartCardOptions = {}): void {
|
|
394
|
+
render(
|
|
395
|
+
<ChartCard
|
|
396
|
+
title="p95 duration"
|
|
397
|
+
icon={IconProp.Clock}
|
|
398
|
+
iconColor="violet"
|
|
399
|
+
series={options.series ?? DATA}
|
|
400
|
+
windowStart={
|
|
401
|
+
options.windowStart === undefined ? WINDOW_START : options.windowStart
|
|
402
|
+
}
|
|
403
|
+
windowEnd={WINDOW_END}
|
|
404
|
+
syncId="test"
|
|
405
|
+
loading={options.loading}
|
|
406
|
+
description={"description" in options ? options.description : P95_TEXT}
|
|
407
|
+
/>,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
describe("ChartCard", () => {
|
|
412
|
+
test("loading: the header already carries the (i)", async () => {
|
|
413
|
+
renderChartCard({ loading: true });
|
|
414
|
+
|
|
415
|
+
expect(screen.queryByTestId("line-chart")).not.toBeInTheDocument();
|
|
416
|
+
await expectTooltip(
|
|
417
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
418
|
+
P95_TEXT,
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("no window yet counts as loading and still carries the (i)", () => {
|
|
423
|
+
renderChartCard({ windowStart: null });
|
|
424
|
+
|
|
425
|
+
expect(screen.queryByTestId("line-chart")).not.toBeInTheDocument();
|
|
426
|
+
expect(
|
|
427
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
428
|
+
).toBeInTheDocument();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("no data: the empty state keeps the (i)", async () => {
|
|
432
|
+
renderChartCard({ series: [{ seriesName: "p95", data: [] }] });
|
|
433
|
+
|
|
434
|
+
expect(screen.getByText("No data in this time range")).toBeInTheDocument();
|
|
435
|
+
await expectTooltip(
|
|
436
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
437
|
+
P95_TEXT,
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("with data: the chart renders and the (i) sits in its header", async () => {
|
|
442
|
+
renderChartCard();
|
|
443
|
+
|
|
444
|
+
expect(screen.getByTestId("line-chart")).toBeInTheDocument();
|
|
445
|
+
expect(lineChartMock).toHaveBeenCalled();
|
|
446
|
+
await expectTooltip(
|
|
447
|
+
screen.getByRole("button", { name: "About p95 duration" }),
|
|
448
|
+
P95_TEXT,
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test.each([
|
|
453
|
+
["loading", { loading: true }],
|
|
454
|
+
["no data", { series: [{ seriesName: "p95", data: [] }] }],
|
|
455
|
+
["data", {}],
|
|
456
|
+
])(
|
|
457
|
+
"%s without a description: no (i)",
|
|
458
|
+
(_: string, options: ChartCardOptions) => {
|
|
459
|
+
renderChartCard({ ...options, description: undefined });
|
|
460
|
+
|
|
461
|
+
expect(screen.getByText("p95 duration")).toBeInTheDocument();
|
|
462
|
+
expect(
|
|
463
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
464
|
+
).not.toBeInTheDocument();
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
test("the (i) follows the title inside the header, not the icon badge", () => {
|
|
469
|
+
renderChartCard();
|
|
470
|
+
|
|
471
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
472
|
+
name: "About p95 duration",
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(info.previousElementSibling).toHaveTextContent("p95 duration");
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
/* ------------------------------------------------------------------ */
|
|
480
|
+
/* Infrastructure GoldenMetricTile */
|
|
481
|
+
/* ------------------------------------------------------------------ */
|
|
482
|
+
|
|
483
|
+
describe("Infrastructure GoldenMetricTile", () => {
|
|
484
|
+
test("a description becomes the (i)'s tooltip", async () => {
|
|
485
|
+
render(
|
|
486
|
+
<InfraGoldenMetricTile
|
|
487
|
+
title="CPU"
|
|
488
|
+
icon={IconProp.Activity}
|
|
489
|
+
iconColor="blue"
|
|
490
|
+
value="42%"
|
|
491
|
+
sublabel="of allocatable"
|
|
492
|
+
percent={42}
|
|
493
|
+
description="Processor time used across the cluster, as a share of what the nodes can allocate."
|
|
494
|
+
/>,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
expect(screen.getByText("42%")).toBeInTheDocument();
|
|
498
|
+
expect(screen.getByText("of allocatable")).toBeInTheDocument();
|
|
499
|
+
await expectTooltip(
|
|
500
|
+
screen.getByRole("button", { name: "About CPU" }),
|
|
501
|
+
"Processor time used across the cluster, as a share of what the nodes can allocate.",
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("no description, no (i)", () => {
|
|
506
|
+
render(
|
|
507
|
+
<InfraGoldenMetricTile
|
|
508
|
+
title="CPU"
|
|
509
|
+
icon={IconProp.Activity}
|
|
510
|
+
iconColor="blue"
|
|
511
|
+
value="42%"
|
|
512
|
+
/>,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
expect(screen.getByText("CPU")).toBeInTheDocument();
|
|
516
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("the (i) follows the title", () => {
|
|
520
|
+
render(
|
|
521
|
+
<InfraGoldenMetricTile
|
|
522
|
+
title="Pods"
|
|
523
|
+
icon={IconProp.Cube}
|
|
524
|
+
iconColor="emerald"
|
|
525
|
+
value="12"
|
|
526
|
+
description="Pods running in the cluster right now."
|
|
527
|
+
/>,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
expect(
|
|
531
|
+
screen.getByRole("button", { name: "About Pods" }).previousElementSibling,
|
|
532
|
+
).toHaveTextContent("Pods");
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
/* ------------------------------------------------------------------ */
|
|
537
|
+
/* WebVitalsCard */
|
|
538
|
+
/* ------------------------------------------------------------------ */
|
|
539
|
+
|
|
540
|
+
function vitalsWith(
|
|
541
|
+
value: (d: WebVitalDefinition) => number | null,
|
|
542
|
+
): Array<WebVital> {
|
|
543
|
+
return WebVitalDefinitions.map((d: WebVitalDefinition): WebVital => {
|
|
544
|
+
return {
|
|
545
|
+
key: d.key,
|
|
546
|
+
label: d.label,
|
|
547
|
+
description: d.description,
|
|
548
|
+
value: value(d),
|
|
549
|
+
unit: d.unit,
|
|
550
|
+
thresholds: d.thresholds,
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const CARD_TEXT: string =
|
|
556
|
+
"Standard measures of how fast and stable your pages feel to real visitors.";
|
|
557
|
+
|
|
558
|
+
describe("WebVitalsCard", () => {
|
|
559
|
+
test("the card title carries an (i) with the card's description", async () => {
|
|
560
|
+
render(
|
|
561
|
+
<WebVitalsCard
|
|
562
|
+
vitals={vitalsWith(() => {
|
|
563
|
+
return 100;
|
|
564
|
+
})}
|
|
565
|
+
loading={false}
|
|
566
|
+
description={CARD_TEXT}
|
|
567
|
+
/>,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const info: HTMLElement = screen.getByRole("button", {
|
|
571
|
+
name: "About Core Web Vitals",
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(info.querySelector("svg")).toHaveClass("h-4", "w-4");
|
|
575
|
+
await expectTooltip(info, CARD_TEXT);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("without a description the title is plain text", () => {
|
|
579
|
+
render(
|
|
580
|
+
<WebVitalsCard
|
|
581
|
+
vitals={vitalsWith(() => {
|
|
582
|
+
return 100;
|
|
583
|
+
})}
|
|
584
|
+
loading={false}
|
|
585
|
+
/>,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
expect(screen.getByText("Core Web Vitals")).toBeInTheDocument();
|
|
589
|
+
expect(
|
|
590
|
+
screen.queryByRole("button", { name: "About Core Web Vitals" }),
|
|
591
|
+
).not.toBeInTheDocument();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("each reported vital has an (i) named after its short and long name", () => {
|
|
595
|
+
render(
|
|
596
|
+
<WebVitalsCard
|
|
597
|
+
vitals={vitalsWith((d: WebVitalDefinition) => {
|
|
598
|
+
return d.thresholds.warn / 2;
|
|
599
|
+
})}
|
|
600
|
+
loading={false}
|
|
601
|
+
/>,
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
for (const d of WebVitalDefinitions) {
|
|
605
|
+
expect(
|
|
606
|
+
screen.getByRole("button", {
|
|
607
|
+
name: `About ${d.key.toUpperCase()} (${d.label})`,
|
|
608
|
+
}),
|
|
609
|
+
).toBeInTheDocument();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test.each(
|
|
614
|
+
WebVitalDefinitions.map(
|
|
615
|
+
(d: WebVitalDefinition): [string, WebVitalDefinition] => {
|
|
616
|
+
return [d.key, d];
|
|
617
|
+
},
|
|
618
|
+
),
|
|
619
|
+
)(
|
|
620
|
+
"%s: the tooltip is what it measures, then its Good / Poor limits",
|
|
621
|
+
async (_: string, d: WebVitalDefinition) => {
|
|
622
|
+
render(
|
|
623
|
+
<WebVitalsCard
|
|
624
|
+
vitals={vitalsWith((def: WebVitalDefinition) => {
|
|
625
|
+
return def.thresholds.warn / 2;
|
|
626
|
+
})}
|
|
627
|
+
loading={false}
|
|
628
|
+
/>,
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
await expectTooltip(
|
|
632
|
+
screen.getByRole("button", {
|
|
633
|
+
name: `About ${d.key.toUpperCase()} (${d.label})`,
|
|
634
|
+
}),
|
|
635
|
+
`${d.description} ${describeWebVitalThresholds(d)}`,
|
|
636
|
+
);
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
test("the LCP limits read the way a person says them", async () => {
|
|
641
|
+
render(
|
|
642
|
+
<WebVitalsCard
|
|
643
|
+
vitals={vitalsWith(() => {
|
|
644
|
+
return 1000;
|
|
645
|
+
})}
|
|
646
|
+
loading={false}
|
|
647
|
+
/>,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
await hover(
|
|
651
|
+
screen.getByRole("button", {
|
|
652
|
+
name: "About LCP (Largest Contentful Paint)",
|
|
653
|
+
}),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
expect(screen.getByRole("tooltip").textContent).toMatch(
|
|
657
|
+
/Good below 2\.5 s; poor at 4 s or more\.$/,
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("a vital with no value still explains itself when others reported", () => {
|
|
662
|
+
render(
|
|
663
|
+
<WebVitalsCard
|
|
664
|
+
vitals={vitalsWith((d: WebVitalDefinition) => {
|
|
665
|
+
return d.key === "lcp" ? 1200 : null;
|
|
666
|
+
})}
|
|
667
|
+
loading={false}
|
|
668
|
+
/>,
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
expect(
|
|
672
|
+
screen.getByRole("button", {
|
|
673
|
+
name: "About INP (Interaction to Next Paint)",
|
|
674
|
+
}),
|
|
675
|
+
).toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("the empty state shows no per-vital (i), but the card's own (i) stays", () => {
|
|
679
|
+
render(
|
|
680
|
+
<WebVitalsCard
|
|
681
|
+
vitals={vitalsWith(() => {
|
|
682
|
+
return null;
|
|
683
|
+
})}
|
|
684
|
+
loading={false}
|
|
685
|
+
description={CARD_TEXT}
|
|
686
|
+
/>,
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
expect(screen.getByText("No web vitals reported yet")).toBeInTheDocument();
|
|
690
|
+
expect(screen.getAllByRole("button", { name: /^About / })).toHaveLength(1);
|
|
691
|
+
expect(
|
|
692
|
+
screen.getByRole("button", { name: "About Core Web Vitals" }),
|
|
693
|
+
).toBeInTheDocument();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test("while loading, only the card's (i) is shown", () => {
|
|
697
|
+
render(
|
|
698
|
+
<WebVitalsCard vitals={[]} loading={true} description={CARD_TEXT} />,
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
expect(screen.getAllByRole("button", { name: /^About / })).toHaveLength(1);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
describe("describeWebVital", () => {
|
|
706
|
+
function vital(key: string): WebVital {
|
|
707
|
+
return vitalsWith(() => {
|
|
708
|
+
return 1;
|
|
709
|
+
}).find((v: WebVital): boolean => {
|
|
710
|
+
return v.key === key;
|
|
711
|
+
})!;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
test.each([
|
|
715
|
+
["lcp", "Good below 2.5 s; poor at 4 s or more."],
|
|
716
|
+
["inp", "Good below 200 ms; poor at 500 ms or more."],
|
|
717
|
+
["cls", "Good below 0.1; poor at 0.25 or more."],
|
|
718
|
+
["fcp", "Good below 1.8 s; poor at 3 s or more."],
|
|
719
|
+
["ttfb", "Good below 800 ms; poor at 1.8 s or more."],
|
|
720
|
+
])(
|
|
721
|
+
"%s: description, one space, then the limits",
|
|
722
|
+
(key: string, limits: string) => {
|
|
723
|
+
const v: WebVital = vital(key);
|
|
724
|
+
|
|
725
|
+
expect(describeWebVital(v)).toBe(`${v.description} ${limits}`);
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
test("with no description it is just the limits, with no leading space", () => {
|
|
730
|
+
expect(describeWebVital({ ...vital("cls"), description: "" })).toBe(
|
|
731
|
+
"Good below 0.1; poor at 0.25 or more.",
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("the limits follow the vital's own thresholds, so a change cannot leave the tooltip stale", () => {
|
|
736
|
+
expect(
|
|
737
|
+
describeWebVital({
|
|
738
|
+
...vital("lcp"),
|
|
739
|
+
description: "Largest paint.",
|
|
740
|
+
thresholds: { warn: 3000, danger: 6000 },
|
|
741
|
+
}),
|
|
742
|
+
).toBe("Largest paint. Good below 3 s; poor at 6 s or more.");
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
/* ------------------------------------------------------------------ */
|
|
747
|
+
/* ResourceOverviewTab summary fields */
|
|
748
|
+
/* ------------------------------------------------------------------ */
|
|
749
|
+
|
|
750
|
+
describe("ResourceOverviewTab summary fields", () => {
|
|
751
|
+
test("a field with a description gets an (i); one without does not", async () => {
|
|
752
|
+
render(
|
|
753
|
+
<ResourceOverviewTab
|
|
754
|
+
summaryFields={[
|
|
755
|
+
{
|
|
756
|
+
title: "Ready",
|
|
757
|
+
value: "3/3",
|
|
758
|
+
description:
|
|
759
|
+
"Pods that passed their readiness check, out of the pods wanted.",
|
|
760
|
+
},
|
|
761
|
+
{ title: "Namespace", value: "default" },
|
|
762
|
+
]}
|
|
763
|
+
labels={{}}
|
|
764
|
+
annotations={{}}
|
|
765
|
+
isLoading={false}
|
|
766
|
+
/>,
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
expect(screen.getByText("3/3")).toBeInTheDocument();
|
|
770
|
+
expect(screen.getByText("default")).toBeInTheDocument();
|
|
771
|
+
expect(screen.getAllByRole("button", { name: /^About / })).toHaveLength(1);
|
|
772
|
+
await expectTooltip(
|
|
773
|
+
screen.getByRole("button", { name: "About Ready" }),
|
|
774
|
+
"Pods that passed their readiness check, out of the pods wanted.",
|
|
775
|
+
);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("an element value keeps its (i)", () => {
|
|
779
|
+
render(
|
|
780
|
+
<ResourceOverviewTab
|
|
781
|
+
summaryFields={[
|
|
782
|
+
{
|
|
783
|
+
title: "Restarts",
|
|
784
|
+
value: <span data-testid="restarts">7</span>,
|
|
785
|
+
description: "Container restarts since the pod was created.",
|
|
786
|
+
},
|
|
787
|
+
]}
|
|
788
|
+
labels={{}}
|
|
789
|
+
annotations={{}}
|
|
790
|
+
isLoading={false}
|
|
791
|
+
/>,
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
expect(screen.getByTestId("restarts")).toHaveTextContent("7");
|
|
795
|
+
expect(
|
|
796
|
+
screen.getByRole("button", { name: "About Restarts" }),
|
|
797
|
+
).toBeInTheDocument();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test("the summary cards are not clickable, so the (i) is the only control", () => {
|
|
801
|
+
render(
|
|
802
|
+
<ResourceOverviewTab
|
|
803
|
+
summaryFields={[
|
|
804
|
+
{
|
|
805
|
+
title: "Ready",
|
|
806
|
+
value: "3/3",
|
|
807
|
+
description: "Pods that passed their readiness check.",
|
|
808
|
+
},
|
|
809
|
+
]}
|
|
810
|
+
labels={{}}
|
|
811
|
+
annotations={{}}
|
|
812
|
+
isLoading={false}
|
|
813
|
+
/>,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
expect(screen.getAllByRole("button")).toHaveLength(1);
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
/* ------------------------------------------------------------------ */
|
|
821
|
+
/* ResourceTable / KubernetesResourceTable header tooltips */
|
|
822
|
+
/* ------------------------------------------------------------------ */
|
|
823
|
+
|
|
824
|
+
const RESOURCES: Array<InfrastructureResource> = [
|
|
825
|
+
{
|
|
826
|
+
name: "api",
|
|
827
|
+
namespace: "prod",
|
|
828
|
+
cpuUtilization: 12,
|
|
829
|
+
memoryUsageBytes: 200 * 1024 * 1024,
|
|
830
|
+
memoryLimitBytes: 512 * 1024 * 1024,
|
|
831
|
+
status: "Running",
|
|
832
|
+
age: "3d",
|
|
833
|
+
additionalAttributes: { restarts: "0" },
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
name: "web",
|
|
837
|
+
namespace: "prod",
|
|
838
|
+
cpuUtilization: 140,
|
|
839
|
+
memoryUsageBytes: 100 * 1024 * 1024,
|
|
840
|
+
memoryLimitBytes: null,
|
|
841
|
+
status: "Running",
|
|
842
|
+
age: "1d",
|
|
843
|
+
additionalAttributes: { restarts: "4" },
|
|
844
|
+
},
|
|
845
|
+
];
|
|
846
|
+
|
|
847
|
+
const BUILT_IN: {
|
|
848
|
+
status: string;
|
|
849
|
+
cpu: string;
|
|
850
|
+
memory: string;
|
|
851
|
+
age: string;
|
|
852
|
+
} = {
|
|
853
|
+
status: "The phase the pod reported most recently.",
|
|
854
|
+
cpu: "Processor time used by the pod's containers - 100% is one full CPU core, so it can exceed 100%.",
|
|
855
|
+
memory:
|
|
856
|
+
"Memory the pod's containers are using, as a share of their limit when one is set.",
|
|
857
|
+
age: "How long ago the pod was created.",
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const RESTARTS_TEXT: string =
|
|
861
|
+
"How many times the pod's containers were restarted since it was created.";
|
|
862
|
+
|
|
863
|
+
function headerInfo(): Array<string> {
|
|
864
|
+
return screen
|
|
865
|
+
.getAllByRole("button", { name: /^About / })
|
|
866
|
+
.map((b: HTMLElement): string => {
|
|
867
|
+
return b.getAttribute("aria-label") || "";
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
describe("ResourceTable header tooltips", () => {
|
|
872
|
+
test("built-in column descriptions become header (i)s", async () => {
|
|
873
|
+
render(
|
|
874
|
+
<ResourceTable
|
|
875
|
+
resources={RESOURCES}
|
|
876
|
+
title="Pods"
|
|
877
|
+
description="Pods in the cluster."
|
|
878
|
+
builtInColumnDescriptions={BUILT_IN}
|
|
879
|
+
/>,
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
expect(headerInfo()).toEqual([
|
|
883
|
+
"About Status",
|
|
884
|
+
"About CPU",
|
|
885
|
+
"About Memory",
|
|
886
|
+
"About Age",
|
|
887
|
+
]);
|
|
888
|
+
|
|
889
|
+
for (const info of screen.getAllByRole("button", { name: /^About / })) {
|
|
890
|
+
expect(info.closest("thead")).not.toBeNull();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
await expectTooltip(
|
|
894
|
+
screen.getByRole("button", { name: "About CPU" }),
|
|
895
|
+
BUILT_IN.cpu,
|
|
896
|
+
);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test.each([
|
|
900
|
+
["status", "About Status"],
|
|
901
|
+
["cpu", "About CPU"],
|
|
902
|
+
["memory", "About Memory"],
|
|
903
|
+
["age", "About Age"],
|
|
904
|
+
] as Array<[keyof typeof BUILT_IN, string]>)(
|
|
905
|
+
"the %s description is shown on that column only",
|
|
906
|
+
async (key: keyof typeof BUILT_IN, name: string) => {
|
|
907
|
+
render(
|
|
908
|
+
<ResourceTable
|
|
909
|
+
resources={RESOURCES}
|
|
910
|
+
title="Pods"
|
|
911
|
+
description="Pods in the cluster."
|
|
912
|
+
builtInColumnDescriptions={{ [key]: BUILT_IN[key] }}
|
|
913
|
+
/>,
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
expect(headerInfo()).toEqual([name]);
|
|
917
|
+
await expectTooltip(screen.getByRole("button", { name }), BUILT_IN[key]);
|
|
918
|
+
},
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
test("a custom column's description becomes its header (i)", async () => {
|
|
922
|
+
render(
|
|
923
|
+
<ResourceTable
|
|
924
|
+
resources={RESOURCES}
|
|
925
|
+
title="Pods"
|
|
926
|
+
description="Pods in the cluster."
|
|
927
|
+
columns={[
|
|
928
|
+
{ title: "Restarts", key: "restarts", description: RESTARTS_TEXT },
|
|
929
|
+
{ title: "Node", key: "node" },
|
|
930
|
+
]}
|
|
931
|
+
/>,
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
expect(headerInfo()).toEqual(["About Restarts"]);
|
|
935
|
+
await expectTooltip(
|
|
936
|
+
screen.getByRole("button", { name: "About Restarts" }),
|
|
937
|
+
RESTARTS_TEXT,
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test("without any descriptions the table has no (i) at all", () => {
|
|
942
|
+
render(
|
|
943
|
+
<ResourceTable
|
|
944
|
+
resources={RESOURCES}
|
|
945
|
+
title="Pods"
|
|
946
|
+
description="Pods in the cluster."
|
|
947
|
+
columns={[{ title: "Restarts", key: "restarts" }]}
|
|
948
|
+
/>,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
expect(
|
|
952
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
953
|
+
).not.toBeInTheDocument();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("hidden built-in columns take their (i) with them", () => {
|
|
957
|
+
render(
|
|
958
|
+
<ResourceTable
|
|
959
|
+
resources={RESOURCES}
|
|
960
|
+
title="Pods"
|
|
961
|
+
description="Pods in the cluster."
|
|
962
|
+
showStatus={false}
|
|
963
|
+
showResourceMetrics={false}
|
|
964
|
+
builtInColumnDescriptions={BUILT_IN}
|
|
965
|
+
/>,
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
expect(
|
|
969
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
970
|
+
).not.toBeInTheDocument();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
test("the CPU column still sorts when it carries an (i), and the (i) does not sort", () => {
|
|
974
|
+
render(
|
|
975
|
+
<ResourceTable
|
|
976
|
+
resources={RESOURCES}
|
|
977
|
+
title="Pods"
|
|
978
|
+
description="Pods in the cluster."
|
|
979
|
+
builtInColumnDescriptions={BUILT_IN}
|
|
980
|
+
/>,
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
const rowNames: () => Array<string> = (): Array<string> => {
|
|
984
|
+
return Array.from(document.querySelectorAll("tbody tr"))
|
|
985
|
+
.map((tr: Element): string => {
|
|
986
|
+
return tr.querySelector("td")?.textContent?.trim() || "";
|
|
987
|
+
})
|
|
988
|
+
.filter(Boolean);
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const before: Array<string> = rowNames();
|
|
992
|
+
fireEvent.click(screen.getByRole("button", { name: "About CPU" }));
|
|
993
|
+
expect(rowNames()).toEqual(before);
|
|
994
|
+
|
|
995
|
+
// First click sorts descending (the table starts ascending).
|
|
996
|
+
fireEvent.click(screen.getByRole("button", { name: "CPU" }));
|
|
997
|
+
expect(rowNames()).toEqual(["web", "api"]);
|
|
998
|
+
|
|
999
|
+
fireEvent.click(screen.getByRole("button", { name: "CPU" }));
|
|
1000
|
+
expect(rowNames()).toEqual(["api", "web"]);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("the cell values render unchanged", () => {
|
|
1004
|
+
render(
|
|
1005
|
+
<ResourceTable
|
|
1006
|
+
resources={RESOURCES}
|
|
1007
|
+
title="Pods"
|
|
1008
|
+
description="Pods in the cluster."
|
|
1009
|
+
builtInColumnDescriptions={BUILT_IN}
|
|
1010
|
+
/>,
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
expect(screen.getAllByText("140.0%").length).toBeGreaterThan(0);
|
|
1014
|
+
expect(screen.getAllByText("12.0%").length).toBeGreaterThan(0);
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
describe("KubernetesResourceTable passes the descriptions through", () => {
|
|
1019
|
+
test("built-in and custom column descriptions both reach the header", async () => {
|
|
1020
|
+
render(
|
|
1021
|
+
<KubernetesResourceTable
|
|
1022
|
+
resources={RESOURCES}
|
|
1023
|
+
title="Pods"
|
|
1024
|
+
description="Pods in the cluster."
|
|
1025
|
+
showNamespace={true}
|
|
1026
|
+
columns={[
|
|
1027
|
+
{ title: "Restarts", key: "restarts", description: RESTARTS_TEXT },
|
|
1028
|
+
]}
|
|
1029
|
+
builtInColumnDescriptions={BUILT_IN}
|
|
1030
|
+
/>,
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
expect(screen.getByText("Namespace")).toBeInTheDocument();
|
|
1034
|
+
expect(headerInfo()).toEqual([
|
|
1035
|
+
"About Status",
|
|
1036
|
+
"About Restarts",
|
|
1037
|
+
"About CPU",
|
|
1038
|
+
"About Memory",
|
|
1039
|
+
"About Age",
|
|
1040
|
+
]);
|
|
1041
|
+
|
|
1042
|
+
await expectTooltip(
|
|
1043
|
+
screen.getByRole("button", { name: "About Restarts" }),
|
|
1044
|
+
RESTARTS_TEXT,
|
|
1045
|
+
);
|
|
1046
|
+
await expectTooltip(
|
|
1047
|
+
screen.getByRole("button", { name: "About Memory" }),
|
|
1048
|
+
BUILT_IN.memory,
|
|
1049
|
+
);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("without descriptions it renders as before, with no (i)", () => {
|
|
1053
|
+
render(
|
|
1054
|
+
<KubernetesResourceTable
|
|
1055
|
+
resources={RESOURCES}
|
|
1056
|
+
title="Pods"
|
|
1057
|
+
description="Pods in the cluster."
|
|
1058
|
+
columns={[{ title: "Restarts", key: "restarts" }]}
|
|
1059
|
+
/>,
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
expect(
|
|
1063
|
+
screen.queryByRole("button", { name: /^About / }),
|
|
1064
|
+
).not.toBeInTheDocument();
|
|
1065
|
+
});
|
|
1066
|
+
});
|