@oneuptime/common 12.0.34 → 13.0.0
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/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
|
@@ -0,0 +1,975 @@
|
|
|
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
|
+
cleanup,
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
screen,
|
|
15
|
+
waitFor,
|
|
16
|
+
within,
|
|
17
|
+
} from "@testing-library/react";
|
|
18
|
+
import React from "react";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* The panel every calendar-feed card shows before a link exists.
|
|
23
|
+
*
|
|
24
|
+
* The state used to be a paragraph of prose with a button under it, and the
|
|
25
|
+
* paragraph was the only place the reader was told that the URL is the
|
|
26
|
+
* credential. What this file guards:
|
|
27
|
+
*
|
|
28
|
+
* - the panel names the state, says what the button will do, and lists what
|
|
29
|
+
* the link will contain - each point its own line, so none of it depends
|
|
30
|
+
* on somebody reading a four-line sentence to the end;
|
|
31
|
+
* - the privacy caution is on screen BEFORE the link is minted, which is
|
|
32
|
+
* the only moment it can change what the reader does;
|
|
33
|
+
* - the action lives inside the panel whoever may take it - a Generate
|
|
34
|
+
* button, a Publish button, a disabled button naming the missing
|
|
35
|
+
* permission, an "ask an editor" note, or the plan gate;
|
|
36
|
+
* - the server's deployment warnings stay ABOVE the panel, where they are
|
|
37
|
+
* read before the click rather than after it;
|
|
38
|
+
* - and every panel namespaces its test ids, because the schedule page puts
|
|
39
|
+
* two of them on one screen.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
let translations: Record<string, string> = {};
|
|
43
|
+
let permissionsForTest: Array<unknown> = [];
|
|
44
|
+
|
|
45
|
+
const getMock: MockFunction = getJestMockFunction();
|
|
46
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
47
|
+
|
|
48
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
49
|
+
return {
|
|
50
|
+
__esModule: true,
|
|
51
|
+
default: {
|
|
52
|
+
get: (...args: Array<any>) => {
|
|
53
|
+
return getMock(...args);
|
|
54
|
+
},
|
|
55
|
+
post: (...args: Array<any>) => {
|
|
56
|
+
return postMock(...args);
|
|
57
|
+
},
|
|
58
|
+
getFriendlyMessage: (error: unknown): string => {
|
|
59
|
+
if (
|
|
60
|
+
error &&
|
|
61
|
+
typeof error === "object" &&
|
|
62
|
+
"message" in (error as Record<string, unknown>)
|
|
63
|
+
) {
|
|
64
|
+
return String((error as Record<string, unknown>)["message"]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return "Something went wrong";
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
74
|
+
return {
|
|
75
|
+
__esModule: true,
|
|
76
|
+
default: {
|
|
77
|
+
getCommonHeaders: (): Record<string, string> => {
|
|
78
|
+
return {};
|
|
79
|
+
},
|
|
80
|
+
updateById: async (): Promise<void> => {
|
|
81
|
+
return undefined;
|
|
82
|
+
},
|
|
83
|
+
deleteItem: async (): Promise<void> => {
|
|
84
|
+
return undefined;
|
|
85
|
+
},
|
|
86
|
+
getList: async (): Promise<Record<string, unknown>> => {
|
|
87
|
+
return { data: [], count: 0, skip: 0, limit: 10 };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
|
|
94
|
+
return {
|
|
95
|
+
__esModule: true,
|
|
96
|
+
default: (): unknown => {
|
|
97
|
+
return React.createElement("div", {
|
|
98
|
+
"data-testid": "card-model-detail-stub",
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
jest.mock("../../../UI/Utils/User", () => {
|
|
105
|
+
return {
|
|
106
|
+
__esModule: true,
|
|
107
|
+
default: {
|
|
108
|
+
isMasterAdmin: (): boolean => {
|
|
109
|
+
return false;
|
|
110
|
+
},
|
|
111
|
+
getUserId: (): null => {
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
jest.mock("../../../UI/Utils/Permission", () => {
|
|
119
|
+
return {
|
|
120
|
+
__esModule: true,
|
|
121
|
+
default: {
|
|
122
|
+
getAllPermissions: (): Array<unknown> => {
|
|
123
|
+
return permissionsForTest;
|
|
124
|
+
},
|
|
125
|
+
getProjectPermissions: (): null => {
|
|
126
|
+
return null;
|
|
127
|
+
},
|
|
128
|
+
getGlobalPermissions: (): { globalPermissions: Array<unknown> } => {
|
|
129
|
+
return { globalPermissions: [...permissionsForTest] };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
* A real lookup, not the identity stub the other feed tests use: the panel is
|
|
137
|
+
* new copy, and a string that skipped translateString would render in English
|
|
138
|
+
* for every non-English reader without anything failing.
|
|
139
|
+
*/
|
|
140
|
+
jest.mock("react-i18next", () => {
|
|
141
|
+
return {
|
|
142
|
+
useTranslation: () => {
|
|
143
|
+
return {
|
|
144
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
145
|
+
return translations[key] ?? options?.defaultValue ?? key;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
153
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
154
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
155
|
+
import Permission from "../../../Types/Permission";
|
|
156
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
157
|
+
import FeedEmptyState, {
|
|
158
|
+
FeedEmptyStatePoint,
|
|
159
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/FeedEmptyState";
|
|
160
|
+
import {
|
|
161
|
+
PERSONAL_FEED_EMPTY_DESCRIPTION,
|
|
162
|
+
PERSONAL_FEED_EMPTY_POINT_COVERING,
|
|
163
|
+
PERSONAL_FEED_EMPTY_POINT_PRIVATE,
|
|
164
|
+
PERSONAL_FEED_EMPTY_POINT_SHIFTS,
|
|
165
|
+
PERSONAL_FEED_EMPTY_TITLE,
|
|
166
|
+
PERSONAL_FEED_ROTATE_PATH,
|
|
167
|
+
SHARED_FEED_EMPTY_TITLE,
|
|
168
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedUtil";
|
|
169
|
+
import PersonalCalendarFeedCard, {
|
|
170
|
+
PersonalCalendarFeedVariant,
|
|
171
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/PersonalCalendarFeedCard";
|
|
172
|
+
import SharedCalendarFeedCard, {
|
|
173
|
+
SharedCalendarFeedKind,
|
|
174
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/SharedCalendarFeedCard";
|
|
175
|
+
|
|
176
|
+
const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
|
|
177
|
+
const FEED_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
178
|
+
const SCHEDULE_ID: ObjectID = new ObjectID(
|
|
179
|
+
"33333333-3333-4333-8333-333333333333",
|
|
180
|
+
);
|
|
181
|
+
const NOW: Date = new Date("2026-08-31T12:00:00.000Z");
|
|
182
|
+
|
|
183
|
+
const HTTPS_URL: string =
|
|
184
|
+
"https://oneuptime.example.com/api/on-call-calendar/user/abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG/shifts.ics";
|
|
185
|
+
|
|
186
|
+
const EMPTY_STATUS_JSON: JSONObject = {
|
|
187
|
+
exists: false,
|
|
188
|
+
feedId: null,
|
|
189
|
+
isEnabled: false,
|
|
190
|
+
needsRegeneration: false,
|
|
191
|
+
tokenHint: null,
|
|
192
|
+
rotatedAt: null,
|
|
193
|
+
previousTokenExpiresAt: null,
|
|
194
|
+
lastFetchedAt: null,
|
|
195
|
+
lastFetchedClient: null,
|
|
196
|
+
fetchCount: 0,
|
|
197
|
+
lastRenderTruncated: false,
|
|
198
|
+
settings: { pastDays: 2, futureDays: 90 },
|
|
199
|
+
urls: null,
|
|
200
|
+
hostWarning: null,
|
|
201
|
+
protocolWarning: null,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const ACTIVE_STATUS_JSON: JSONObject = {
|
|
205
|
+
...EMPTY_STATUS_JSON,
|
|
206
|
+
exists: true,
|
|
207
|
+
feedId: FEED_ID,
|
|
208
|
+
isEnabled: true,
|
|
209
|
+
tokenHint: "k3Qx",
|
|
210
|
+
rotatedAt: "2026-08-01T10:00:00.000Z",
|
|
211
|
+
urls: {
|
|
212
|
+
https: HTTPS_URL,
|
|
213
|
+
webcal: HTTPS_URL.replace("https:", "webcals:"),
|
|
214
|
+
googleAdd: `https://calendar.google.com/calendar/r?cid=${encodeURIComponent(
|
|
215
|
+
HTTPS_URL,
|
|
216
|
+
)}`,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type OkFunction = (json: JSONObject) => HTTPResponse<JSONObject>;
|
|
221
|
+
|
|
222
|
+
const ok: OkFunction = (json: JSONObject): HTTPResponse<JSONObject> => {
|
|
223
|
+
return new HTTPResponse<JSONObject>(200, json, {});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function resetMocks(): void {
|
|
227
|
+
translations = {};
|
|
228
|
+
permissionsForTest = [];
|
|
229
|
+
getMock.mockReset();
|
|
230
|
+
postMock.mockReset();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function goToProjectPage(): void {
|
|
234
|
+
window.history.pushState({}, "", `/dashboard/${PROJECT_ID}/on-call-duty`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type IsBeforeFunction = (first: HTMLElement, second: HTMLElement) => boolean;
|
|
238
|
+
|
|
239
|
+
/** True when `first` comes before `second` in document order. */
|
|
240
|
+
const isBefore: IsBeforeFunction = (
|
|
241
|
+
first: HTMLElement,
|
|
242
|
+
second: HTMLElement,
|
|
243
|
+
): boolean => {
|
|
244
|
+
return Boolean(
|
|
245
|
+
first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
type ActionFunction = (label: string) => React.ReactElement;
|
|
250
|
+
|
|
251
|
+
const action: ActionFunction = (label: string): React.ReactElement => {
|
|
252
|
+
return <button data-testid="panel-action">{label}</button>;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
describe("FeedEmptyState", () => {
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
resetMocks();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
cleanup();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("names the state, says what the action does and hosts the action itself", () => {
|
|
265
|
+
render(
|
|
266
|
+
<FeedEmptyState
|
|
267
|
+
idPrefix="feed"
|
|
268
|
+
title="No calendar link yet"
|
|
269
|
+
description="Generate one, then subscribe from your calendar app."
|
|
270
|
+
control={action("Generate")}
|
|
271
|
+
/>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(screen.getByTestId("feed-empty-state")).toBeInTheDocument();
|
|
275
|
+
expect(screen.getByTestId("feed-empty-title")).toHaveTextContent(
|
|
276
|
+
"No calendar link yet",
|
|
277
|
+
);
|
|
278
|
+
expect(screen.getByTestId("feed-empty-description")).toHaveTextContent(
|
|
279
|
+
"Generate one, then subscribe from your calendar app.",
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// The action is INSIDE the panel, not loose underneath it.
|
|
283
|
+
expect(
|
|
284
|
+
within(screen.getByTestId("feed-empty-control")).getByTestId(
|
|
285
|
+
"panel-action",
|
|
286
|
+
),
|
|
287
|
+
).toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("no bullet list at all when there is nothing to list", () => {
|
|
291
|
+
const { rerender } = render(
|
|
292
|
+
<FeedEmptyState
|
|
293
|
+
idPrefix="feed"
|
|
294
|
+
title="Title"
|
|
295
|
+
description="Description"
|
|
296
|
+
control={action("Go")}
|
|
297
|
+
/>,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(screen.queryByTestId("feed-empty-points")).not.toBeInTheDocument();
|
|
301
|
+
|
|
302
|
+
// An empty array is the same as no array - not an empty <ul>.
|
|
303
|
+
rerender(
|
|
304
|
+
<FeedEmptyState
|
|
305
|
+
idPrefix="feed"
|
|
306
|
+
title="Title"
|
|
307
|
+
description="Description"
|
|
308
|
+
points={[]}
|
|
309
|
+
control={action("Go")}
|
|
310
|
+
/>,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(screen.queryByTestId("feed-empty-points")).not.toBeInTheDocument();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("renders one bullet per point, in the order given", () => {
|
|
317
|
+
const points: Array<FeedEmptyStatePoint> = [
|
|
318
|
+
{ icon: IconProp.Check, text: "First point" },
|
|
319
|
+
{ icon: IconProp.Check, text: "Second point" },
|
|
320
|
+
{ icon: IconProp.Lock, text: "Third point" },
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
render(
|
|
324
|
+
<FeedEmptyState
|
|
325
|
+
idPrefix="feed"
|
|
326
|
+
title="Title"
|
|
327
|
+
description="Description"
|
|
328
|
+
points={points}
|
|
329
|
+
control={action("Go")}
|
|
330
|
+
/>,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const list: HTMLElement = screen.getByTestId("feed-empty-points");
|
|
334
|
+
expect(within(list).getAllByRole("listitem")).toHaveLength(3);
|
|
335
|
+
|
|
336
|
+
expect(screen.getByTestId("feed-empty-point-0")).toHaveTextContent(
|
|
337
|
+
"First point",
|
|
338
|
+
);
|
|
339
|
+
expect(screen.getByTestId("feed-empty-point-1")).toHaveTextContent(
|
|
340
|
+
"Second point",
|
|
341
|
+
);
|
|
342
|
+
expect(screen.getByTestId("feed-empty-point-2")).toHaveTextContent(
|
|
343
|
+
"Third point",
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
/*
|
|
348
|
+
* The ticks and the caution have to read differently at a glance: three
|
|
349
|
+
* identical grey glyphs would make "treat it like a password" look like
|
|
350
|
+
* another feature bullet.
|
|
351
|
+
*/
|
|
352
|
+
test("a point keeps its own icon colour, and falls back to the muted default", () => {
|
|
353
|
+
render(
|
|
354
|
+
<FeedEmptyState
|
|
355
|
+
idPrefix="feed"
|
|
356
|
+
title="Title"
|
|
357
|
+
description="Description"
|
|
358
|
+
points={[
|
|
359
|
+
{
|
|
360
|
+
icon: IconProp.Check,
|
|
361
|
+
text: "Included",
|
|
362
|
+
iconClassName: "text-indigo-600",
|
|
363
|
+
},
|
|
364
|
+
{ icon: IconProp.Lock, text: "Caution" },
|
|
365
|
+
]}
|
|
366
|
+
control={action("Go")}
|
|
367
|
+
/>,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(screen.getByTestId("feed-empty-point-icon-0")).toHaveClass(
|
|
371
|
+
"text-indigo-600",
|
|
372
|
+
);
|
|
373
|
+
expect(screen.getByTestId("feed-empty-point-icon-0")).not.toHaveClass(
|
|
374
|
+
"text-gray-400",
|
|
375
|
+
);
|
|
376
|
+
expect(screen.getByTestId("feed-empty-point-icon-1")).toHaveClass(
|
|
377
|
+
"text-gray-400",
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("the badge is a calendar unless the caller asks for another icon", () => {
|
|
382
|
+
const { unmount } = render(
|
|
383
|
+
<FeedEmptyState
|
|
384
|
+
idPrefix="feed"
|
|
385
|
+
title="Title"
|
|
386
|
+
description="Description"
|
|
387
|
+
control={action("Go")}
|
|
388
|
+
/>,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const defaultBadge: string =
|
|
392
|
+
screen.getByTestId("feed-empty-icon").innerHTML;
|
|
393
|
+
unmount();
|
|
394
|
+
|
|
395
|
+
render(
|
|
396
|
+
<FeedEmptyState
|
|
397
|
+
idPrefix="feed"
|
|
398
|
+
title="Title"
|
|
399
|
+
description="Description"
|
|
400
|
+
icon={IconProp.Calendar}
|
|
401
|
+
control={action("Go")}
|
|
402
|
+
/>,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
expect(screen.getByTestId("feed-empty-icon").innerHTML).toBe(defaultBadge);
|
|
406
|
+
cleanup();
|
|
407
|
+
|
|
408
|
+
render(
|
|
409
|
+
<FeedEmptyState
|
|
410
|
+
idPrefix="feed"
|
|
411
|
+
title="Title"
|
|
412
|
+
description="Description"
|
|
413
|
+
icon={IconProp.Team}
|
|
414
|
+
control={action("Go")}
|
|
415
|
+
/>,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(screen.getByTestId("feed-empty-icon").innerHTML).not.toBe(
|
|
419
|
+
defaultBadge,
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("every visible string goes through the translator, bullets included", () => {
|
|
424
|
+
translations = {
|
|
425
|
+
"No calendar link yet": "Noch kein Kalenderlink",
|
|
426
|
+
"Generate a private link.": "Erzeugen Sie einen privaten Link.",
|
|
427
|
+
"Every shift you hold.": "Jede Schicht, die Sie haben.",
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
render(
|
|
431
|
+
<FeedEmptyState
|
|
432
|
+
idPrefix="feed"
|
|
433
|
+
title="No calendar link yet"
|
|
434
|
+
description="Generate a private link."
|
|
435
|
+
points={[{ icon: IconProp.Check, text: "Every shift you hold." }]}
|
|
436
|
+
control={action("Go")}
|
|
437
|
+
/>,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(screen.getByTestId("feed-empty-title")).toHaveTextContent(
|
|
441
|
+
"Noch kein Kalenderlink",
|
|
442
|
+
);
|
|
443
|
+
expect(screen.getByTestId("feed-empty-description")).toHaveTextContent(
|
|
444
|
+
"Erzeugen Sie einen privaten Link.",
|
|
445
|
+
);
|
|
446
|
+
expect(screen.getByTestId("feed-empty-point-0")).toHaveTextContent(
|
|
447
|
+
"Jede Schicht, die Sie haben.",
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
/*
|
|
452
|
+
* The schedule page renders the personal panel and the shared panel one
|
|
453
|
+
* above the other. Un-namespaced ids would make getByTestId ambiguous and,
|
|
454
|
+
* worse, make a passing assertion about the wrong panel.
|
|
455
|
+
*/
|
|
456
|
+
test("two panels on one page keep entirely distinct test ids", () => {
|
|
457
|
+
render(
|
|
458
|
+
<div>
|
|
459
|
+
<FeedEmptyState
|
|
460
|
+
idPrefix="mine"
|
|
461
|
+
title="Mine"
|
|
462
|
+
description="My link"
|
|
463
|
+
points={[{ icon: IconProp.Check, text: "My point" }]}
|
|
464
|
+
control={action("Generate")}
|
|
465
|
+
/>
|
|
466
|
+
<FeedEmptyState
|
|
467
|
+
idPrefix="theirs"
|
|
468
|
+
title="Theirs"
|
|
469
|
+
description="Team link"
|
|
470
|
+
points={[{ icon: IconProp.Check, text: "Their point" }]}
|
|
471
|
+
control={action("Publish")}
|
|
472
|
+
/>
|
|
473
|
+
</div>,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(screen.getByTestId("mine-empty-title")).toHaveTextContent("Mine");
|
|
477
|
+
expect(screen.getByTestId("theirs-empty-title")).toHaveTextContent(
|
|
478
|
+
"Theirs",
|
|
479
|
+
);
|
|
480
|
+
expect(screen.getByTestId("mine-empty-point-0")).toHaveTextContent(
|
|
481
|
+
"My point",
|
|
482
|
+
);
|
|
483
|
+
expect(screen.getByTestId("theirs-empty-point-0")).toHaveTextContent(
|
|
484
|
+
"Their point",
|
|
485
|
+
);
|
|
486
|
+
expect(screen.getAllByTestId("panel-action")).toHaveLength(2);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("PersonalCalendarFeedCard empty state (settings page)", () => {
|
|
491
|
+
beforeEach(() => {
|
|
492
|
+
resetMocks();
|
|
493
|
+
goToProjectPage();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
afterEach(() => {
|
|
497
|
+
cleanup();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("names the state, says what Generate does and lists what the link will hold", async () => {
|
|
501
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
502
|
+
|
|
503
|
+
render(
|
|
504
|
+
<PersonalCalendarFeedCard
|
|
505
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
506
|
+
now={NOW}
|
|
507
|
+
/>,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await waitFor(() => {
|
|
511
|
+
expect(
|
|
512
|
+
screen.getByTestId("personal-calendar-feed-empty-state"),
|
|
513
|
+
).toBeInTheDocument();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
screen.getByTestId("personal-calendar-feed-empty-title"),
|
|
518
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_TITLE);
|
|
519
|
+
expect(
|
|
520
|
+
screen.getByTestId("personal-calendar-feed-empty-description"),
|
|
521
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_DESCRIPTION);
|
|
522
|
+
|
|
523
|
+
expect(
|
|
524
|
+
screen.getByTestId("personal-calendar-feed-empty-point-0"),
|
|
525
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_POINT_SHIFTS);
|
|
526
|
+
expect(
|
|
527
|
+
screen.getByTestId("personal-calendar-feed-empty-point-1"),
|
|
528
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_POINT_COVERING);
|
|
529
|
+
expect(
|
|
530
|
+
screen.getByTestId("personal-calendar-feed-empty-point-2"),
|
|
531
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_POINT_PRIVATE);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
/*
|
|
535
|
+
* The URL is the credential: anyone holding it reads the shifts. Saying so
|
|
536
|
+
* after the link is minted and pasted into a shared calendar is too late,
|
|
537
|
+
* so the caution has to be part of the state that offers to mint it.
|
|
538
|
+
*/
|
|
539
|
+
test("the reader is told the link is a secret BEFORE minting one", async () => {
|
|
540
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
541
|
+
|
|
542
|
+
render(
|
|
543
|
+
<PersonalCalendarFeedCard
|
|
544
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
545
|
+
now={NOW}
|
|
546
|
+
/>,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
await waitFor(() => {
|
|
550
|
+
expect(
|
|
551
|
+
screen.getByTestId("personal-calendar-feed-empty-state"),
|
|
552
|
+
).toBeInTheDocument();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const caution: HTMLElement = screen.getByTestId(
|
|
556
|
+
"personal-calendar-feed-empty-point-2",
|
|
557
|
+
);
|
|
558
|
+
expect(caution).toHaveTextContent(/private to you/i);
|
|
559
|
+
expect(caution).toHaveTextContent(/like a password/i);
|
|
560
|
+
expect(
|
|
561
|
+
isBefore(caution, screen.getByTestId("personal-calendar-feed-generate")),
|
|
562
|
+
).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("Generate lives inside the panel and still posts to /feed/rotate", async () => {
|
|
566
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
567
|
+
postMock.mockResolvedValue(ok(ACTIVE_STATUS_JSON));
|
|
568
|
+
|
|
569
|
+
render(
|
|
570
|
+
<PersonalCalendarFeedCard
|
|
571
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
572
|
+
now={NOW}
|
|
573
|
+
/>,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
await waitFor(() => {
|
|
577
|
+
expect(
|
|
578
|
+
screen.getByTestId("personal-calendar-feed-empty-control"),
|
|
579
|
+
).toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const control: HTMLElement = screen.getByTestId(
|
|
583
|
+
"personal-calendar-feed-empty-control",
|
|
584
|
+
);
|
|
585
|
+
fireEvent.click(
|
|
586
|
+
within(control).getByTestId("personal-calendar-feed-generate"),
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
await waitFor(() => {
|
|
590
|
+
expect(
|
|
591
|
+
screen.getByTestId("personal-calendar-feed-active"),
|
|
592
|
+
).toBeInTheDocument();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(
|
|
596
|
+
String((postMock.mock.calls[0]![0] as Record<string, unknown>)["url"]),
|
|
597
|
+
).toContain(PERSONAL_FEED_ROTATE_PATH);
|
|
598
|
+
|
|
599
|
+
// The panel is for the absent link only; it goes when the link arrives.
|
|
600
|
+
expect(
|
|
601
|
+
screen.queryByTestId("personal-calendar-feed-empty-state"),
|
|
602
|
+
).not.toBeInTheDocument();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
* "This link will be unreachable" is only actionable before the click. The
|
|
607
|
+
* warnings are Alerts, and an Alert buried under three bullets inside a
|
|
608
|
+
* grey panel is an Alert nobody reads.
|
|
609
|
+
*/
|
|
610
|
+
test("the deployment warnings sit above the panel, and Generate is still offered", async () => {
|
|
611
|
+
getMock.mockResolvedValue(
|
|
612
|
+
ok({
|
|
613
|
+
...EMPTY_STATUS_JSON,
|
|
614
|
+
hostWarning: "HOST is not set, so the link points at localhost.",
|
|
615
|
+
protocolWarning: "HTTP_PROTOCOL is http, so the link is not encrypted.",
|
|
616
|
+
}),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
render(
|
|
620
|
+
<PersonalCalendarFeedCard
|
|
621
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
622
|
+
now={NOW}
|
|
623
|
+
/>,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await waitFor(() => {
|
|
627
|
+
expect(
|
|
628
|
+
screen.getByTestId("personal-calendar-feed-empty-state"),
|
|
629
|
+
).toBeInTheDocument();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const panel: HTMLElement = screen.getByTestId(
|
|
633
|
+
"personal-calendar-feed-empty-state",
|
|
634
|
+
);
|
|
635
|
+
const host: HTMLElement = screen.getByTestId(
|
|
636
|
+
"personal-calendar-feed-host-warning",
|
|
637
|
+
);
|
|
638
|
+
const protocol: HTMLElement = screen.getByTestId(
|
|
639
|
+
"personal-calendar-feed-protocol-warning",
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
expect(isBefore(host, panel)).toBe(true);
|
|
643
|
+
expect(isBefore(protocol, panel)).toBe(true);
|
|
644
|
+
expect(panel).not.toContainElement(host);
|
|
645
|
+
expect(panel).not.toContainElement(protocol);
|
|
646
|
+
|
|
647
|
+
// The warning informs the click; it does not take the button away.
|
|
648
|
+
expect(
|
|
649
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
650
|
+
).toBeInTheDocument();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("a failed Generate leaves the panel standing and shows the reason", async () => {
|
|
654
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
655
|
+
postMock.mockRejectedValue(new Error("Feeds are disabled on this server."));
|
|
656
|
+
|
|
657
|
+
render(
|
|
658
|
+
<PersonalCalendarFeedCard
|
|
659
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
660
|
+
now={NOW}
|
|
661
|
+
/>,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
await waitFor(() => {
|
|
665
|
+
expect(
|
|
666
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
667
|
+
).toBeInTheDocument();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
fireEvent.click(screen.getByTestId("personal-calendar-feed-generate"));
|
|
671
|
+
|
|
672
|
+
await waitFor(() => {
|
|
673
|
+
expect(
|
|
674
|
+
screen.getByText("Feeds are disabled on this server."),
|
|
675
|
+
).toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
expect(
|
|
679
|
+
screen.getByTestId("personal-calendar-feed-empty-state"),
|
|
680
|
+
).toBeInTheDocument();
|
|
681
|
+
expect(
|
|
682
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
683
|
+
).toBeInTheDocument();
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("PersonalCalendarFeedCard empty state (schedule page)", () => {
|
|
688
|
+
beforeEach(() => {
|
|
689
|
+
resetMocks();
|
|
690
|
+
goToProjectPage();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
afterEach(() => {
|
|
694
|
+
cleanup();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
/*
|
|
698
|
+
* The schedule page is half a card inside somebody else's page. It gets the
|
|
699
|
+
* same panel so the two halves match, but the three lines of "what's in it"
|
|
700
|
+
* stay on the settings page the reader went to for the feature.
|
|
701
|
+
*/
|
|
702
|
+
test("the narrow variant shows the same panel without the bullet list", async () => {
|
|
703
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
704
|
+
|
|
705
|
+
render(
|
|
706
|
+
<PersonalCalendarFeedCard
|
|
707
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
708
|
+
scheduleId={SCHEDULE_ID}
|
|
709
|
+
now={NOW}
|
|
710
|
+
/>,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
await waitFor(() => {
|
|
714
|
+
expect(
|
|
715
|
+
screen.getByTestId("schedule-personal-calendar-feed-empty-state"),
|
|
716
|
+
).toBeInTheDocument();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
expect(
|
|
720
|
+
screen.getByTestId("schedule-personal-calendar-feed-empty-title"),
|
|
721
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_TITLE);
|
|
722
|
+
expect(
|
|
723
|
+
screen.getByTestId("schedule-personal-calendar-feed-empty-description"),
|
|
724
|
+
).toHaveTextContent(PERSONAL_FEED_EMPTY_DESCRIPTION);
|
|
725
|
+
expect(
|
|
726
|
+
screen.queryByTestId("schedule-personal-calendar-feed-empty-points"),
|
|
727
|
+
).not.toBeInTheDocument();
|
|
728
|
+
|
|
729
|
+
expect(
|
|
730
|
+
within(
|
|
731
|
+
screen.getByTestId("schedule-personal-calendar-feed-empty-control"),
|
|
732
|
+
).getByTestId("schedule-personal-calendar-feed-generate"),
|
|
733
|
+
).toBeInTheDocument();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("its test ids never collide with the settings page's", async () => {
|
|
737
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
738
|
+
|
|
739
|
+
render(
|
|
740
|
+
<div>
|
|
741
|
+
<PersonalCalendarFeedCard
|
|
742
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
743
|
+
now={NOW}
|
|
744
|
+
/>
|
|
745
|
+
<PersonalCalendarFeedCard
|
|
746
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
747
|
+
scheduleId={SCHEDULE_ID}
|
|
748
|
+
now={NOW}
|
|
749
|
+
/>
|
|
750
|
+
</div>,
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
await waitFor(() => {
|
|
754
|
+
expect(
|
|
755
|
+
screen.getByTestId("schedule-personal-calendar-feed-empty-state"),
|
|
756
|
+
).toBeInTheDocument();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Both panels are on screen, and each id resolves to exactly one of them.
|
|
760
|
+
expect(
|
|
761
|
+
screen.getAllByTestId("personal-calendar-feed-empty-state"),
|
|
762
|
+
).toHaveLength(1);
|
|
763
|
+
expect(
|
|
764
|
+
screen.getAllByTestId("schedule-personal-calendar-feed-empty-state"),
|
|
765
|
+
).toHaveLength(1);
|
|
766
|
+
// Only the settings-page panel carries the bullets.
|
|
767
|
+
expect(
|
|
768
|
+
screen.getAllByTestId("personal-calendar-feed-empty-points"),
|
|
769
|
+
).toHaveLength(1);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
describe("SharedCalendarFeedCard empty state", () => {
|
|
774
|
+
beforeEach(() => {
|
|
775
|
+
resetMocks();
|
|
776
|
+
goToProjectPage();
|
|
777
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
afterEach(() => {
|
|
781
|
+
cleanup();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("a schedule with no shared link says who the link is for and offers Publish inside the panel", async () => {
|
|
785
|
+
permissionsForTest = [Permission.ProjectAdmin];
|
|
786
|
+
|
|
787
|
+
render(
|
|
788
|
+
<SharedCalendarFeedCard
|
|
789
|
+
kind={SharedCalendarFeedKind.Schedule}
|
|
790
|
+
scheduleId={SCHEDULE_ID}
|
|
791
|
+
scheduleTimezone="Europe/Stockholm"
|
|
792
|
+
now={NOW}
|
|
793
|
+
/>,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
await waitFor(() => {
|
|
797
|
+
expect(
|
|
798
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-state"),
|
|
799
|
+
).toBeInTheDocument();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
expect(
|
|
803
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-title"),
|
|
804
|
+
).toHaveTextContent(SHARED_FEED_EMPTY_TITLE);
|
|
805
|
+
expect(
|
|
806
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-description"),
|
|
807
|
+
).toHaveTextContent("anyone with the link sees everyone's shifts on it");
|
|
808
|
+
|
|
809
|
+
expect(
|
|
810
|
+
within(
|
|
811
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-control"),
|
|
812
|
+
).getByTestId("schedule-shared-calendar-feed-publish"),
|
|
813
|
+
).toBeInTheDocument();
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
/*
|
|
817
|
+
* The schedule page stacks the personal panel on the shared one. Same
|
|
818
|
+
* shape, same spacing - so the badge is the only thing carrying "this one
|
|
819
|
+
* is mine, that one is the team's" before the reader starts reading.
|
|
820
|
+
*/
|
|
821
|
+
test("the shared panel wears a different badge from the personal one", async () => {
|
|
822
|
+
permissionsForTest = [Permission.ProjectAdmin];
|
|
823
|
+
|
|
824
|
+
render(
|
|
825
|
+
<div>
|
|
826
|
+
<PersonalCalendarFeedCard
|
|
827
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
828
|
+
now={NOW}
|
|
829
|
+
/>
|
|
830
|
+
<SharedCalendarFeedCard
|
|
831
|
+
kind={SharedCalendarFeedKind.Schedule}
|
|
832
|
+
scheduleId={SCHEDULE_ID}
|
|
833
|
+
scheduleTimezone="Europe/Stockholm"
|
|
834
|
+
now={NOW}
|
|
835
|
+
/>
|
|
836
|
+
</div>,
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
await waitFor(() => {
|
|
840
|
+
expect(
|
|
841
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-icon"),
|
|
842
|
+
).toBeInTheDocument();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
expect(
|
|
846
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-icon").innerHTML,
|
|
847
|
+
).not.toBe(
|
|
848
|
+
screen.getByTestId("personal-calendar-feed-empty-icon").innerHTML,
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("the project-wide panel says it covers every schedule in the project", async () => {
|
|
853
|
+
permissionsForTest = [Permission.ProjectAdmin];
|
|
854
|
+
|
|
855
|
+
render(
|
|
856
|
+
<SharedCalendarFeedCard
|
|
857
|
+
kind={SharedCalendarFeedKind.Project}
|
|
858
|
+
now={NOW}
|
|
859
|
+
/>,
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
await waitFor(() => {
|
|
863
|
+
expect(
|
|
864
|
+
screen.getByTestId("project-shared-calendar-feed-empty-state"),
|
|
865
|
+
).toBeInTheDocument();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
expect(
|
|
869
|
+
screen.getByTestId("project-shared-calendar-feed-empty-title"),
|
|
870
|
+
).toHaveTextContent(SHARED_FEED_EMPTY_TITLE);
|
|
871
|
+
expect(
|
|
872
|
+
screen.getByTestId("project-shared-calendar-feed-empty-description"),
|
|
873
|
+
).toHaveTextContent("every shift on every schedule in this project");
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
/*
|
|
877
|
+
* Not every reader of a schedule may publish its link, and what stands in
|
|
878
|
+
* for the button differs by how much the client knows: a real-but-
|
|
879
|
+
* insufficient permission gets a disabled button naming what is missing, a
|
|
880
|
+
* permission snapshot that has not landed gets a plain note rather than an
|
|
881
|
+
* accusation. Both belong in the same slot, so the panel reads the same
|
|
882
|
+
* either way.
|
|
883
|
+
*/
|
|
884
|
+
test("a reader with an insufficient permission gets a disabled Publish in the action slot", async () => {
|
|
885
|
+
permissionsForTest = [Permission.ReadProjectOnCallDutyPolicySchedule];
|
|
886
|
+
|
|
887
|
+
render(
|
|
888
|
+
<SharedCalendarFeedCard
|
|
889
|
+
kind={SharedCalendarFeedKind.Schedule}
|
|
890
|
+
scheduleId={SCHEDULE_ID}
|
|
891
|
+
scheduleTimezone="Europe/Stockholm"
|
|
892
|
+
now={NOW}
|
|
893
|
+
/>,
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
await waitFor(() => {
|
|
897
|
+
expect(
|
|
898
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-state"),
|
|
899
|
+
).toBeInTheDocument();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const publish: HTMLElement = within(
|
|
903
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-control"),
|
|
904
|
+
).getByTestId("schedule-shared-calendar-feed-publish");
|
|
905
|
+
|
|
906
|
+
expect(publish).toBeDisabled();
|
|
907
|
+
// The panel still explains the feature; only the action is withheld.
|
|
908
|
+
expect(
|
|
909
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-description"),
|
|
910
|
+
).toHaveTextContent("anyone with the link sees everyone's shifts on it");
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("a reader with no permissions at all gets the 'ask an editor' note there instead", async () => {
|
|
914
|
+
permissionsForTest = [];
|
|
915
|
+
|
|
916
|
+
render(
|
|
917
|
+
<SharedCalendarFeedCard
|
|
918
|
+
kind={SharedCalendarFeedKind.Schedule}
|
|
919
|
+
scheduleId={SCHEDULE_ID}
|
|
920
|
+
scheduleTimezone="Europe/Stockholm"
|
|
921
|
+
now={NOW}
|
|
922
|
+
/>,
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
await waitFor(() => {
|
|
926
|
+
expect(
|
|
927
|
+
screen.getByTestId("schedule-shared-calendar-feed-empty-state"),
|
|
928
|
+
).toBeInTheDocument();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const control: HTMLElement = screen.getByTestId(
|
|
932
|
+
"schedule-shared-calendar-feed-empty-control",
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expect(
|
|
936
|
+
within(control).getByTestId("schedule-shared-calendar-feed-ask-editor"),
|
|
937
|
+
).toHaveTextContent("Ask an editor of this schedule to publish it.");
|
|
938
|
+
expect(
|
|
939
|
+
within(control).queryByTestId("schedule-shared-calendar-feed-publish"),
|
|
940
|
+
).not.toBeInTheDocument();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("the deployment warnings sit above the shared panel too", async () => {
|
|
944
|
+
permissionsForTest = [Permission.ProjectAdmin];
|
|
945
|
+
getMock.mockResolvedValue(
|
|
946
|
+
ok({
|
|
947
|
+
...EMPTY_STATUS_JSON,
|
|
948
|
+
hostWarning: "HOST is not set, so the link points at localhost.",
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
render(
|
|
953
|
+
<SharedCalendarFeedCard
|
|
954
|
+
kind={SharedCalendarFeedKind.Project}
|
|
955
|
+
now={NOW}
|
|
956
|
+
/>,
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
await waitFor(() => {
|
|
960
|
+
expect(
|
|
961
|
+
screen.getByTestId("project-shared-calendar-feed-empty-state"),
|
|
962
|
+
).toBeInTheDocument();
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const panel: HTMLElement = screen.getByTestId(
|
|
966
|
+
"project-shared-calendar-feed-empty-state",
|
|
967
|
+
);
|
|
968
|
+
const host: HTMLElement = screen.getByTestId(
|
|
969
|
+
"project-shared-calendar-feed-host-warning",
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
expect(isBefore(host, panel)).toBe(true);
|
|
973
|
+
expect(panel).not.toContainElement(host);
|
|
974
|
+
});
|
|
975
|
+
});
|