@oneuptime/common 12.0.18 → 12.0.20
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/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +29 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +171 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/Marketing/MarketingEvent.ts +140 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
- package/UI/Components/Charts/Line/LineChart.tsx +8 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +25 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +144 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { ChartTooltip as AreaChartTooltip } from "../../../../UI/Components/Charts/ChartLibrary/AreaChart/AreaChart";
|
|
6
|
+
import { ChartTooltip as BarChartTooltip } from "../../../../UI/Components/Charts/ChartLibrary/BarChart/BarChart";
|
|
7
|
+
import { ChartTooltip as LineChartTooltip } from "../../../../UI/Components/Charts/ChartLibrary/LineChart/LineChart";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The rendered tooltip is where "which host is this spike?" gets answered:
|
|
11
|
+
* with a 20-series group-by chart the entries must arrive highest-value
|
|
12
|
+
* first (recharts hands them over in series-mount order, effectively
|
|
13
|
+
* alphabetical) and the tail past the cap must be summarized, not
|
|
14
|
+
* silently dropped. All three chart types ship their own ChartTooltip, so
|
|
15
|
+
* all three are pinned to the same contract here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type TooltipComponent =
|
|
19
|
+
| typeof AreaChartTooltip
|
|
20
|
+
| typeof LineChartTooltip
|
|
21
|
+
| typeof BarChartTooltip;
|
|
22
|
+
|
|
23
|
+
interface TooltipCase {
|
|
24
|
+
name: string;
|
|
25
|
+
Tooltip: TooltipComponent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TOOLTIP_CASES: Array<TooltipCase> = [
|
|
29
|
+
{ name: "AreaChart", Tooltip: AreaChartTooltip },
|
|
30
|
+
{ name: "LineChart", Tooltip: LineChartTooltip },
|
|
31
|
+
{ name: "BarChart", Tooltip: BarChartTooltip },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function buildPayload(count: number): Array<{
|
|
35
|
+
category: string;
|
|
36
|
+
value: number;
|
|
37
|
+
index: string;
|
|
38
|
+
color: string;
|
|
39
|
+
payload: Record<string, unknown>;
|
|
40
|
+
}> {
|
|
41
|
+
return Array.from({ length: count }, (_: unknown, index: number) => {
|
|
42
|
+
return {
|
|
43
|
+
/*
|
|
44
|
+
* Alphabetical categories with ASCENDING values — exactly the shape
|
|
45
|
+
* where mount order (host-00 first) and value order (host-NN first)
|
|
46
|
+
* disagree, so a pass proves the sort actually ran.
|
|
47
|
+
*/
|
|
48
|
+
category: `host-${String(index).padStart(2, "0")}`,
|
|
49
|
+
value: index,
|
|
50
|
+
index: "10:00",
|
|
51
|
+
color: "blue",
|
|
52
|
+
payload: {},
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
cleanup();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe.each(TOOLTIP_CASES)("$name tooltip", ({ Tooltip }: TooltipCase) => {
|
|
62
|
+
test("renders entries highest-value first with an overflow summary", () => {
|
|
63
|
+
const { container } = render(
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
<Tooltip
|
|
66
|
+
active={true}
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
payload={buildPayload(14) as any}
|
|
69
|
+
label="10:00"
|
|
70
|
+
valueFormatter={(value: number): string => {
|
|
71
|
+
return `${value}%`;
|
|
72
|
+
}}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const text: string = container.textContent || "";
|
|
77
|
+
|
|
78
|
+
// The top-valued series leads; the lowest-valued ones are elided.
|
|
79
|
+
expect(text.indexOf("host-13")).toBeGreaterThan(-1);
|
|
80
|
+
expect(text.indexOf("host-13")).toBeLessThan(text.indexOf("host-12"));
|
|
81
|
+
expect(text.indexOf("host-12")).toBeLessThan(text.indexOf("host-04"));
|
|
82
|
+
expect(text).not.toContain("host-03");
|
|
83
|
+
expect(text).not.toContain("host-00");
|
|
84
|
+
|
|
85
|
+
// 14 series, 10 shown → 4 summarized.
|
|
86
|
+
expect(
|
|
87
|
+
screen.getByText(/\+4 more series — highest values shown/),
|
|
88
|
+
).toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
// Values run through the caller's formatter.
|
|
91
|
+
expect(text).toContain("13%");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("shows no overflow line when everything fits", () => {
|
|
95
|
+
const { container } = render(
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
<Tooltip
|
|
98
|
+
active={true}
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
payload={buildPayload(3) as any}
|
|
101
|
+
label="10:00"
|
|
102
|
+
valueFormatter={(value: number): string => {
|
|
103
|
+
return String(value);
|
|
104
|
+
}}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(container.textContent).not.toContain("more series");
|
|
109
|
+
expect(container.textContent).toContain("host-02");
|
|
110
|
+
expect(container.textContent).toContain("host-00");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("renders nothing when inactive", () => {
|
|
114
|
+
const { container } = render(
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
<Tooltip
|
|
117
|
+
active={false}
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
payload={buildPayload(3) as any}
|
|
120
|
+
label="10:00"
|
|
121
|
+
valueFormatter={(value: number): string => {
|
|
122
|
+
return String(value);
|
|
123
|
+
}}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(container.firstChild).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_TOOLTIP_MAX_ENTRIES,
|
|
4
|
+
PreparedTooltipEntries,
|
|
5
|
+
SortableTooltipItem,
|
|
6
|
+
prepareTooltipEntries,
|
|
7
|
+
} from "../../../../UI/Components/Charts/ChartLibrary/Utils/TooltipEntries";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The tooltip ordering contract: on a grouped chart the series that is
|
|
11
|
+
* spiking AT THE HOVERED TIMESTAMP must be the first tooltip line, the
|
|
12
|
+
* list must stay readable at high cardinality (cap + "+N more"), and
|
|
13
|
+
* non-data entries (hidden series, band fills) must never displace a real
|
|
14
|
+
* reading. The chips already rank by peak; the tooltip is the only place
|
|
15
|
+
* the user reads per-timestamp values, so its order is what answers
|
|
16
|
+
* "which host is this spike?".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface TestItem extends SortableTooltipItem {
|
|
20
|
+
color?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function item(category: string, value: number, type?: string): TestItem {
|
|
24
|
+
return { category, value, ...(type ? { type } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function categories(prepared: PreparedTooltipEntries<TestItem>): Array<string> {
|
|
28
|
+
return prepared.entries.map((entry: TestItem) => {
|
|
29
|
+
return entry.category;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("prepareTooltipEntries", () => {
|
|
34
|
+
test("orders entries by value at the hovered point, highest first", () => {
|
|
35
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
36
|
+
item("host-b", 10),
|
|
37
|
+
item("host-a", 90),
|
|
38
|
+
item("host-c", 40),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
expect(categories(prepared)).toEqual(["host-a", "host-c", "host-b"]);
|
|
42
|
+
expect(prepared.overflowCount).toBe(0);
|
|
43
|
+
expect(prepared.totalCount).toBe(3);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("breaks value ties by natural name order (cpu2 before cpu10)", () => {
|
|
47
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
48
|
+
item("cpu10", 5),
|
|
49
|
+
item("cpu2", 5),
|
|
50
|
+
item("cpu1", 5),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
expect(categories(prepared)).toEqual(["cpu1", "cpu2", "cpu10"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("sorts non-finite values last without dropping them", () => {
|
|
57
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
58
|
+
item("nan-series", NaN),
|
|
59
|
+
item("real-low", 1),
|
|
60
|
+
item("infinite", Infinity),
|
|
61
|
+
item("real-high", 7),
|
|
62
|
+
// The anomaly band's dataKey yields a [low, high] tuple at runtime.
|
|
63
|
+
item("band", [2, 9] as unknown as number),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
expect(categories(prepared).slice(0, 2)).toEqual(["real-high", "real-low"]);
|
|
67
|
+
// Non-finite tail keeps natural name order among itself.
|
|
68
|
+
expect(categories(prepared).slice(2)).toEqual([
|
|
69
|
+
"band",
|
|
70
|
+
"infinite",
|
|
71
|
+
"nan-series",
|
|
72
|
+
]);
|
|
73
|
+
expect(prepared.totalCount).toBe(5);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('filters out type "none" entries (hidden click-target lines, band fills)', () => {
|
|
77
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
78
|
+
item("visible", 3),
|
|
79
|
+
item("hidden", 99, "none"),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
expect(categories(prepared)).toEqual(["visible"]);
|
|
83
|
+
expect(prepared.totalCount).toBe(1);
|
|
84
|
+
expect(prepared.overflowCount).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("caps at the default and reports the overflow", () => {
|
|
88
|
+
const payload: Array<TestItem> = Array.from(
|
|
89
|
+
{ length: 25 },
|
|
90
|
+
(_: unknown, index: number) => {
|
|
91
|
+
return item(`series-${index}`, index);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const prepared: PreparedTooltipEntries<TestItem> =
|
|
96
|
+
prepareTooltipEntries(payload);
|
|
97
|
+
|
|
98
|
+
expect(prepared.entries).toHaveLength(DEFAULT_TOOLTIP_MAX_ENTRIES);
|
|
99
|
+
expect(prepared.overflowCount).toBe(25 - DEFAULT_TOOLTIP_MAX_ENTRIES);
|
|
100
|
+
expect(prepared.totalCount).toBe(25);
|
|
101
|
+
// The cap keeps the HIGHEST values, not the first-mounted series.
|
|
102
|
+
expect(categories(prepared)[0]).toBe("series-24");
|
|
103
|
+
expect(categories(prepared)[DEFAULT_TOOLTIP_MAX_ENTRIES - 1]).toBe(
|
|
104
|
+
`series-${25 - DEFAULT_TOOLTIP_MAX_ENTRIES}`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("exactly at the cap means no overflow", () => {
|
|
109
|
+
const payload: Array<TestItem> = Array.from(
|
|
110
|
+
{ length: DEFAULT_TOOLTIP_MAX_ENTRIES },
|
|
111
|
+
(_: unknown, index: number) => {
|
|
112
|
+
return item(`series-${index}`, index);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const prepared: PreparedTooltipEntries<TestItem> =
|
|
117
|
+
prepareTooltipEntries(payload);
|
|
118
|
+
|
|
119
|
+
expect(prepared.entries).toHaveLength(DEFAULT_TOOLTIP_MAX_ENTRIES);
|
|
120
|
+
expect(prepared.overflowCount).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("honors a custom cap and rejects nonsense caps", () => {
|
|
124
|
+
const payload: Array<TestItem> = [item("a", 1), item("b", 2), item("c", 3)];
|
|
125
|
+
|
|
126
|
+
expect(prepareTooltipEntries(payload, 2).entries).toHaveLength(2);
|
|
127
|
+
expect(prepareTooltipEntries(payload, 2).overflowCount).toBe(1);
|
|
128
|
+
|
|
129
|
+
// Zero, negative, and fractional caps fall back to the default.
|
|
130
|
+
expect(prepareTooltipEntries(payload, 0).entries).toHaveLength(3);
|
|
131
|
+
expect(prepareTooltipEntries(payload, -5).entries).toHaveLength(3);
|
|
132
|
+
expect(prepareTooltipEntries(payload, 1.5).entries).toHaveLength(3);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("tolerates empty, null, and undefined payloads", () => {
|
|
136
|
+
for (const payload of [[], null, undefined]) {
|
|
137
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries(
|
|
138
|
+
payload as Array<TestItem>,
|
|
139
|
+
);
|
|
140
|
+
expect(prepared.entries).toEqual([]);
|
|
141
|
+
expect(prepared.overflowCount).toBe(0);
|
|
142
|
+
expect(prepared.totalCount).toBe(0);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("does not mutate the input payload", () => {
|
|
147
|
+
const payload: Array<TestItem> = [item("low", 1), item("high", 2)];
|
|
148
|
+
prepareTooltipEntries(payload);
|
|
149
|
+
expect(
|
|
150
|
+
payload.map((entry: TestItem) => {
|
|
151
|
+
return entry.category;
|
|
152
|
+
}),
|
|
153
|
+
).toEqual(["low", "high"]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
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 { cleanup, render } from "@testing-library/react";
|
|
11
|
+
import React, { ReactElement } from "react";
|
|
12
|
+
import getJestMockFunction, { MockFunction } from "../../../MockType";
|
|
13
|
+
|
|
14
|
+
const loggerErrorMock: MockFunction = getJestMockFunction();
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The arrow wrapper is load bearing: jest.mock is hoisted above the compiled
|
|
18
|
+
* requires, so loggerErrorMock is still unassigned when the factory runs.
|
|
19
|
+
* Dereferencing it lazily, at call time, is what makes this work.
|
|
20
|
+
*/
|
|
21
|
+
jest.mock("../../../../UI/Utils/Logger", () => {
|
|
22
|
+
return {
|
|
23
|
+
__esModule: true,
|
|
24
|
+
Logger: {
|
|
25
|
+
error: (...args: Array<any>) => {
|
|
26
|
+
return loggerErrorMock(...args);
|
|
27
|
+
},
|
|
28
|
+
warn: () => {
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
log: () => {
|
|
32
|
+
return undefined;
|
|
33
|
+
},
|
|
34
|
+
info: () => {
|
|
35
|
+
return undefined;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
import Detail from "../../../../UI/Components/Detail/Detail";
|
|
42
|
+
import Field from "../../../../UI/Components/Detail/Field";
|
|
43
|
+
import FieldType from "../../../../UI/Components/Types/FieldType";
|
|
44
|
+
import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
|
|
45
|
+
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
46
|
+
import Color from "../../../../Types/Color";
|
|
47
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Detail's getField is a plain closure called from Detail's own render, so a
|
|
51
|
+
* throw out of a field's getElement used to unwind the entire route up to the
|
|
52
|
+
* app level ErrorBoundary - replacing the whole page with "Something went
|
|
53
|
+
* wrong" over one bad cell. That is issue #3374: an Incident Episode with no
|
|
54
|
+
* severity threw "Episode Severity not found" from the severity renderer and
|
|
55
|
+
* took the detail page with it.
|
|
56
|
+
*
|
|
57
|
+
* The two call sites in that issue are fixed at source, but the same shape
|
|
58
|
+
* exists in ~20 other getElement callbacks across the dashboard. These tests
|
|
59
|
+
* pin the framework level guard: a throwing field renderer degrades to
|
|
60
|
+
* whatever the field type already rendered, logs the misconfiguration, and
|
|
61
|
+
* costs exactly one field - never the page.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
interface DetailItem {
|
|
65
|
+
incidentSeverity?: IncidentSeverity | undefined;
|
|
66
|
+
title?: string | undefined;
|
|
67
|
+
description?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type RenderDetailFunction = (
|
|
71
|
+
item: DetailItem,
|
|
72
|
+
fields: Array<Field<DetailItem>>,
|
|
73
|
+
) => HTMLElement;
|
|
74
|
+
|
|
75
|
+
const renderDetail: RenderDetailFunction = (
|
|
76
|
+
item: DetailItem,
|
|
77
|
+
fields: Array<Field<DetailItem>>,
|
|
78
|
+
): HTMLElement => {
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<Detail<DetailItem>
|
|
81
|
+
item={item}
|
|
82
|
+
fields={fields}
|
|
83
|
+
showDetailsInNumberOfColumns={1}
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return container;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type BuildSeverityFunction = (name: string, color?: Color) => IncidentSeverity;
|
|
91
|
+
|
|
92
|
+
const buildSeverity: BuildSeverityFunction = (
|
|
93
|
+
name: string,
|
|
94
|
+
color?: Color,
|
|
95
|
+
): IncidentSeverity => {
|
|
96
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
97
|
+
severity._id = new ObjectID("severity-id").toString();
|
|
98
|
+
severity.name = name;
|
|
99
|
+
|
|
100
|
+
if (color) {
|
|
101
|
+
severity.color = color;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return severity;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type ThrowingFieldFunction = (
|
|
108
|
+
key: keyof DetailItem,
|
|
109
|
+
title: string,
|
|
110
|
+
error: unknown,
|
|
111
|
+
) => Field<DetailItem>;
|
|
112
|
+
|
|
113
|
+
const throwingField: ThrowingFieldFunction = (
|
|
114
|
+
key: keyof DetailItem,
|
|
115
|
+
title: string,
|
|
116
|
+
error: unknown,
|
|
117
|
+
): Field<DetailItem> => {
|
|
118
|
+
return {
|
|
119
|
+
key: key,
|
|
120
|
+
title: title,
|
|
121
|
+
fieldType: FieldType.Element,
|
|
122
|
+
getElement: (): ReactElement => {
|
|
123
|
+
throw error;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const badgeSelector: string = '[data-dropdown-value-badge="true"]';
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
loggerErrorMock.mockClear();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
cleanup();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Detail: a throwing field renderer never takes down the page", () => {
|
|
139
|
+
test("does not rethrow out of render when getElement throws", () => {
|
|
140
|
+
expect(() => {
|
|
141
|
+
renderDetail({ title: "Episode one" }, [
|
|
142
|
+
throwingField("title", "Episode Title", new Error("boom")),
|
|
143
|
+
]);
|
|
144
|
+
}).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("renders the surrounding grid, so the route survives", () => {
|
|
148
|
+
const container: HTMLElement = renderDetail({ title: "Episode one" }, [
|
|
149
|
+
throwingField("title", "Episode Title", new Error("boom")),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
// The label is still rendered - the row exists, only its value is missing.
|
|
153
|
+
expect(container.textContent).toContain("Episode Title");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("costs exactly one field - the siblings still render their values", () => {
|
|
157
|
+
const container: HTMLElement = renderDetail(
|
|
158
|
+
{ title: "Episode one", description: "A long running episode" },
|
|
159
|
+
[
|
|
160
|
+
throwingField("title", "Episode Title", new Error("boom")),
|
|
161
|
+
{
|
|
162
|
+
key: "description",
|
|
163
|
+
title: "Description",
|
|
164
|
+
fieldType: FieldType.Text,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(container.textContent).toContain("Description");
|
|
170
|
+
expect(container.textContent).toContain("A long running episode");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("logs the misconfiguration once, naming the field and the message", () => {
|
|
174
|
+
renderDetail({ title: "Episode one" }, [
|
|
175
|
+
throwingField(
|
|
176
|
+
"title",
|
|
177
|
+
"Episode Title",
|
|
178
|
+
new BadDataException("Episode Severity not found"),
|
|
179
|
+
),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
expect(loggerErrorMock).toHaveBeenCalledTimes(1);
|
|
183
|
+
|
|
184
|
+
const logged: string = String(loggerErrorMock.mock.calls[0]?.[0]);
|
|
185
|
+
|
|
186
|
+
expect(logged).toContain("title");
|
|
187
|
+
expect(logged).toContain("Episode Severity not found");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("logs once per throwing field when several fields throw", () => {
|
|
191
|
+
const container: HTMLElement = renderDetail(
|
|
192
|
+
{ title: "Episode one", description: "A long running episode" },
|
|
193
|
+
[
|
|
194
|
+
throwingField("title", "Episode Title", new Error("first")),
|
|
195
|
+
throwingField("description", "Description", new Error("second")),
|
|
196
|
+
],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(loggerErrorMock).toHaveBeenCalledTimes(2);
|
|
200
|
+
expect(container.textContent).toContain("Episode Title");
|
|
201
|
+
expect(container.textContent).toContain("Description");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("catches a thrown non Error value and still logs it", () => {
|
|
205
|
+
expect(() => {
|
|
206
|
+
renderDetail({ title: "Episode one" }, [
|
|
207
|
+
throwingField("title", "Episode Title", "a bare string"),
|
|
208
|
+
]);
|
|
209
|
+
}).not.toThrow();
|
|
210
|
+
|
|
211
|
+
expect(loggerErrorMock).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(String(loggerErrorMock.mock.calls[0]?.[0])).toContain(
|
|
213
|
+
"a bare string",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("keeps the entity badge the field type already rendered", () => {
|
|
218
|
+
/*
|
|
219
|
+
* data is computed from the field key and transformed by the Entity
|
|
220
|
+
* branch before getElement runs, so swallowing the throw leaves the
|
|
221
|
+
* framework's own rendering of the relation in place.
|
|
222
|
+
*/
|
|
223
|
+
const container: HTMLElement = renderDetail(
|
|
224
|
+
{ incidentSeverity: buildSeverity("Critical", new Color("#ef4444")) },
|
|
225
|
+
[
|
|
226
|
+
{
|
|
227
|
+
key: "incidentSeverity",
|
|
228
|
+
title: "Episode Severity",
|
|
229
|
+
fieldType: FieldType.Entity,
|
|
230
|
+
getElement: (): ReactElement => {
|
|
231
|
+
throw new BadDataException("Episode Severity not found");
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const badge: HTMLElement | null =
|
|
238
|
+
container.querySelector<HTMLElement>(badgeSelector);
|
|
239
|
+
|
|
240
|
+
expect(badge).not.toBeNull();
|
|
241
|
+
expect(badge?.textContent).toContain("Critical");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("falls back to the field placeholder when the relation is absent", () => {
|
|
245
|
+
const container: HTMLElement = renderDetail({}, [
|
|
246
|
+
{
|
|
247
|
+
key: "incidentSeverity",
|
|
248
|
+
title: "Episode Severity",
|
|
249
|
+
fieldType: FieldType.Entity,
|
|
250
|
+
placeholder: "No severity",
|
|
251
|
+
getElement: (): ReactElement => {
|
|
252
|
+
throw new BadDataException("Episode Severity not found");
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
expect(container.textContent).toContain("No severity");
|
|
258
|
+
expect(container.querySelector(badgeSelector)).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("never shows the app level error boundary copy", () => {
|
|
262
|
+
const container: HTMLElement = renderDetail({ title: "Episode one" }, [
|
|
263
|
+
throwingField("title", "Episode Title", new Error("boom")),
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
expect(container.textContent).not.toContain("Something went wrong");
|
|
267
|
+
expect(container.textContent).not.toContain(
|
|
268
|
+
"An unexpected error has occurred",
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Detail: fields that do not throw are untouched by the guard", () => {
|
|
274
|
+
test("renders the element a healthy getElement returns", () => {
|
|
275
|
+
const container: HTMLElement = renderDetail({ title: "Episode one" }, [
|
|
276
|
+
{
|
|
277
|
+
key: "title",
|
|
278
|
+
title: "Episode Title",
|
|
279
|
+
fieldType: FieldType.Element,
|
|
280
|
+
getElement: (item: DetailItem): ReactElement => {
|
|
281
|
+
return <span data-testid="custom">{item.title}</span>;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
expect(
|
|
287
|
+
container.querySelector<HTMLElement>('[data-testid="custom"]')
|
|
288
|
+
?.textContent,
|
|
289
|
+
).toEqual("Episode one");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("does not log anything when no field throws", () => {
|
|
293
|
+
renderDetail({ title: "Episode one" }, [
|
|
294
|
+
{
|
|
295
|
+
key: "title",
|
|
296
|
+
title: "Episode Title",
|
|
297
|
+
fieldType: FieldType.Text,
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
expect(loggerErrorMock).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("getElement still wins over the value the field type computed", () => {
|
|
305
|
+
const container: HTMLElement = renderDetail(
|
|
306
|
+
{ incidentSeverity: buildSeverity("Critical", new Color("#ef4444")) },
|
|
307
|
+
[
|
|
308
|
+
{
|
|
309
|
+
key: "incidentSeverity",
|
|
310
|
+
title: "Episode Severity",
|
|
311
|
+
fieldType: FieldType.Entity,
|
|
312
|
+
getElement: (): ReactElement => {
|
|
313
|
+
return <span data-testid="custom">Handled</span>;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(container.querySelector('[data-testid="custom"]')).not.toBeNull();
|
|
320
|
+
expect(container.querySelector(badgeSelector)).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("a getElement that returns a placeholder dash renders the dash", () => {
|
|
324
|
+
/*
|
|
325
|
+
* The shape both episode detail pages now use for an absent severity or
|
|
326
|
+
* state. It must survive the guard untouched and must not log.
|
|
327
|
+
*/
|
|
328
|
+
const container: HTMLElement = renderDetail({}, [
|
|
329
|
+
{
|
|
330
|
+
key: "incidentSeverity",
|
|
331
|
+
title: "Episode Severity",
|
|
332
|
+
fieldType: FieldType.Entity,
|
|
333
|
+
getElement: (item: DetailItem): ReactElement => {
|
|
334
|
+
if (!item["incidentSeverity"]) {
|
|
335
|
+
return <>-</>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return <span data-testid="custom">present</span>;
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
expect(container.textContent).toContain("Episode Severity");
|
|
344
|
+
expect(container.textContent).toContain("-");
|
|
345
|
+
expect(loggerErrorMock).not.toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
});
|