@oneuptime/common 12.0.23 → 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.
Files changed (294) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  3. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  4. package/Models/DatabaseModels/Project.ts +68 -0
  5. package/Models/DatabaseModels/User.ts +68 -0
  6. package/Server/API/TelemetryAPI.ts +29 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Middleware/GzipRequestBody.ts +311 -0
  12. package/Server/Middleware/MultipartFormData.ts +87 -3
  13. package/Server/Services/DetectionRuleService.ts +73 -6
  14. package/Server/Services/Index.ts +205 -0
  15. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  16. package/Server/Services/NetworkDeviceService.ts +148 -0
  17. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  18. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  19. package/Server/Services/ProjectService.ts +29 -19
  20. package/Server/Services/SecurityEventService.ts +90 -13
  21. package/Server/Services/StatusPageResourceService.ts +223 -0
  22. package/Server/Services/TelemetryAttributeService.ts +17 -0
  23. package/Server/Services/UserService.ts +4 -6
  24. package/Server/Types/Markdown.ts +161 -67
  25. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  26. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  28. package/Server/Utils/Attribution.ts +4 -0
  29. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  30. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  31. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  32. package/Server/Utils/StartServer.ts +12 -32
  33. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  34. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  35. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  36. package/Server/Views/Partials/Head.ejs +2 -1
  37. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  38. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  39. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  40. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  41. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  42. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  43. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  44. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  45. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  46. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  47. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  48. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  49. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  50. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  51. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  52. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  53. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  54. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  55. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  56. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  57. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  58. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  59. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  60. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  61. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  62. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  63. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  64. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  65. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  66. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  67. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  68. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  69. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  70. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  72. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  73. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  74. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  75. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  76. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  77. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  78. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  79. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  80. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  81. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  82. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  83. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  84. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  85. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  86. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  87. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  88. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  89. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  90. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  91. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  92. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  93. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  94. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  95. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  96. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  97. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  98. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  99. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  100. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  101. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  102. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  103. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  104. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  105. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  106. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  107. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  108. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  109. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  110. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  111. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  112. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  113. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  114. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  115. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  116. package/Types/Code/YamlSyntax.ts +396 -0
  117. package/Types/Exception/ExceptionCode.ts +1 -0
  118. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  119. package/Types/Marketing/Attribution.ts +10 -0
  120. package/Types/Marketing/MarketingEvent.ts +4 -0
  121. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  122. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  123. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  124. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  125. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  126. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  127. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  128. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  129. package/UI/Components/Detail/Detail.tsx +10 -0
  130. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  131. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  132. package/UI/Components/Forms/Types/Field.ts +8 -0
  133. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  134. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  135. package/UI/Components/Forms/Validation.ts +46 -0
  136. package/UI/Components/Input/Input.tsx +3 -0
  137. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  138. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  139. package/UI/Components/ModelTable/Column.ts +8 -0
  140. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  141. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  142. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  143. package/UI/Components/SideOver/SideOver.tsx +5 -5
  144. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  145. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  146. package/UI/Components/Toggle/Toggle.tsx +9 -1
  147. package/UI/Components/Types/FieldType.ts +1 -0
  148. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  149. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  150. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  151. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  152. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  153. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  155. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  157. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  159. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/User.js +76 -0
  161. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  162. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  163. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  164. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  172. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  173. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  174. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  175. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  176. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  177. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  178. package/build/dist/Server/Services/Index.js +204 -0
  179. package/build/dist/Server/Services/Index.js.map +1 -1
  180. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  181. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  182. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  183. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  185. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  187. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectService.js +25 -45
  189. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  190. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  191. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  192. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  193. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  194. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  195. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  196. package/build/dist/Server/Services/UserService.js +2 -12
  197. package/build/dist/Server/Services/UserService.js.map +1 -1
  198. package/build/dist/Server/Types/Markdown.js +147 -61
  199. package/build/dist/Server/Types/Markdown.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  201. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  202. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  203. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  204. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  205. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  206. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  207. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  208. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  210. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  211. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  212. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  213. package/build/dist/Server/Utils/StartServer.js +12 -21
  214. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  215. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  216. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  217. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  218. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  219. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  220. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  221. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  222. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  223. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  224. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  225. package/build/dist/Types/Marketing/Attribution.js +10 -0
  226. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  227. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  228. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  229. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  230. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  231. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  232. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  233. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  234. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  235. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  236. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  237. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  238. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  239. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  240. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  241. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  242. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  243. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  244. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  245. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  246. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  247. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  248. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  249. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  250. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  251. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  252. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  253. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  254. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  255. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  256. package/build/dist/UI/Components/Input/Input.js +1 -1
  257. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  258. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  259. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  260. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  261. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  262. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  263. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  264. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  265. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  266. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  267. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  268. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  269. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  270. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  271. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  272. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  273. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  274. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  275. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  276. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  277. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  278. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  279. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  280. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  281. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  282. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  283. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  284. package/jest.config.json +2 -0
  285. package/package.json +1 -1
  286. package/tsconfig.json +2 -1
  287. package/Tests/UI/Components/Toast.test.tsx +0 -197
  288. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  289. package/UI/Components/Toast/Toast.tsx +0 -189
  290. package/UI/Components/Toast/ToastInit.tsx +0 -110
  291. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  292. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  293. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  294. 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
+ });