@oneuptime/common 12.0.22 → 12.0.24
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/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/TelemetryAPI.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +207 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
- package/Server/Services/NetworkDeviceService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
- package/Server/Services/ProjectService.ts +29 -19
- package/Server/Services/SecurityEventService.ts +90 -13
- package/Server/Services/StatusPageResourceService.ts +223 -0
- package/Server/Services/TelemetryAttributeService.ts +17 -0
- package/Server/Services/UserService.ts +4 -6
- package/Server/Types/Markdown.ts +161 -67
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
- package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
- package/Server/Utils/Attribution.ts +4 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
- package/Server/Utils/StartServer.ts +12 -32
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
- package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
- package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
- package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
- package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +76 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +76 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +17 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
- package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
- package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +49 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +206 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -45
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +57 -5
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
- package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +2 -12
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +147 -61
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -21
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/Permission.js +41 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
cleanup,
|
|
6
|
+
fireEvent,
|
|
7
|
+
render,
|
|
8
|
+
screen,
|
|
9
|
+
waitFor,
|
|
10
|
+
} from "@testing-library/react";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Monaco cannot run in jsdom, so it is replaced with a textarea that echoes
|
|
15
|
+
* the props <Editor> was given. Every suite that mounts CodeEditor has to
|
|
16
|
+
* declare its own copy of this mock - there is no shared __mocks__ entry - and
|
|
17
|
+
* it must export `loader`, because CodeEditor calls configureMonacoLoader() at
|
|
18
|
+
* module scope.
|
|
19
|
+
*/
|
|
20
|
+
interface RecordedEditorProps {
|
|
21
|
+
value?: string | undefined;
|
|
22
|
+
defaultLanguage?: string | undefined;
|
|
23
|
+
options?: Record<string, unknown> | undefined;
|
|
24
|
+
readOnly?: boolean | undefined;
|
|
25
|
+
height?: string | undefined;
|
|
26
|
+
/*
|
|
27
|
+
* Recorded so a test can prove what YamlEditor deliberately WITHHOLDS: the
|
|
28
|
+
* chrome owns the border and prints the error once, so neither may reach
|
|
29
|
+
* CodeEditor and be painted a second time.
|
|
30
|
+
*/
|
|
31
|
+
className?: string | undefined;
|
|
32
|
+
error?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mockEditorRenders: Array<RecordedEditorProps> = [];
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* tabSize/insertSpaces are set on the MODEL, not in the options bag: in
|
|
39
|
+
* monaco's standalone build they are global config, so an editor that set
|
|
40
|
+
* them per-instance would re-indent every other editor on the page.
|
|
41
|
+
*/
|
|
42
|
+
const mockModelOptionUpdates: Array<Record<string, unknown>> = [];
|
|
43
|
+
|
|
44
|
+
jest.mock("@monaco-editor/react", () => {
|
|
45
|
+
return {
|
|
46
|
+
__esModule: true,
|
|
47
|
+
loader: {
|
|
48
|
+
config: jest.fn(),
|
|
49
|
+
},
|
|
50
|
+
default: (editorProps: {
|
|
51
|
+
value?: string | undefined;
|
|
52
|
+
defaultLanguage?: string | undefined;
|
|
53
|
+
options?: Record<string, unknown> | undefined;
|
|
54
|
+
height?: string | undefined;
|
|
55
|
+
className?: string | undefined;
|
|
56
|
+
error?: string | undefined;
|
|
57
|
+
onChange?: ((value: string | undefined) => void) | undefined;
|
|
58
|
+
onMount?: ((editor: unknown, monaco: unknown) => void) | undefined;
|
|
59
|
+
}) => {
|
|
60
|
+
mockEditorRenders.push({
|
|
61
|
+
value: editorProps.value,
|
|
62
|
+
defaultLanguage: editorProps.defaultLanguage,
|
|
63
|
+
options: editorProps.options,
|
|
64
|
+
readOnly: editorProps.options?.["readOnly"] as boolean | undefined,
|
|
65
|
+
height: editorProps.height,
|
|
66
|
+
className: editorProps.className,
|
|
67
|
+
error: editorProps.error,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const hostRef: React.MutableRefObject<HTMLDivElement | null> =
|
|
71
|
+
React.useRef<HTMLDivElement | null>(null);
|
|
72
|
+
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
if (!editorProps.onMount) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
editorProps.onMount(
|
|
79
|
+
{
|
|
80
|
+
getDomNode: () => {
|
|
81
|
+
return hostRef.current;
|
|
82
|
+
},
|
|
83
|
+
getModel: () => {
|
|
84
|
+
return {
|
|
85
|
+
updateOptions: (modelOptions: Record<string, unknown>) => {
|
|
86
|
+
mockModelOptionUpdates.push(modelOptions);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{},
|
|
92
|
+
);
|
|
93
|
+
// eslint-disable-next-line
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div ref={hostRef}>
|
|
98
|
+
<textarea
|
|
99
|
+
data-testid="monaco"
|
|
100
|
+
value={editorProps.value || ""}
|
|
101
|
+
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
102
|
+
if (editorProps.onChange) {
|
|
103
|
+
editorProps.onChange(event.target.value);
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Imported after the mock so the module picks up the stand-in editor.
|
|
114
|
+
import YamlEditor, {
|
|
115
|
+
YAML_VALIDATION_DEBOUNCE_MS,
|
|
116
|
+
} from "../../../UI/Components/CodeEditor/YamlEditor";
|
|
117
|
+
|
|
118
|
+
const VALID_SIGMA_RULE: string = `title: Failed logon burst
|
|
119
|
+
logsource:
|
|
120
|
+
category: authentication
|
|
121
|
+
detection:
|
|
122
|
+
selection:
|
|
123
|
+
className: Authentication
|
|
124
|
+
condition: selection
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
type GetEditorFunction = () => HTMLTextAreaElement;
|
|
128
|
+
|
|
129
|
+
const getEditor: GetEditorFunction = (): HTMLTextAreaElement => {
|
|
130
|
+
return screen.getByTestId("monaco") as HTMLTextAreaElement;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type StatusTextFunction = () => string;
|
|
134
|
+
|
|
135
|
+
const statusText: StatusTextFunction = (): string => {
|
|
136
|
+
return screen.getByTestId("yaml-editor-status").textContent || "";
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type ExpectStatusFunction = (matcher: RegExp) => Promise<void>;
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* The parse is debounced, so the status bar is always asserted through
|
|
143
|
+
* waitFor rather than read synchronously after a keystroke.
|
|
144
|
+
*/
|
|
145
|
+
const expectStatus: ExpectStatusFunction = async (
|
|
146
|
+
matcher: RegExp,
|
|
147
|
+
): Promise<void> => {
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(statusText()).toMatch(matcher);
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
mockEditorRenders.length = 0;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
cleanup();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("YamlEditor — it is a YAML editor, not a rich text editor", () => {
|
|
162
|
+
/*
|
|
163
|
+
* The bug this component exists for: the "Sigma Rule (YAML)" field rendered
|
|
164
|
+
* the Markdown editor, so a Sigma rule got a Bold button and an H1 button
|
|
165
|
+
* over it - and the value was round-tripped through HTML, which
|
|
166
|
+
* indentation-sensitive YAML does not survive.
|
|
167
|
+
*/
|
|
168
|
+
test("Monaco is given the yaml grammar", () => {
|
|
169
|
+
render(<YamlEditor value={VALID_SIGMA_RULE} />);
|
|
170
|
+
|
|
171
|
+
expect(mockEditorRenders[0]?.defaultLanguage).toBe("yaml");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("there is no rich-text toolbar anywhere in it", () => {
|
|
175
|
+
render(<YamlEditor value={VALID_SIGMA_RULE} />);
|
|
176
|
+
|
|
177
|
+
for (const label of ["Bold", "Italic", "Heading 1", "Heading 2", "Link"]) {
|
|
178
|
+
expect(screen.queryByTitle(label)).toBeNull();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("the header names the language", () => {
|
|
183
|
+
render(<YamlEditor value="" />);
|
|
184
|
+
|
|
185
|
+
expect(screen.getByText("YAML")).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("YAML-safe indentation reaches Monaco's model", () => {
|
|
189
|
+
mockModelOptionUpdates.length = 0;
|
|
190
|
+
|
|
191
|
+
render(<YamlEditor value="a: 1" />);
|
|
192
|
+
|
|
193
|
+
expect(mockModelOptionUpdates).toContainEqual({
|
|
194
|
+
tabSize: 2,
|
|
195
|
+
insertSpaces: true,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("the gutter is on, because every parse error names a line", () => {
|
|
200
|
+
render(<YamlEditor value="a: 1" />);
|
|
201
|
+
|
|
202
|
+
const options: Record<string, unknown> = (mockEditorRenders[0]?.options ||
|
|
203
|
+
{}) as Record<string, unknown>;
|
|
204
|
+
|
|
205
|
+
expect(options["lineNumbers"]).toBe("on");
|
|
206
|
+
expect(options["renderWhitespace"]).toBe("boundary");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("YamlEditor — the value it shows", () => {
|
|
211
|
+
test("renders the value prop", () => {
|
|
212
|
+
render(<YamlEditor value={VALID_SIGMA_RULE} />);
|
|
213
|
+
|
|
214
|
+
expect(getEditor().value).toBe(VALID_SIGMA_RULE);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("renders initialValue when no value is given", () => {
|
|
218
|
+
render(<YamlEditor initialValue="a: 1" />);
|
|
219
|
+
|
|
220
|
+
expect(getEditor().value).toBe("a: 1");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("prefers value over initialValue", () => {
|
|
224
|
+
render(<YamlEditor initialValue="stale: true" value="current: true" />);
|
|
225
|
+
|
|
226
|
+
expect(getEditor().value).toBe("current: true");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("follows the value prop after mount", () => {
|
|
230
|
+
const { rerender } = render(<YamlEditor value="a: 1" />);
|
|
231
|
+
|
|
232
|
+
expect(getEditor().value).toBe("a: 1");
|
|
233
|
+
|
|
234
|
+
rerender(<YamlEditor value="a: 2" />);
|
|
235
|
+
|
|
236
|
+
expect(getEditor().value).toBe("a: 2");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("picks up a value that only arrives later", () => {
|
|
240
|
+
const { rerender } = render(<YamlEditor />);
|
|
241
|
+
|
|
242
|
+
expect(getEditor().value).toBe("");
|
|
243
|
+
|
|
244
|
+
rerender(<YamlEditor value={VALID_SIGMA_RULE} />);
|
|
245
|
+
|
|
246
|
+
expect(getEditor().value).toBe(VALID_SIGMA_RULE);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a non-string value is stringified rather than crashing", () => {
|
|
250
|
+
render(<YamlEditor value={{ a: 1 } as unknown as string} />);
|
|
251
|
+
|
|
252
|
+
expect(getEditor().value).toBe(JSON.stringify({ a: 1 }, null, 4));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("reports edits to onChange", () => {
|
|
256
|
+
const onChange: jest.Mock = jest.fn();
|
|
257
|
+
|
|
258
|
+
render(<YamlEditor value="before: 1" onChange={onChange} />);
|
|
259
|
+
|
|
260
|
+
fireEvent.change(getEditor(), { target: { value: "after: 2" } });
|
|
261
|
+
|
|
262
|
+
expect(onChange).toHaveBeenCalledWith("after: 2");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("typing is reflected even when the parent does not feed the value back", () => {
|
|
266
|
+
// The form field is controlled, but a bare caller may not be.
|
|
267
|
+
render(<YamlEditor />);
|
|
268
|
+
|
|
269
|
+
fireEvent.change(getEditor(), { target: { value: "typed: yes" } });
|
|
270
|
+
|
|
271
|
+
expect(getEditor().value).toBe("typed: yes");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("YamlEditor — the status bar tells you whether it parses", () => {
|
|
276
|
+
test("an empty editor says so instead of claiming an error", async () => {
|
|
277
|
+
render(<YamlEditor value="" />);
|
|
278
|
+
|
|
279
|
+
await expectStatus(/Nothing entered yet/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("a valid rule is confirmed, with a line count", async () => {
|
|
283
|
+
render(<YamlEditor value={"a: 1\nb: 2\n"} />);
|
|
284
|
+
|
|
285
|
+
await expectStatus(/Valid YAML/);
|
|
286
|
+
await expectStatus(/3 lines/);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("a one-line document says line, not lines", async () => {
|
|
290
|
+
render(<YamlEditor value="a: 1" />);
|
|
291
|
+
|
|
292
|
+
await expectStatus(/1 line(?!s)/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* The parser's own words, not a generic "Invalid YAML." - the reason is the
|
|
297
|
+
* only part of the message that tells the reader what to change.
|
|
298
|
+
*/
|
|
299
|
+
test("a broken rule is reported with the parser's reason", async () => {
|
|
300
|
+
render(<YamlEditor value="title: [unclosed" />);
|
|
301
|
+
|
|
302
|
+
await expectStatus(/unexpected end of the stream/i);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("a misindented rule names the indentation as the problem", async () => {
|
|
306
|
+
render(<YamlEditor value={"a: 1\nb:\n c: 1\n d: 2\n"} />);
|
|
307
|
+
|
|
308
|
+
await expectStatus(/bad indentation/i);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("a tab used for indentation is caught", async () => {
|
|
312
|
+
render(<YamlEditor value={"detection:\n\tselection: 1\n"} />);
|
|
313
|
+
|
|
314
|
+
await expectStatus(/tab/i);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("the failure names a line, which is why the gutter is on", async () => {
|
|
318
|
+
render(<YamlEditor value={"a: 1\nb:\n c: 1\n d: 2\n"} />);
|
|
319
|
+
|
|
320
|
+
await expectStatus(/line \d+/);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("it re-checks as the document is edited", async () => {
|
|
324
|
+
render(<YamlEditor value="title: [unclosed" />);
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(statusText()).not.toMatch(/Valid YAML/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
fireEvent.change(getEditor(), { target: { value: "title: fixed" } });
|
|
331
|
+
|
|
332
|
+
await expectStatus(/Valid YAML/);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/*
|
|
336
|
+
* checkYamlSyntax declines to judge a document whose shape is only known at
|
|
337
|
+
* run time. Claiming it is valid would be a claim about text nobody parsed.
|
|
338
|
+
*/
|
|
339
|
+
test("handlebars are reported as unchecked, not as broken and not as valid", async () => {
|
|
340
|
+
render(
|
|
341
|
+
<YamlEditor
|
|
342
|
+
value={"items:\n{{#each hosts}}\n - {{this}}\n{{/each}}\n"}
|
|
343
|
+
/>,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await expectStatus(/syntax not checked/);
|
|
347
|
+
expect(statusText()).not.toMatch(/Valid YAML/);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("a template standing in for a single value still parses", async () => {
|
|
351
|
+
render(<YamlEditor value="threshold: {{local.variables.count}}" />);
|
|
352
|
+
|
|
353
|
+
await expectStatus(/Valid YAML/);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("it is announced politely rather than interrupting", () => {
|
|
357
|
+
render(<YamlEditor value="a: 1" />);
|
|
358
|
+
|
|
359
|
+
const status: HTMLElement = screen.getByTestId("yaml-editor-status");
|
|
360
|
+
|
|
361
|
+
expect(status).toHaveAttribute("role", "status");
|
|
362
|
+
expect(status).toHaveAttribute("aria-live", "polite");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("YamlEditor — a form error is shown once, not twice", () => {
|
|
367
|
+
/*
|
|
368
|
+
* The form's own message wins whenever there is one, because it is the
|
|
369
|
+
* sentence blocking Save. It must also not reach CodeEditor, which would
|
|
370
|
+
* paint a second red border and print the text a second time under the box.
|
|
371
|
+
*/
|
|
372
|
+
test("the form error replaces the live status", async () => {
|
|
373
|
+
render(
|
|
374
|
+
<YamlEditor
|
|
375
|
+
value="title: [unclosed"
|
|
376
|
+
error="Sigma Rule (YAML) is not valid YAML. unexpected end of the stream"
|
|
377
|
+
/>,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await expectStatus(/unexpected end of the stream/);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("the error text appears exactly once in the whole component", () => {
|
|
384
|
+
const message: string = "Sigma Rule (YAML) is required.";
|
|
385
|
+
|
|
386
|
+
render(<YamlEditor value="" error={message} />);
|
|
387
|
+
|
|
388
|
+
expect(screen.getAllByText(message)).toHaveLength(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("a valid document with a form error still shows the error", async () => {
|
|
392
|
+
render(<YamlEditor value="a: 1" error="Something else is wrong." />);
|
|
393
|
+
|
|
394
|
+
await expectStatus(/Something else is wrong/);
|
|
395
|
+
expect(statusText()).not.toMatch(/Valid YAML/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("clearing the error hands the status bar back to the parser", async () => {
|
|
399
|
+
const { rerender } = render(
|
|
400
|
+
<YamlEditor value="a: 1" error="Something else is wrong." />,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
await expectStatus(/Something else is wrong/);
|
|
404
|
+
|
|
405
|
+
rerender(<YamlEditor value="a: 1" />);
|
|
406
|
+
|
|
407
|
+
await expectStatus(/Valid YAML/);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("YamlEditor — copying the document", () => {
|
|
412
|
+
type WriteTextMock = jest.Mock;
|
|
413
|
+
|
|
414
|
+
let writeText: WriteTextMock;
|
|
415
|
+
|
|
416
|
+
beforeEach(() => {
|
|
417
|
+
writeText = jest.fn(() => {
|
|
418
|
+
return Promise.resolve();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
422
|
+
value: { writeText },
|
|
423
|
+
configurable: true,
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("copies exactly what is in the editor", () => {
|
|
428
|
+
render(<YamlEditor value={VALID_SIGMA_RULE} />);
|
|
429
|
+
|
|
430
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
431
|
+
|
|
432
|
+
expect(writeText).toHaveBeenCalledWith(VALID_SIGMA_RULE);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("copies the edited text, not the text it mounted with", () => {
|
|
436
|
+
render(<YamlEditor initialValue="old: 1" />);
|
|
437
|
+
|
|
438
|
+
fireEvent.change(getEditor(), { target: { value: "new: 2" } });
|
|
439
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
440
|
+
|
|
441
|
+
expect(writeText).toHaveBeenCalledWith("new: 2");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("confirms the copy", async () => {
|
|
445
|
+
render(<YamlEditor value="a: 1" />);
|
|
446
|
+
|
|
447
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
448
|
+
|
|
449
|
+
await waitFor(() => {
|
|
450
|
+
expect(screen.getByTestId("yaml-editor-copy-button")).toHaveTextContent(
|
|
451
|
+
"Copied",
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("there is nothing to copy from an empty editor", () => {
|
|
457
|
+
render(<YamlEditor value="" />);
|
|
458
|
+
|
|
459
|
+
expect(screen.getByTestId("yaml-editor-copy-button")).toBeDisabled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("a browser with no clipboard API does not throw", () => {
|
|
463
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
464
|
+
value: undefined,
|
|
465
|
+
configurable: true,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
render(<YamlEditor value="a: 1" />);
|
|
469
|
+
|
|
470
|
+
expect(() => {
|
|
471
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
472
|
+
}).not.toThrow();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("a denied clipboard permission does not throw", () => {
|
|
476
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
477
|
+
value: {
|
|
478
|
+
writeText: jest.fn(() => {
|
|
479
|
+
return Promise.reject(new Error("denied"));
|
|
480
|
+
}),
|
|
481
|
+
},
|
|
482
|
+
configurable: true,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
render(<YamlEditor value="a: 1" />);
|
|
486
|
+
|
|
487
|
+
expect(() => {
|
|
488
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
489
|
+
}).not.toThrow();
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe("YamlEditor — the hint above the editor", () => {
|
|
494
|
+
test("says how YAML indentation works when the caller has nothing to add", () => {
|
|
495
|
+
render(<YamlEditor value="" />);
|
|
496
|
+
|
|
497
|
+
expect(screen.getByTestId("yaml-editor-hint").textContent).toMatch(
|
|
498
|
+
/2 spaces/,
|
|
499
|
+
);
|
|
500
|
+
expect(screen.getByTestId("yaml-editor-hint").textContent).toMatch(
|
|
501
|
+
/[Tt]abs/,
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("a caller's placeholder replaces it", () => {
|
|
506
|
+
render(<YamlEditor value="" placeholder="Sigma rule YAML." />);
|
|
507
|
+
|
|
508
|
+
expect(screen.getByTestId("yaml-editor-hint")).toHaveTextContent(
|
|
509
|
+
"Sigma rule YAML.",
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
/*
|
|
514
|
+
* The hint must never be seeded into the document: whatever Monaco is given
|
|
515
|
+
* as its initial content is text the user can accidentally save, and the
|
|
516
|
+
* server would reject a prose sentence as a Sigma rule.
|
|
517
|
+
*/
|
|
518
|
+
test("the hint is never written into the editor", () => {
|
|
519
|
+
render(<YamlEditor value="" placeholder="Sigma rule YAML." />);
|
|
520
|
+
|
|
521
|
+
expect(getEditor().value).toBe("");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("YamlEditor — accessibility and pass-through", () => {
|
|
526
|
+
test("the field label names the editor", () => {
|
|
527
|
+
render(<YamlEditor value="" ariaLabelledby="field-label-id" />);
|
|
528
|
+
|
|
529
|
+
expect(
|
|
530
|
+
document.querySelector('[aria-labelledby="field-label-id"]'),
|
|
531
|
+
).not.toBeNull();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("the caller's dataTestId reaches the editor itself", () => {
|
|
535
|
+
render(<YamlEditor value="" dataTestId="sigma-rule-yaml" />);
|
|
536
|
+
|
|
537
|
+
expect(screen.getByTestId("sigma-rule-yaml")).toBeInTheDocument();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("readOnly reaches Monaco", () => {
|
|
541
|
+
render(<YamlEditor value="a: 1" readOnly={true} />);
|
|
542
|
+
|
|
543
|
+
expect(mockEditorRenders[0]?.readOnly).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("a keyboard user is told how to get out of the editor", () => {
|
|
547
|
+
render(<YamlEditor value="" />);
|
|
548
|
+
|
|
549
|
+
expect(screen.getByText(/Control plus M/)).toBeInTheDocument();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("blur is reported so the form can mark the field touched", () => {
|
|
553
|
+
const onBlur: jest.Mock = jest.fn();
|
|
554
|
+
|
|
555
|
+
render(<YamlEditor value="a: 1" onBlur={onBlur} />);
|
|
556
|
+
|
|
557
|
+
fireEvent.change(getEditor(), { target: { value: "a: 2" } });
|
|
558
|
+
|
|
559
|
+
expect(onBlur).toHaveBeenCalled();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe("YamlEditor — the parse is debounced", () => {
|
|
564
|
+
beforeEach(() => {
|
|
565
|
+
jest.useFakeTimers();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
afterEach(() => {
|
|
569
|
+
jest.useRealTimers();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
type AdvanceFunction = (ms: number) => void;
|
|
573
|
+
|
|
574
|
+
const advance: AdvanceFunction = (ms: number): void => {
|
|
575
|
+
act(() => {
|
|
576
|
+
jest.advanceTimersByTime(ms);
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/*
|
|
581
|
+
* Without the debounce the document is judged on every keystroke, so a rule
|
|
582
|
+
* flashes an error the instant a line is half typed. This pins that the
|
|
583
|
+
* status waits for the typing to stop.
|
|
584
|
+
*/
|
|
585
|
+
test("a keystroke does not immediately re-judge the document", () => {
|
|
586
|
+
render(<YamlEditor value="a: 1" />);
|
|
587
|
+
|
|
588
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS);
|
|
589
|
+
expect(statusText()).toMatch(/Valid YAML/);
|
|
590
|
+
|
|
591
|
+
fireEvent.change(getEditor(), { target: { value: "a: [" } });
|
|
592
|
+
|
|
593
|
+
// One tick short of the debounce: the old verdict still stands.
|
|
594
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS - 1);
|
|
595
|
+
expect(statusText()).toMatch(/Valid YAML/);
|
|
596
|
+
|
|
597
|
+
advance(1);
|
|
598
|
+
expect(statusText()).not.toMatch(/Valid YAML/);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("only the settled text is judged, not every intermediate one", () => {
|
|
602
|
+
render(<YamlEditor value="a: 1" />);
|
|
603
|
+
|
|
604
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS);
|
|
605
|
+
|
|
606
|
+
fireEvent.change(getEditor(), { target: { value: "a: [" } });
|
|
607
|
+
advance(50);
|
|
608
|
+
fireEvent.change(getEditor(), { target: { value: "a: [1" } });
|
|
609
|
+
advance(50);
|
|
610
|
+
fireEvent.change(getEditor(), { target: { value: "a: [1]" } });
|
|
611
|
+
|
|
612
|
+
// The two broken intermediates never made it to the status bar.
|
|
613
|
+
expect(statusText()).toMatch(/Valid YAML/);
|
|
614
|
+
|
|
615
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS);
|
|
616
|
+
expect(statusText()).toMatch(/Valid YAML/);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
/*
|
|
620
|
+
* The form re-validates synchronously on every keystroke and marks the field
|
|
621
|
+
* touched from the first one, so props.error is undebounced. Letting it win
|
|
622
|
+
* immediately would put a red message under a half-typed line and defeat the
|
|
623
|
+
* debounce entirely.
|
|
624
|
+
*/
|
|
625
|
+
test("an undebounced form error does not overtake the debounced status", () => {
|
|
626
|
+
const { rerender } = render(<YamlEditor value="a: 1" />);
|
|
627
|
+
|
|
628
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS);
|
|
629
|
+
expect(statusText()).toMatch(/Valid YAML/);
|
|
630
|
+
|
|
631
|
+
rerender(<YamlEditor value="a: [" error="Config is not valid YAML." />);
|
|
632
|
+
|
|
633
|
+
expect(statusText()).not.toMatch(/Config is not valid YAML/);
|
|
634
|
+
|
|
635
|
+
advance(YAML_VALIDATION_DEBOUNCE_MS);
|
|
636
|
+
expect(statusText()).toMatch(/Config is not valid YAML/);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("the copy confirmation goes back to Copy on its own", () => {
|
|
640
|
+
render(<YamlEditor value="a: 1" />);
|
|
641
|
+
|
|
642
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
643
|
+
value: {
|
|
644
|
+
writeText: () => {
|
|
645
|
+
return Promise.resolve();
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
configurable: true,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
652
|
+
expect(screen.getByTestId("yaml-editor-copy-button")).toHaveTextContent(
|
|
653
|
+
"Copied",
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
advance(2000);
|
|
657
|
+
|
|
658
|
+
expect(screen.getByTestId("yaml-editor-copy-button")).toHaveTextContent(
|
|
659
|
+
"Copy",
|
|
660
|
+
);
|
|
661
|
+
expect(screen.getByTestId("yaml-editor-copy-button")).not.toHaveTextContent(
|
|
662
|
+
"Copied",
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe("YamlEditor — what it withholds from CodeEditor", () => {
|
|
668
|
+
beforeEach(() => {
|
|
669
|
+
mockEditorRenders.length = 0;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
type LastRenderFunction = () => RecordedEditorProps;
|
|
673
|
+
|
|
674
|
+
const lastRender: LastRenderFunction = (): RecordedEditorProps => {
|
|
675
|
+
expect(mockEditorRenders.length).toBeGreaterThan(0);
|
|
676
|
+
|
|
677
|
+
return mockEditorRenders[
|
|
678
|
+
mockEditorRenders.length - 1
|
|
679
|
+
] as RecordedEditorProps;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/*
|
|
683
|
+
* CodeEditor renders its own red border and its own copy of the error under
|
|
684
|
+
* the box. The chrome already says it once in the status bar, so forwarding
|
|
685
|
+
* the error would say it twice and outline it twice.
|
|
686
|
+
*/
|
|
687
|
+
test("the error is not forwarded — the status bar is the single voice", () => {
|
|
688
|
+
render(<YamlEditor value="a: [" error="Config is not valid YAML." />);
|
|
689
|
+
|
|
690
|
+
expect(lastRender().error).toBeUndefined();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("the editor is given a chrome-free className", () => {
|
|
694
|
+
render(<YamlEditor value="a: 1" />);
|
|
695
|
+
|
|
696
|
+
const className: string = lastRender().className || "";
|
|
697
|
+
|
|
698
|
+
expect(className).not.toContain("border");
|
|
699
|
+
expect(className).not.toContain("rounded");
|
|
700
|
+
expect(className).not.toContain("shadow");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("it is tall enough to author a rule in", () => {
|
|
704
|
+
render(<YamlEditor value="a: 1" />);
|
|
705
|
+
|
|
706
|
+
expect(lastRender().height).toBe("22rem");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("a caller can still choose the height", () => {
|
|
710
|
+
render(<YamlEditor value="a: 1" height="40rem" />);
|
|
711
|
+
|
|
712
|
+
expect(lastRender().height).toBe("40rem");
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe("YamlEditor — the editor's own accessible description", () => {
|
|
717
|
+
/*
|
|
718
|
+
* Monaco takes Tab to indent, so Tab cannot leave the editor. WCAG 2.1.2 is
|
|
719
|
+
* met only if the user is told the way out — which means the advisory has to
|
|
720
|
+
* reach them on the way IN, not as trailing text they meet on the way past.
|
|
721
|
+
*/
|
|
722
|
+
test("the escape hatch and the status are described to Monaco's input", () => {
|
|
723
|
+
render(<YamlEditor value="a: 1" />);
|
|
724
|
+
|
|
725
|
+
const describedBy: string =
|
|
726
|
+
getEditor().getAttribute("aria-describedby") || "";
|
|
727
|
+
|
|
728
|
+
expect(describedBy.split(" ")).toHaveLength(3);
|
|
729
|
+
|
|
730
|
+
for (const id of describedBy.split(" ")) {
|
|
731
|
+
expect(document.getElementById(id)).not.toBeNull();
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("the escape hint comes before the editor in reading order", () => {
|
|
736
|
+
render(<YamlEditor value="a: 1" />);
|
|
737
|
+
|
|
738
|
+
const hint: HTMLElement = screen.getByText(/toggle whether the Tab key/);
|
|
739
|
+
const editor: HTMLElement = getEditor();
|
|
740
|
+
|
|
741
|
+
expect(
|
|
742
|
+
hint.compareDocumentPosition(editor) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
743
|
+
).toBeTruthy();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("a broken document is announced as invalid to assistive technology", async () => {
|
|
747
|
+
render(<YamlEditor value="a: [" error="Config is not valid YAML." />);
|
|
748
|
+
|
|
749
|
+
await waitFor(() => {
|
|
750
|
+
expect(getEditor().getAttribute("aria-invalid")).toBe("true");
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("a clean document carries no aria-invalid", () => {
|
|
755
|
+
render(<YamlEditor value="a: 1" />);
|
|
756
|
+
|
|
757
|
+
expect(getEditor().hasAttribute("aria-invalid")).toBe(false);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("the copy confirmation is announced, not just drawn", () => {
|
|
761
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
762
|
+
value: {
|
|
763
|
+
writeText: () => {
|
|
764
|
+
return Promise.resolve();
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
configurable: true,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
render(<YamlEditor value="a: 1" />);
|
|
771
|
+
|
|
772
|
+
fireEvent.click(screen.getByTestId("yaml-editor-copy-button"));
|
|
773
|
+
|
|
774
|
+
const liveRegions: Array<HTMLElement> = screen.getAllByRole("status");
|
|
775
|
+
|
|
776
|
+
expect(
|
|
777
|
+
liveRegions.some((region: HTMLElement): boolean => {
|
|
778
|
+
return (region.textContent || "").includes("YAML copied to clipboard");
|
|
779
|
+
}),
|
|
780
|
+
).toBe(true);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("the copy button's own name tracks its state", () => {
|
|
784
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
785
|
+
value: {
|
|
786
|
+
writeText: () => {
|
|
787
|
+
return Promise.resolve();
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
configurable: true,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
render(<YamlEditor value="a: 1" />);
|
|
794
|
+
|
|
795
|
+
const button: HTMLElement = screen.getByTestId("yaml-editor-copy-button");
|
|
796
|
+
|
|
797
|
+
expect(button.getAttribute("aria-label")).toBe("Copy YAML to clipboard");
|
|
798
|
+
|
|
799
|
+
fireEvent.click(button);
|
|
800
|
+
|
|
801
|
+
/*
|
|
802
|
+
* WCAG 2.5.3: while the visible label reads "Copied", the accessible name
|
|
803
|
+
* has to contain it, or speech input users cannot address the control.
|
|
804
|
+
*/
|
|
805
|
+
expect(button.getAttribute("aria-label")).toContain("copied");
|
|
806
|
+
expect(button).toHaveTextContent("Copied");
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
describe("YamlEditor — the tab-trap escape hint names the right key", () => {
|
|
811
|
+
type SetPlatformFunction = (platform: string) => void;
|
|
812
|
+
|
|
813
|
+
const setPlatform: SetPlatformFunction = (platform: string): void => {
|
|
814
|
+
Object.defineProperty(navigator, "platform", {
|
|
815
|
+
value: platform,
|
|
816
|
+
configurable: true,
|
|
817
|
+
});
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
afterEach(() => {
|
|
821
|
+
setPlatform("Linux x86_64");
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
/*
|
|
825
|
+
* Monaco binds toggleTabFocusMode to Ctrl+M everywhere except macOS, whose
|
|
826
|
+
* `mac` override makes it Ctrl+Shift+M. Naming the wrong key turns the
|
|
827
|
+
* keyboard-trap advisory (WCAG 2.1.2) into a false one — the user presses a
|
|
828
|
+
* combination bound to nothing and stays trapped.
|
|
829
|
+
*/
|
|
830
|
+
test("Ctrl+M off macOS", () => {
|
|
831
|
+
setPlatform("Linux x86_64");
|
|
832
|
+
|
|
833
|
+
render(<YamlEditor value="a: 1" />);
|
|
834
|
+
|
|
835
|
+
expect(screen.getByText(/Press Control plus M to toggle/)).toBeTruthy();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("Ctrl+Shift+M on macOS", () => {
|
|
839
|
+
setPlatform("MacIntel");
|
|
840
|
+
|
|
841
|
+
render(<YamlEditor value="a: 1" />);
|
|
842
|
+
|
|
843
|
+
expect(
|
|
844
|
+
screen.getByText(/Press Control plus Shift plus M to toggle/),
|
|
845
|
+
).toBeTruthy();
|
|
846
|
+
});
|
|
847
|
+
});
|