@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,556 @@
|
|
|
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 } from "react-router-dom";
|
|
20
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* The (i) tooltips on a device's connectivity and traffic numbers,
|
|
24
|
+
* RENDERED: the on-demand ping result rows, the traceroute hop table, the
|
|
25
|
+
* past hour's latency trend (device Overview and topology drawer) and the
|
|
26
|
+
* Traffic tab's NetFlow top talkers.
|
|
27
|
+
*
|
|
28
|
+
* Only the network is replaced: the metrics query, the diagnostic row reads
|
|
29
|
+
* (through DeviceDiagnostics' own modelAPI seam) and the top-talkers POST.
|
|
30
|
+
* Every statistic is checked for an (i) whose tooltip is the matching
|
|
31
|
+
* NETWORK_DEVICE_METRIC_DESCRIPTIONS entry; the rows and columns that only
|
|
32
|
+
* name something (Host, Reason, Hop, Source IP) are checked for having none.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const DEVICE_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
36
|
+
const DIAGNOSTIC_ID: string = "22222222-2222-4222-8222-222222222222";
|
|
37
|
+
const PROBE_ID: string = "44444444-4444-4444-8444-444444444444";
|
|
38
|
+
const PROJECT_ID: string = "10000000-0000-4000-8000-000000000001";
|
|
39
|
+
|
|
40
|
+
const fetchResultsMock: MockFunction = getJestMockFunction();
|
|
41
|
+
const apiPostMock: MockFunction = getJestMockFunction();
|
|
42
|
+
|
|
43
|
+
jest.mock("../../../UI/Utils/Project", () => {
|
|
44
|
+
return {
|
|
45
|
+
__esModule: true,
|
|
46
|
+
default: {
|
|
47
|
+
// Read at render time, long after the module-level id below exists.
|
|
48
|
+
getCurrentProjectId: (): unknown => {
|
|
49
|
+
return mockProjectObjectId;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
56
|
+
return {
|
|
57
|
+
__esModule: true,
|
|
58
|
+
default: {
|
|
59
|
+
post: (...args: Array<unknown>): unknown => {
|
|
60
|
+
return apiPostMock(...args);
|
|
61
|
+
},
|
|
62
|
+
getFriendlyMessage: (error: Error): string => {
|
|
63
|
+
return error.message;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
jest.mock(
|
|
70
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
|
|
71
|
+
() => {
|
|
72
|
+
return {
|
|
73
|
+
__esModule: true,
|
|
74
|
+
default: {
|
|
75
|
+
fetchResults: (...args: Array<unknown>): unknown => {
|
|
76
|
+
return fetchResultsMock(...args);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
jest.mock(
|
|
84
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricSparkline",
|
|
85
|
+
() => {
|
|
86
|
+
return {
|
|
87
|
+
__esModule: true,
|
|
88
|
+
default: (): string => {
|
|
89
|
+
return "sparkline-stub";
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
import DeviceDiagnostics from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceDiagnostics";
|
|
96
|
+
import { DIAGNOSTIC_POLL_INTERVAL_IN_MS } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceDiagnosticsViewModel";
|
|
97
|
+
import DeviceLatencyTrend from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/DeviceLatencyTrend";
|
|
98
|
+
import FlowTopTalkers, {
|
|
99
|
+
FlowSectionTitle,
|
|
100
|
+
FlowStatTile,
|
|
101
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/FlowTopTalkers";
|
|
102
|
+
import TracerouteHopsTable from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/TracerouteHopsTable";
|
|
103
|
+
import {
|
|
104
|
+
NETWORK_DEVICE_METRIC_DESCRIPTIONS,
|
|
105
|
+
NetworkDeviceMetric,
|
|
106
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/NetworkDeviceMetricDescriptions";
|
|
107
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
108
|
+
import NetworkDeviceDiagnostic from "../../../Models/DatabaseModels/NetworkDeviceDiagnostic";
|
|
109
|
+
import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
|
|
110
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
111
|
+
import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
|
|
112
|
+
|
|
113
|
+
const DESCRIPTIONS: Record<NetworkDeviceMetric, string> =
|
|
114
|
+
NETWORK_DEVICE_METRIC_DESCRIPTIONS;
|
|
115
|
+
|
|
116
|
+
// Handed out by the Project mock; below the imports so ObjectID exists.
|
|
117
|
+
const mockProjectObjectId: ObjectID = new ObjectID(PROJECT_ID);
|
|
118
|
+
|
|
119
|
+
async function flush(): Promise<void> {
|
|
120
|
+
for (let i: number = 0; i < 5; i++) {
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await Promise.resolve();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function infoButtonNames(container?: HTMLElement): Array<string> {
|
|
128
|
+
const scope: ReturnType<typeof within> | typeof screen = container
|
|
129
|
+
? within(container)
|
|
130
|
+
: screen;
|
|
131
|
+
|
|
132
|
+
return scope
|
|
133
|
+
.queryAllByRole("button", { name: /^About / })
|
|
134
|
+
.map((button: HTMLElement): string => {
|
|
135
|
+
return button.getAttribute("aria-label") || "";
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function expectExplained(label: string, text: string): Promise<void> {
|
|
140
|
+
const button: HTMLElement = screen.getByRole("button", {
|
|
141
|
+
name: `About ${label}`,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
fireEvent.mouseEnter(button);
|
|
145
|
+
await act(async () => {
|
|
146
|
+
jest.advanceTimersByTime(200);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(
|
|
150
|
+
screen.getAllByRole("tooltip").map((tooltip: HTMLElement): string => {
|
|
151
|
+
return tooltip.textContent || "";
|
|
152
|
+
}),
|
|
153
|
+
).toContain(text);
|
|
154
|
+
|
|
155
|
+
fireEvent.mouseLeave(button);
|
|
156
|
+
await act(async () => {
|
|
157
|
+
jest.advanceTimersByTime(400);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
jest.useFakeTimers();
|
|
163
|
+
fetchResultsMock.mockReset();
|
|
164
|
+
apiPostMock.mockReset();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
cleanup();
|
|
169
|
+
jest.useRealTimers();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// --------------------------------------------------------- ping result
|
|
173
|
+
|
|
174
|
+
const createMock: MockFunction = getJestMockFunction();
|
|
175
|
+
const getDiagnosticMock: MockFunction = getJestMockFunction();
|
|
176
|
+
const getDeviceMock: MockFunction = getJestMockFunction();
|
|
177
|
+
|
|
178
|
+
const fakeModelAPI: typeof ModelAPI = {
|
|
179
|
+
create: createMock,
|
|
180
|
+
getItem: (request: { modelType: unknown }) => {
|
|
181
|
+
return request.modelType === NetworkDevice
|
|
182
|
+
? getDeviceMock(request)
|
|
183
|
+
: getDiagnosticMock(request);
|
|
184
|
+
},
|
|
185
|
+
} as unknown as typeof ModelAPI;
|
|
186
|
+
|
|
187
|
+
function diagnosticRow(
|
|
188
|
+
overrides: Partial<NetworkDeviceDiagnostic>,
|
|
189
|
+
): NetworkDeviceDiagnostic {
|
|
190
|
+
return Object.assign(
|
|
191
|
+
new NetworkDeviceDiagnostic(),
|
|
192
|
+
{ _id: DIAGNOSTIC_ID, status: "Pending" },
|
|
193
|
+
overrides,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function runPing(result: NetworkDeviceDiagnostic): Promise<HTMLElement> {
|
|
198
|
+
createMock.mockResolvedValue({ data: diagnosticRow({}) });
|
|
199
|
+
getDiagnosticMock.mockResolvedValue(result);
|
|
200
|
+
getDeviceMock.mockResolvedValue(
|
|
201
|
+
Object.assign(new NetworkDevice(), {
|
|
202
|
+
_id: DEVICE_ID,
|
|
203
|
+
hostname: "10.0.0.1",
|
|
204
|
+
probeId: new ObjectID(PROBE_ID),
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
render(
|
|
209
|
+
<DeviceDiagnostics
|
|
210
|
+
networkDeviceId={new ObjectID(DEVICE_ID)}
|
|
211
|
+
modelAPI={fakeModelAPI}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
await flush();
|
|
215
|
+
|
|
216
|
+
await act(async () => {
|
|
217
|
+
fireEvent.click(screen.getByTestId("network-device-diagnostic-ping"));
|
|
218
|
+
});
|
|
219
|
+
await act(async () => {
|
|
220
|
+
jest.advanceTimersByTime(DIAGNOSTIC_POLL_INTERVAL_IN_MS);
|
|
221
|
+
});
|
|
222
|
+
await flush();
|
|
223
|
+
|
|
224
|
+
return screen.getByTestId("network-device-diagnostic-result");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const COMPLETED_PING: NetworkDeviceDiagnostic = diagnosticRow({
|
|
228
|
+
status: "Completed",
|
|
229
|
+
hostname: "10.0.0.1",
|
|
230
|
+
pingResult: {
|
|
231
|
+
isOnline: true,
|
|
232
|
+
failureCause: "",
|
|
233
|
+
pingResponse: {
|
|
234
|
+
packetsSent: 5,
|
|
235
|
+
packetsReceived: 4,
|
|
236
|
+
packetLossPercent: 20,
|
|
237
|
+
minRoundTripTimeInMs: 10.2,
|
|
238
|
+
maxRoundTripTimeInMs: 20.1,
|
|
239
|
+
avgRoundTripTimeInMs: 12.44,
|
|
240
|
+
jitterInMs: 1.2,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
} as Partial<NetworkDeviceDiagnostic>);
|
|
244
|
+
|
|
245
|
+
describe("the on-demand ping result", () => {
|
|
246
|
+
const PING_ROWS: Array<[string, NetworkDeviceMetric]> = [
|
|
247
|
+
["Average RTT", "pingAverageRtt"],
|
|
248
|
+
["Min / Max RTT", "pingMinMaxRtt"],
|
|
249
|
+
["Jitter", "pingJitter"],
|
|
250
|
+
["Packet loss", "pingPacketLoss"],
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
test("each statistic row has an (i); the Host row does not", async () => {
|
|
254
|
+
const result: HTMLElement = await runPing(COMPLETED_PING);
|
|
255
|
+
|
|
256
|
+
expect(result).toHaveTextContent("10.0.0.1");
|
|
257
|
+
expect(infoButtonNames(result)).toEqual(
|
|
258
|
+
PING_ROWS.map(([label]: [string, NetworkDeviceMetric]): string => {
|
|
259
|
+
return `About ${label}`;
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test.each(PING_ROWS)(
|
|
265
|
+
"the %s row's (i) reads NETWORK_DEVICE_METRIC_DESCRIPTIONS.%s",
|
|
266
|
+
async (label: string, key: NetworkDeviceMetric) => {
|
|
267
|
+
await runPing(COMPLETED_PING);
|
|
268
|
+
|
|
269
|
+
await expectExplained(label, DESCRIPTIONS[key]);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
test("the (i) sits in the term, beside its label, not in the value", async () => {
|
|
274
|
+
const result: HTMLElement = await runPing(COMPLETED_PING);
|
|
275
|
+
const info: HTMLElement = within(result).getByRole("button", {
|
|
276
|
+
name: "About Jitter",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(info.closest("dt")).toHaveTextContent("Jitter");
|
|
280
|
+
expect(info.closest("dd")).toBeNull();
|
|
281
|
+
// The value still reads as it did.
|
|
282
|
+
expect(result).toHaveTextContent("20% (4/5 received)");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a ping that could not run shows only its Reason, with no (i)", async () => {
|
|
286
|
+
const result: HTMLElement = await runPing(
|
|
287
|
+
diagnosticRow({
|
|
288
|
+
status: "Completed",
|
|
289
|
+
pingResult: {
|
|
290
|
+
isOnline: false,
|
|
291
|
+
failureCause: "ICMP is not usable on this probe.",
|
|
292
|
+
},
|
|
293
|
+
} as Partial<NetworkDeviceDiagnostic>),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(result).toHaveTextContent("ICMP is not usable on this probe.");
|
|
297
|
+
expect(infoButtonNames(result)).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// -------------------------------------------------------- traceroute
|
|
302
|
+
|
|
303
|
+
describe("the traceroute hop table", () => {
|
|
304
|
+
test("only the RTT header has an (i), and it explains the first-reply rule", async () => {
|
|
305
|
+
render(
|
|
306
|
+
<TracerouteHopsTable
|
|
307
|
+
hops={[
|
|
308
|
+
{
|
|
309
|
+
hopNumber: 1,
|
|
310
|
+
address: "10.0.0.1",
|
|
311
|
+
hostName: "gw",
|
|
312
|
+
roundTripTimeInMS: 0.8,
|
|
313
|
+
isTimeout: false,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
hopNumber: 2,
|
|
317
|
+
address: undefined,
|
|
318
|
+
hostName: undefined,
|
|
319
|
+
roundTripTimeInMS: undefined,
|
|
320
|
+
isTimeout: true,
|
|
321
|
+
},
|
|
322
|
+
]}
|
|
323
|
+
/>,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(infoButtonNames()).toEqual(["About RTT"]);
|
|
327
|
+
expect(
|
|
328
|
+
screen.getByRole("button", { name: "About RTT" }).closest("th"),
|
|
329
|
+
).toHaveTextContent("RTT");
|
|
330
|
+
await expectExplained("RTT", DESCRIPTIONS.tracerouteRtt);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("no hops, no table, no (i)", () => {
|
|
334
|
+
const { container } = render(<TracerouteHopsTable hops={[]} />);
|
|
335
|
+
|
|
336
|
+
expect(container).toBeEmptyDOMElement();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ------------------------------------------------------- latency trend
|
|
341
|
+
|
|
342
|
+
function aggregate(
|
|
343
|
+
rows: Array<{ time: string; value: number }>,
|
|
344
|
+
): AggregatedResult {
|
|
345
|
+
return {
|
|
346
|
+
data: rows.map((row: { time: string; value: number }) => {
|
|
347
|
+
return { timestamp: new Date(row.time), value: row.value };
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function renderTrend(): Promise<void> {
|
|
353
|
+
render(
|
|
354
|
+
<MemoryRouter>
|
|
355
|
+
<DeviceLatencyTrend networkDeviceId={new ObjectID(DEVICE_ID)} />
|
|
356
|
+
</MemoryRouter>,
|
|
357
|
+
);
|
|
358
|
+
await flush();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
describe("the latency trend", () => {
|
|
362
|
+
test("the header and the loss line each have an (i)", async () => {
|
|
363
|
+
fetchResultsMock.mockResolvedValue([
|
|
364
|
+
aggregate([
|
|
365
|
+
{ time: "2026-09-15T10:00:00Z", value: 10 },
|
|
366
|
+
{ time: "2026-09-15T10:01:00Z", value: 30 },
|
|
367
|
+
]),
|
|
368
|
+
aggregate([{ time: "2026-09-15T10:01:00Z", value: 50 }]),
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
await renderTrend();
|
|
372
|
+
|
|
373
|
+
expect(infoButtonNames()).toEqual([
|
|
374
|
+
"About Round-trip time, past hour",
|
|
375
|
+
"About Packet loss (1h max)",
|
|
376
|
+
]);
|
|
377
|
+
// The (i) adds no text: the lines read exactly as before.
|
|
378
|
+
expect(
|
|
379
|
+
screen.getByTestId("network-device-latency-summary"),
|
|
380
|
+
).toHaveTextContent("now 30.0 ms · avg 20.0 ms · max 30.0 ms");
|
|
381
|
+
expect(screen.getByTestId("network-device-loss-summary").textContent).toBe(
|
|
382
|
+
"Packet loss (1h max): 50%",
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("the header's (i) explains now / avg / max as per-minute figures", async () => {
|
|
387
|
+
fetchResultsMock.mockResolvedValue([
|
|
388
|
+
aggregate([{ time: "2026-09-15T10:00:00Z", value: 10 }]),
|
|
389
|
+
aggregate([]),
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
await renderTrend();
|
|
393
|
+
|
|
394
|
+
await expectExplained(
|
|
395
|
+
"Round-trip time, past hour",
|
|
396
|
+
DESCRIPTIONS.latencyTrend,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("the loss line's (i) explains the worst minute of the hour", async () => {
|
|
401
|
+
fetchResultsMock.mockResolvedValue([
|
|
402
|
+
aggregate([{ time: "2026-09-15T10:00:00Z", value: 10 }]),
|
|
403
|
+
aggregate([{ time: "2026-09-15T10:00:00Z", value: 0 }]),
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
await renderTrend();
|
|
407
|
+
|
|
408
|
+
await expectExplained("Packet loss (1h max)", DESCRIPTIONS.packetLossPeak);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("with no ping data there is no loss line, but the header keeps its (i)", async () => {
|
|
412
|
+
fetchResultsMock.mockResolvedValue([aggregate([]), aggregate([])]);
|
|
413
|
+
|
|
414
|
+
await renderTrend();
|
|
415
|
+
|
|
416
|
+
expect(
|
|
417
|
+
screen.getByText("No ping data in the last hour."),
|
|
418
|
+
).toBeInTheDocument();
|
|
419
|
+
expect(infoButtonNames()).toEqual(["About Round-trip time, past hour"]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("the (i) is not inside the Open metrics link", async () => {
|
|
423
|
+
fetchResultsMock.mockResolvedValue([aggregate([]), aggregate([])]);
|
|
424
|
+
|
|
425
|
+
await renderTrend();
|
|
426
|
+
|
|
427
|
+
expect(
|
|
428
|
+
screen
|
|
429
|
+
.getByRole("button", { name: "About Round-trip time, past hour" })
|
|
430
|
+
.closest("a"),
|
|
431
|
+
).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ------------------------------------------------------- top talkers
|
|
436
|
+
|
|
437
|
+
describe("FlowStatTile and FlowSectionTitle", () => {
|
|
438
|
+
test("the stat tile shows its value and explains it", async () => {
|
|
439
|
+
render(
|
|
440
|
+
<FlowStatTile
|
|
441
|
+
title="Flows"
|
|
442
|
+
value="1,204"
|
|
443
|
+
description={DESCRIPTIONS.flowCount}
|
|
444
|
+
/>,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
expect(screen.getByText("1,204")).toBeInTheDocument();
|
|
448
|
+
await expectExplained("Flows", DESCRIPTIONS.flowCount);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("the section title explains its block", async () => {
|
|
452
|
+
render(
|
|
453
|
+
<FlowSectionTitle
|
|
454
|
+
title="Bandwidth Over Time"
|
|
455
|
+
description={DESCRIPTIONS.flowBandwidth}
|
|
456
|
+
/>,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
expect(screen.getByText("Bandwidth Over Time")).toBeInTheDocument();
|
|
460
|
+
await expectExplained("Bandwidth Over Time", DESCRIPTIONS.flowBandwidth);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
function topTalkersResponse(): unknown {
|
|
465
|
+
return {
|
|
466
|
+
data: {
|
|
467
|
+
totalOctets: 5 * 1024 * 1024,
|
|
468
|
+
totalPackets: 4200,
|
|
469
|
+
totalFlows: 37,
|
|
470
|
+
topSources: [{ key: "10.0.0.5", octets: 3000, packets: 20 }],
|
|
471
|
+
topDestinations: [{ key: "10.0.0.9", octets: 2000, packets: 10 }],
|
|
472
|
+
topProtocolPorts: [
|
|
473
|
+
{ protocolNumber: 6, destinationPort: 443, octets: 5000, packets: 30 },
|
|
474
|
+
],
|
|
475
|
+
topConversations: [
|
|
476
|
+
{
|
|
477
|
+
sourceIp: "10.0.0.5",
|
|
478
|
+
destinationIp: "10.0.0.9",
|
|
479
|
+
octets: 1000,
|
|
480
|
+
packets: 5,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
series: [{ time: "2026-09-24T11:00:00Z", octets: 60000, packets: 50 }],
|
|
484
|
+
seriesBucketSeconds: 60,
|
|
485
|
+
windowStartAt: "2026-09-24T11:00:00.000Z",
|
|
486
|
+
windowEndAt: "2026-09-24T12:00:00.000Z",
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
describe("the Traffic tab's top talkers", () => {
|
|
492
|
+
const FLOW_TITLES: Array<[string, NetworkDeviceMetric]> = [
|
|
493
|
+
["Total Traffic", "flowTotalTraffic"],
|
|
494
|
+
["Packets", "flowPackets"],
|
|
495
|
+
["Flows", "flowCount"],
|
|
496
|
+
["Bandwidth Over Time", "flowBandwidth"],
|
|
497
|
+
["Top Sources", "flowTopSources"],
|
|
498
|
+
["Top Destinations", "flowTopDestinations"],
|
|
499
|
+
["Top Conversations", "flowTopConversations"],
|
|
500
|
+
["Top Protocols & Ports", "flowTopProtocolsPorts"],
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
test("every total, the chart and every top-N table carry an (i)", async () => {
|
|
504
|
+
apiPostMock.mockResolvedValue(topTalkersResponse());
|
|
505
|
+
|
|
506
|
+
render(<FlowTopTalkers networkDeviceId={new ObjectID(DEVICE_ID)} />);
|
|
507
|
+
await flush();
|
|
508
|
+
|
|
509
|
+
expect(screen.getByText("5.00 MB")).toBeInTheDocument();
|
|
510
|
+
expect(infoButtonNames()).toEqual(
|
|
511
|
+
FLOW_TITLES.map(([title]: [string, NetworkDeviceMetric]): string => {
|
|
512
|
+
return `About ${title}`;
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test.each(FLOW_TITLES)(
|
|
518
|
+
"the %s (i) reads NETWORK_DEVICE_METRIC_DESCRIPTIONS.%s",
|
|
519
|
+
async (title: string, key: NetworkDeviceMetric) => {
|
|
520
|
+
apiPostMock.mockResolvedValue(topTalkersResponse());
|
|
521
|
+
|
|
522
|
+
render(<FlowTopTalkers networkDeviceId={new ObjectID(DEVICE_ID)} />);
|
|
523
|
+
await flush();
|
|
524
|
+
|
|
525
|
+
await expectExplained(title, DESCRIPTIONS[key]);
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
test("no flow data yet: the setup hint, and no numbers to explain", async () => {
|
|
530
|
+
apiPostMock.mockResolvedValue({
|
|
531
|
+
data: { totalOctets: 0, totalPackets: 0, totalFlows: 0 },
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
render(<FlowTopTalkers networkDeviceId={new ObjectID(DEVICE_ID)} />);
|
|
535
|
+
await flush();
|
|
536
|
+
|
|
537
|
+
expect(screen.getByText("No flow data yet.")).toBeInTheDocument();
|
|
538
|
+
expect(infoButtonNames()).toEqual([]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("the query is the card's own window, which the texts call the selected range", async () => {
|
|
542
|
+
apiPostMock.mockResolvedValue(topTalkersResponse());
|
|
543
|
+
|
|
544
|
+
render(<FlowTopTalkers networkDeviceId={new ObjectID(DEVICE_ID)} />);
|
|
545
|
+
await flush();
|
|
546
|
+
|
|
547
|
+
const request: { data: Record<string, unknown> } = apiPostMock.mock
|
|
548
|
+
.calls[0]![0] as { data: Record<string, unknown> };
|
|
549
|
+
|
|
550
|
+
expect(request.data["networkDeviceId"]).toBe(DEVICE_ID);
|
|
551
|
+
expect(request.data["projectId"]).toBe(PROJECT_ID);
|
|
552
|
+
expect(typeof request.data["startTime"]).toBe("string");
|
|
553
|
+
expect(typeof request.data["endTime"]).toBe("string");
|
|
554
|
+
expect(DESCRIPTIONS.flowTotalTraffic).toContain("the selected range");
|
|
555
|
+
});
|
|
556
|
+
});
|