@oneuptime/common 12.0.9 → 12.0.11

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 (262) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Models/DatabaseModels/RunnerJob.ts +17 -2
  4. package/Server/API/LlmProviderAPI.ts +8 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
  8. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  9. package/Server/Services/NetworkDeviceService.ts +265 -12
  10. package/Server/Services/RunnerJobService.ts +24 -1
  11. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  12. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  13. package/Server/Utils/LLM/LLMService.ts +108 -19
  14. package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
  15. package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
  16. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
  17. package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
  18. package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
  19. package/Server/Utils/SSRFProtection.ts +5 -4
  20. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  21. package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
  22. package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
  23. package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
  24. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  25. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
  26. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  27. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  28. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  29. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  30. package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
  31. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
  32. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  33. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  34. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  35. package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
  36. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  37. package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
  38. package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
  39. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  40. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  41. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
  42. package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
  43. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
  44. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
  45. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
  46. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
  47. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
  48. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  49. package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
  50. package/Tests/Types/API/URLTelScheme.test.ts +261 -0
  51. package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
  52. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  53. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  54. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  55. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
  56. package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
  57. package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
  58. package/Tests/UI/Components/Button.test.tsx +279 -0
  59. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  60. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  61. package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
  62. package/Tests/UI/Components/Icon.test.tsx +57 -0
  63. package/Tests/UI/Components/List.test.tsx +20 -3
  64. package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
  65. package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
  66. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  67. package/Tests/UI/Components/Pagination.test.tsx +721 -146
  68. package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
  69. package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
  70. package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
  71. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
  72. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
  73. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
  74. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
  75. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
  76. package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
  77. package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  79. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  80. package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
  81. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  82. package/Types/API/Protocol.ts +9 -0
  83. package/Types/API/URL.ts +286 -20
  84. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  85. package/Types/CustomField/CustomFieldType.ts +28 -0
  86. package/Types/Monitor/MonitorType.ts +11 -0
  87. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  88. package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
  89. package/Types/Runbook/RunbookStepType.ts +23 -0
  90. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
  91. package/UI/Components/Button/Button.tsx +14 -22
  92. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  93. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  94. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  95. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  96. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  97. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  98. package/UI/Components/Dictionary/Dictionary.tsx +4 -39
  99. package/UI/Components/Icon/Icon.tsx +1 -1
  100. package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
  101. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
  102. package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
  103. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
  104. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
  105. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  106. package/UI/Components/Pagination/Pagination.tsx +373 -328
  107. package/UI/Components/Pagination/PaginationUtil.ts +263 -0
  108. package/UI/Components/Pagination/Screenshots/README.md +15 -0
  109. package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
  110. package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
  111. package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
  112. package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
  113. package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
  114. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
  115. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
  116. package/UI/Components/TextArea/TextArea.tsx +3 -0
  117. package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
  118. package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
  119. package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
  120. package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
  121. package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
  122. package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
  123. package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
  124. package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
  125. package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
  126. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
  127. package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
  128. package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
  129. package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
  130. package/Utils/API.ts +243 -36
  131. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  132. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  133. package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
  134. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  135. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  137. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
  139. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  140. package/build/dist/Server/API/LlmProviderAPI.js +9 -1
  141. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  146. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
  147. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  148. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  149. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  150. package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
  151. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  152. package/build/dist/Server/Services/RunnerJobService.js +20 -2
  153. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  154. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  155. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  156. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  157. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  158. package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
  159. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  160. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
  161. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  162. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
  163. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  164. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
  165. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  166. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
  167. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  168. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
  169. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
  170. package/build/dist/Server/Utils/SSRFProtection.js +5 -4
  171. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  172. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  173. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  174. package/build/dist/Types/API/Protocol.js +9 -0
  175. package/build/dist/Types/API/Protocol.js.map +1 -1
  176. package/build/dist/Types/API/URL.js +241 -18
  177. package/build/dist/Types/API/URL.js.map +1 -1
  178. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  179. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  180. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  181. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  182. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  183. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  184. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
  185. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
  186. package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
  187. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  188. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  189. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  190. package/build/dist/UI/Components/Button/Button.js +10 -16
  191. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  193. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  195. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  196. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  197. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  198. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  199. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  200. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  201. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  202. package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
  203. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  204. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  205. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  206. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
  207. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  208. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
  209. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  210. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
  211. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  212. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  213. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  214. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
  215. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  216. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  217. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  218. package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
  219. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  220. package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
  221. package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
  222. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
  226. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  227. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
  229. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
  231. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
  232. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
  233. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
  234. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
  235. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
  236. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
  237. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
  238. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
  239. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
  240. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
  241. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
  242. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
  243. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
  244. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
  245. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
  246. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
  247. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
  248. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
  249. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
  250. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
  251. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  252. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
  253. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
  254. package/build/dist/Utils/API.js +175 -34
  255. package/build/dist/Utils/API.js.map +1 -1
  256. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  257. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  258. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  259. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  260. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
  261. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
  262. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import RunbookStepType, {
2
+ PAYLOAD_CARRYING_STEP_TYPES,
2
3
  RUNNER_EXECUTED_STEP_TYPES,
4
+ isPayloadCarryingStepType,
3
5
  isRunnerExecutedStepType,
4
6
  } from "../../../Types/Runbook/RunbookStepType";
5
7
  import { describe, expect, test } from "@jest/globals";
@@ -240,3 +242,125 @@ describe("isRunnerExecutedStepType", () => {
240
242
  );
241
243
  });
242
244
  });
245
+
246
+ /*
247
+ * The second partition, inside the Runner lane: how a job carries its
248
+ * instruction.
249
+ *
250
+ * A Bash or JavaScript job carries a script. An SSH or Kubernetes job carries
251
+ * structured instructions plus a credential resolved at claim time, and an
252
+ * EMPTY script — which is the whole reason this split has to be named
253
+ * somewhere. Issue #3209 was RunnerJob.script being declared a required
254
+ * column: an empty string is falsy, so the required-column check rejected
255
+ * every SSH and Kubernetes job at create() with "script is required". The rule
256
+ * is per-type and cannot be written as column metadata, so it lives in
257
+ * RunnerJobService.enqueue and reads this list.
258
+ */
259
+ describe("PAYLOAD_CARRYING_STEP_TYPES", () => {
260
+ test("contains exactly SSH and Kubernetes", () => {
261
+ expect(PAYLOAD_CARRYING_STEP_TYPES).toEqual([
262
+ RunbookStepType.SSH,
263
+ RunbookStepType.Kubernetes,
264
+ ]);
265
+ expect(new Set(PAYLOAD_CARRYING_STEP_TYPES).size).toBe(
266
+ PAYLOAD_CARRYING_STEP_TYPES.length,
267
+ );
268
+ });
269
+
270
+ test("every entry is also a Runner-executed type", () => {
271
+ /*
272
+ * A payload-carrying type that the Worker executes would be a
273
+ * contradiction: the payload exists to be handed to a Runner.
274
+ */
275
+ for (const type of PAYLOAD_CARRYING_STEP_TYPES) {
276
+ expect(RUNNER_EXECUTED_STEP_TYPES).toContain(type);
277
+ }
278
+ });
279
+ });
280
+
281
+ describe("isPayloadCarryingStepType", () => {
282
+ test.each([RunbookStepType.SSH, RunbookStepType.Kubernetes])(
283
+ "%s carries a payload rather than a script",
284
+ (type: RunbookStepType) => {
285
+ expect(isPayloadCarryingStepType(type)).toBe(true);
286
+ },
287
+ );
288
+
289
+ test.each([RunbookStepType.Bash, RunbookStepType.JavaScript])(
290
+ "%s carries a script rather than a payload",
291
+ (type: RunbookStepType) => {
292
+ /*
293
+ * Answering true for a script type would let a Bash job with no script
294
+ * through enqueue, and the Runner would spawn an empty shell and report
295
+ * success — indistinguishable from a step that actually ran.
296
+ */
297
+ expect(isPayloadCarryingStepType(type)).toBe(false);
298
+ },
299
+ );
300
+
301
+ test("the Runner lane splits cleanly into script-carrying and payload-carrying", () => {
302
+ /*
303
+ * Total and disjoint within the Runner lane. enqueue treats the two as
304
+ * complementary — anything not payload-carrying is required to bring a
305
+ * script — so a new Runner type that belongs in neither set does not
306
+ * exist, and a new one added to the enum must be placed here deliberately.
307
+ */
308
+ const payload: Array<RunbookStepType> = RUNNER_EXECUTED_STEP_TYPES.filter(
309
+ (type: RunbookStepType) => {
310
+ return isPayloadCarryingStepType(type);
311
+ },
312
+ );
313
+ const script: Array<RunbookStepType> = RUNNER_EXECUTED_STEP_TYPES.filter(
314
+ (type: RunbookStepType) => {
315
+ return !isPayloadCarryingStepType(type);
316
+ },
317
+ );
318
+
319
+ expect(payload.slice().sort()).toEqual(
320
+ PAYLOAD_CARRYING_STEP_TYPES.slice().sort(),
321
+ );
322
+ expect(script.slice().sort()).toEqual(
323
+ [RunbookStepType.JavaScript, RunbookStepType.Bash].slice().sort(),
324
+ );
325
+ expect(payload.length + script.length).toBe(
326
+ RUNNER_EXECUTED_STEP_TYPES.length,
327
+ );
328
+ });
329
+
330
+ test("agrees with the exported list for every member of the enum", () => {
331
+ for (const type of Object.values(RunbookStepType)) {
332
+ const answer: boolean = isPayloadCarryingStepType(type);
333
+
334
+ expect(typeof answer).toBe("boolean");
335
+ expect(answer).toBe(PAYLOAD_CARRYING_STEP_TYPES.includes(type));
336
+ }
337
+ });
338
+
339
+ test("no Worker-executed type carries a payload", () => {
340
+ for (const type of WORKER_EXECUTED_STEP_TYPES) {
341
+ expect(isPayloadCarryingStepType(type)).toBe(false);
342
+ }
343
+ });
344
+
345
+ test("near misses, null and undefined do not carry a payload", () => {
346
+ /*
347
+ * Falling back to false is the safe direction: an unrecognised type is
348
+ * then required to bring a script, and enqueue rejects it rather than
349
+ * creating a job with nothing in it.
350
+ */
351
+ const notPayloadTypes: Array<unknown> = [
352
+ "",
353
+ " ",
354
+ "ssh",
355
+ "SSH ",
356
+ "kubernetes",
357
+ "Docker",
358
+ null,
359
+ undefined,
360
+ ];
361
+
362
+ for (const value of notPayloadTypes) {
363
+ expect(isPayloadCarryingStepType(value as RunbookStepType)).toBe(false);
364
+ }
365
+ });
366
+ });
@@ -203,6 +203,23 @@ describe("the write payload is keyed on columns", () => {
203
203
  );
204
204
  });
205
205
 
206
+ /*
207
+ * ArgumentsForm reads the argument id to decide whether the record editor
208
+ * opens with a blank row for every column a create must supply: "json" is a
209
+ * create, anything else is an update. An update that opened on six seeded
210
+ * fields when the builder meant to set one would be a regression nothing else
211
+ * here would catch, so the two spellings are pinned as a pair.
212
+ */
213
+ test('a create\'s payload is "json" and an update\'s is "data", never the reverse', () => {
214
+ expect(argumentOf(find("-create-one"), "json")).toBeDefined();
215
+ expect(argumentOf(find("-create-one"), "data")).toBeUndefined();
216
+
217
+ for (const idSuffix of ["-update-one", "-update-many"]) {
218
+ expect(argumentOf(find(idSuffix), "data")).toBeDefined();
219
+ expect(argumentOf(find(idSuffix), "json")).toBeUndefined();
220
+ }
221
+ });
222
+
206
223
  test("no component declares a BaseModel argument", () => {
207
224
  /*
208
225
  * The bug this whole change came from: the row editor was selected only for
@@ -1,6 +1,7 @@
1
1
  import Button, {
2
2
  ButtonSize,
3
3
  ButtonStyleType,
4
+ ComponentProps,
4
5
  } from "../../../UI/Components/Button/Button";
5
6
  import ButtonType from "../../../UI/Components/Button/ButtonTypes";
6
7
  import ShortcutKey from "../../../UI/Components/ShortcutKey/ShortcutKey";
@@ -200,3 +201,281 @@ describe("Button", () => {
200
201
  expect(handleClick).toBeCalled();
201
202
  });
202
203
  });
204
+
205
+ type RenderButtonFunction = (props: Partial<ComponentProps>) => HTMLElement;
206
+
207
+ const renderButton: RenderButtonFunction = (
208
+ props: Partial<ComponentProps>,
209
+ ): HTMLElement => {
210
+ const { unmount } = render(<Button dataTestId="test-id" {...props} />);
211
+ const button: HTMLElement = screen.getByTestId("test-id");
212
+ // Clone so assertions survive the unmount and each case starts from a clean DOM.
213
+ const clone: HTMLElement = button.cloneNode(true) as HTMLElement;
214
+ unmount();
215
+ return clone;
216
+ };
217
+
218
+ type ClassesOfFunction = (element: HTMLElement) => Array<string>;
219
+
220
+ const classesOf: ClassesOfFunction = (element: HTMLElement): Array<string> => {
221
+ return element.className.split(/\s+/).filter((cssClass: string) => {
222
+ return cssClass.length > 0;
223
+ });
224
+ };
225
+
226
+ /*
227
+ * Every ButtonStyleType, so new styles are covered by the invariants below
228
+ * without anyone having to remember to add them here.
229
+ */
230
+ const ALL_BUTTON_STYLES: Array<[string, ButtonStyleType]> = Object.keys(
231
+ ButtonStyleType,
232
+ )
233
+ .filter((key: string) => {
234
+ return isNaN(Number(key));
235
+ })
236
+ .map((key: string) => {
237
+ return [key, ButtonStyleType[key as keyof typeof ButtonStyleType]] as [
238
+ string,
239
+ ButtonStyleType,
240
+ ];
241
+ });
242
+
243
+ describe("Button disabled styling", () => {
244
+ /*
245
+ * The regression this suite exists for: the disabled background used to be
246
+ * appended alongside the enabled one, and Tailwind emits .bg-indigo-300
247
+ * before .bg-indigo-600, so the enabled colour won the cascade and every
248
+ * disabled primary button rendered at full saturation.
249
+ */
250
+ test("disabled PRIMARY does not carry the enabled background class", () => {
251
+ const button: HTMLElement = renderButton({
252
+ buttonStyle: ButtonStyleType.PRIMARY,
253
+ disabled: true,
254
+ });
255
+
256
+ expect(button).not.toHaveClass("bg-indigo-600");
257
+ expect(button).toHaveClass("bg-indigo-300");
258
+ });
259
+
260
+ test("disabled SECONDARY does not carry the enabled background class", () => {
261
+ const button: HTMLElement = renderButton({
262
+ buttonStyle: ButtonStyleType.SECONDARY,
263
+ disabled: true,
264
+ });
265
+
266
+ expect(button).not.toHaveClass("bg-indigo-100");
267
+ expect(button).toHaveClass("bg-indigo-300");
268
+ });
269
+
270
+ test("enabled PRIMARY keeps the enabled background and no disabled background", () => {
271
+ const button: HTMLElement = renderButton({
272
+ buttonStyle: ButtonStyleType.PRIMARY,
273
+ disabled: false,
274
+ });
275
+
276
+ expect(button).toHaveClass("bg-indigo-600");
277
+ expect(button).not.toHaveClass("bg-indigo-300");
278
+ });
279
+
280
+ test("enabled SECONDARY keeps the enabled background and no disabled background", () => {
281
+ const button: HTMLElement = renderButton({
282
+ buttonStyle: ButtonStyleType.SECONDARY,
283
+ disabled: false,
284
+ });
285
+
286
+ expect(button).toHaveClass("bg-indigo-100");
287
+ expect(button).not.toHaveClass("bg-indigo-300");
288
+ });
289
+
290
+ test("an omitted disabled prop styles PRIMARY as enabled", () => {
291
+ const button: HTMLElement = renderButton({
292
+ buttonStyle: ButtonStyleType.PRIMARY,
293
+ });
294
+
295
+ expect(button).toHaveClass("bg-indigo-600");
296
+ expect(button).not.toHaveClass("bg-indigo-300");
297
+ });
298
+
299
+ test("an omitted disabled prop styles SECONDARY as enabled", () => {
300
+ const button: HTMLElement = renderButton({
301
+ buttonStyle: ButtonStyleType.SECONDARY,
302
+ });
303
+
304
+ expect(button).toHaveClass("bg-indigo-100");
305
+ expect(button).not.toHaveClass("bg-indigo-300");
306
+ });
307
+
308
+ test.each(ALL_BUTTON_STYLES)(
309
+ "%s emits at most one background utility when disabled",
310
+ (_name: string, buttonStyle: ButtonStyleType) => {
311
+ const button: HTMLElement = renderButton({ buttonStyle, disabled: true });
312
+
313
+ const backgrounds: Array<string> = classesOf(button).filter(
314
+ (cssClass: string) => {
315
+ return cssClass.startsWith("bg-");
316
+ },
317
+ );
318
+
319
+ expect(backgrounds.length).toBeLessThanOrEqual(1);
320
+ },
321
+ );
322
+
323
+ test.each(ALL_BUTTON_STYLES)(
324
+ "%s emits at most one background utility when enabled",
325
+ (_name: string, buttonStyle: ButtonStyleType) => {
326
+ const button: HTMLElement = renderButton({
327
+ buttonStyle,
328
+ disabled: false,
329
+ });
330
+
331
+ const backgrounds: Array<string> = classesOf(button).filter(
332
+ (cssClass: string) => {
333
+ return cssClass.startsWith("bg-");
334
+ },
335
+ );
336
+
337
+ expect(backgrounds.length).toBeLessThanOrEqual(1);
338
+ },
339
+ );
340
+
341
+ const DIMMING_STYLES: Array<[string, ButtonStyleType]> = [
342
+ ["PRIMARY", ButtonStyleType.PRIMARY],
343
+ ["SECONDARY", ButtonStyleType.SECONDARY],
344
+ ];
345
+
346
+ test.each(DIMMING_STYLES)(
347
+ "%s resolves to a different background when disabled",
348
+ (_name: string, buttonStyle: ButtonStyleType) => {
349
+ type BackgroundOfFunction = (disabled: boolean) => string | undefined;
350
+
351
+ const backgroundOf: BackgroundOfFunction = (
352
+ disabled: boolean,
353
+ ): string | undefined => {
354
+ return classesOf(renderButton({ buttonStyle, disabled })).find(
355
+ (cssClass: string) => {
356
+ return cssClass.startsWith("bg-");
357
+ },
358
+ );
359
+ };
360
+
361
+ const enabled: string | undefined = backgroundOf(false);
362
+ const disabled: string | undefined = backgroundOf(true);
363
+
364
+ expect(enabled).toBeDefined();
365
+ expect(disabled).toBeDefined();
366
+ expect(disabled).not.toBe(enabled);
367
+ },
368
+ );
369
+ });
370
+
371
+ describe("Button hover styling", () => {
372
+ /*
373
+ * Hover affordances are gated on `disabled`; the ternary used to be inverted,
374
+ * so hover styles applied to exactly the buttons that could not be clicked.
375
+ */
376
+ const HOVER_GATED_STYLES: Array<[string, ButtonStyleType, string]> = [
377
+ ["PRIMARY", ButtonStyleType.PRIMARY, "hover:bg-indigo-700"],
378
+ ["SECONDARY", ButtonStyleType.SECONDARY, "hover:bg-indigo-200"],
379
+ ["DANGER", ButtonStyleType.DANGER, "hover:bg-red-700"],
380
+ ["DANGER_OUTLINE", ButtonStyleType.DANGER_OUTLINE, "hover:bg-red-50"],
381
+ ["SUCCESS", ButtonStyleType.SUCCESS, "hover:bg-green-700"],
382
+ ["SUCCESS_OUTLINE", ButtonStyleType.SUCCESS_OUTLINE, "hover:bg-green-50"],
383
+ ["WARNING", ButtonStyleType.WARNING, "hover:bg-yellow-700"],
384
+ ["WARNING_OUTLINE", ButtonStyleType.WARNING_OUTLINE, "hover:bg-yellow-50"],
385
+ ["ICON", ButtonStyleType.ICON, "hover:text-gray-900"],
386
+ ["ICON_LIGHT", ButtonStyleType.ICON_LIGHT, "hover:text-gray-500"],
387
+ ];
388
+
389
+ test.each(HOVER_GATED_STYLES)(
390
+ "%s applies its hover class only when enabled",
391
+ (_name: string, buttonStyle: ButtonStyleType, hoverClass: string) => {
392
+ expect(renderButton({ buttonStyle, disabled: false })).toHaveClass(
393
+ hoverClass,
394
+ );
395
+ expect(renderButton({ buttonStyle, disabled: true })).not.toHaveClass(
396
+ hoverClass,
397
+ );
398
+ },
399
+ );
400
+ });
401
+
402
+ describe("Button class list composition", () => {
403
+ test("the size class is appended for every style", () => {
404
+ ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
405
+ expect(
406
+ renderButton({ buttonStyle, buttonSize: ButtonSize.Small }),
407
+ ).toHaveClass("px-2", "py-1");
408
+ });
409
+ });
410
+
411
+ test("a caller-supplied className is appended alongside the style classes", () => {
412
+ const button: HTMLElement = renderButton({
413
+ buttonStyle: ButtonStyleType.PRIMARY,
414
+ className: "my-custom-class",
415
+ disabled: true,
416
+ });
417
+
418
+ expect(button).toHaveClass("my-custom-class");
419
+ expect(button).toHaveClass("bg-indigo-300");
420
+ });
421
+
422
+ test("every style resolves to a non-empty class list in both states", () => {
423
+ ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
424
+ [true, false].forEach((disabled: boolean) => {
425
+ expect(
426
+ classesOf(renderButton({ buttonStyle, disabled })).length,
427
+ ).toBeGreaterThan(0);
428
+ });
429
+ });
430
+ });
431
+
432
+ test("no style emits a class twice", () => {
433
+ ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
434
+ [true, false].forEach((disabled: boolean) => {
435
+ const classes: Array<string> = classesOf(
436
+ renderButton({ buttonStyle, disabled }),
437
+ );
438
+
439
+ expect(classes).toHaveLength(new Set(classes).size);
440
+ });
441
+ });
442
+ });
443
+ });
444
+
445
+ describe("Button disabled behaviour", () => {
446
+ test("a disabled button is marked disabled to assistive technology", () => {
447
+ const button: HTMLElement = renderButton({
448
+ buttonStyle: ButtonStyleType.PRIMARY,
449
+ disabled: true,
450
+ });
451
+
452
+ expect(button).toHaveAttribute("disabled");
453
+ expect(button).toHaveAttribute("aria-disabled", "true");
454
+ });
455
+
456
+ test("a loading button is disabled", () => {
457
+ const button: HTMLElement = renderButton({
458
+ buttonStyle: ButtonStyleType.PRIMARY,
459
+ isLoading: true,
460
+ });
461
+
462
+ expect(button).toHaveAttribute("disabled");
463
+ expect(button).toHaveAttribute("aria-disabled", "true");
464
+ });
465
+
466
+ test("a disabled button does not fire onClick", () => {
467
+ const handleClick: () => void = jest.fn();
468
+ render(
469
+ <Button
470
+ dataTestId="test-id"
471
+ buttonStyle={ButtonStyleType.PRIMARY}
472
+ disabled={true}
473
+ onClick={handleClick}
474
+ />,
475
+ );
476
+
477
+ fireEvent.click(screen.getByTestId("test-id"));
478
+
479
+ expect(handleClick).not.toBeCalled();
480
+ });
481
+ });
@@ -0,0 +1,256 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import React from "react";
4
+ import { afterEach, describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
8
+ * the chart renders nothing without a fixed size. Same shim as
9
+ * AxisTickColor.test.tsx — anything that asserts on rendered GEOMETRY needs
10
+ * it, because a 0x0 chart draws no marks either and would make every one of
11
+ * these assertions pass for the wrong reason.
12
+ */
13
+ jest.mock("recharts", () => {
14
+ const actual: Record<string, any> = jest.requireActual("recharts");
15
+ return {
16
+ ...actual,
17
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
18
+ return React.cloneElement(children, { width: 600, height: 300 });
19
+ },
20
+ };
21
+ });
22
+
23
+ import AreaChartElement from "../../../../UI/Components/Charts/Area/AreaChart";
24
+ import BarChartElement from "../../../../UI/Components/Charts/Bar/BarChart";
25
+ import LineChartElement from "../../../../UI/Components/Charts/Line/LineChart";
26
+ import { CHART_DATA_POINT_X_AXIS_KEY } from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
27
+ import ChartCurve from "../../../../UI/Components/Charts/Types/ChartCurve";
28
+ import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
29
+ import SeriesPoint from "../../../../UI/Components/Charts/Types/SeriesPoints";
30
+ import {
31
+ XAxis as ChartXAxis,
32
+ XAxisAggregateType,
33
+ } from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
34
+ import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
35
+ import YAxis, {
36
+ YAxisPrecision,
37
+ } from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
38
+ import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
39
+
40
+ /*
41
+ * The three chart wrappers hand recharts a FIXED x-axis dataKey, while the
42
+ * rows they hand it are built by DataPointUtil. Those two used to agree only
43
+ * by convention: the util keyed each row by the caller's `xAxis.legend`, and
44
+ * every caller but one happened to pass the literal "Time".
45
+ *
46
+ * The one that did not — the SLO dashboard widget, which passed "" — got a
47
+ * chart with axes, grid and tooltip but no line: recharts looked up "Time" on
48
+ * every row, found nothing, and drew no path. Nothing failed, nothing logged;
49
+ * the widget just rendered an empty frame on public and private dashboards
50
+ * alike.
51
+ *
52
+ * These tests hold the axis key on both sides of that seam: whatever legend a
53
+ * caller passes, the marks get drawn.
54
+ */
55
+
56
+ const START: Date = new Date("2026-08-10T00:00:00.000Z");
57
+ const END: Date = new Date("2026-08-10T01:00:00.000Z");
58
+
59
+ /* An hour window buckets at one minute, so these land on distinct buckets. */
60
+ type BuildPointsFunction = () => Array<DataPoint>;
61
+
62
+ const buildPoints: BuildPointsFunction = (): Array<DataPoint> => {
63
+ const points: Array<DataPoint> = [];
64
+
65
+ for (let index: number = 0; index < 10; index++) {
66
+ points.push({
67
+ x: new Date(START.getTime() + index * 60 * 1000),
68
+ y: 90 + index,
69
+ });
70
+ }
71
+
72
+ return points;
73
+ };
74
+
75
+ type BuildXAxisFunction = (legend: string) => ChartXAxis;
76
+
77
+ const buildXAxis: BuildXAxisFunction = (legend: string): ChartXAxis => {
78
+ return {
79
+ legend: legend,
80
+ options: {
81
+ type: XAxisType.Time,
82
+ min: START,
83
+ max: END,
84
+ aggregateType: XAxisAggregateType.Average,
85
+ },
86
+ };
87
+ };
88
+
89
+ const Y_AXIS: YAxis = {
90
+ legend: "%",
91
+ options: {
92
+ type: YAxisType.Number,
93
+ min: "auto",
94
+ max: 100,
95
+ precision: YAxisPrecision.TwoDecimals,
96
+ formatter: (value: number): string => {
97
+ return `${value}%`;
98
+ },
99
+ },
100
+ };
101
+
102
+ const SERIES_NAME: string = "SLI";
103
+
104
+ type BuildSeriesFunction = () => Array<SeriesPoint>;
105
+
106
+ const buildSeries: BuildSeriesFunction = (): Array<SeriesPoint> => {
107
+ return [{ seriesName: SERIES_NAME, data: buildPoints() }];
108
+ };
109
+
110
+ /*
111
+ * Legends worth covering: the canonical one every other caller passes, the
112
+ * empty string the SLO widget passed (the reported bug), and an arbitrary
113
+ * label, which is what a future caller is most likely to reach for.
114
+ */
115
+ const LEGENDS: Array<[string, string]> = [
116
+ ['the canonical "Time"', "Time"],
117
+ ["an empty legend", ""],
118
+ ["an arbitrary legend", "Timestamp"],
119
+ ["a legend with markup-ish characters", "Time (UTC) <>"],
120
+ ];
121
+
122
+ interface ChartUnderTest {
123
+ name: string;
124
+ /** CSS selector for the SVG geometry this chart type draws per series. */
125
+ markSelector: string;
126
+ render: (xAxis: ChartXAxis) => React.ReactElement;
127
+ }
128
+
129
+ const CHARTS: Array<ChartUnderTest> = [
130
+ {
131
+ name: "LineChart",
132
+ markSelector: "path.recharts-line-curve",
133
+ render: (xAxis: ChartXAxis): React.ReactElement => {
134
+ return (
135
+ <LineChartElement
136
+ data={buildSeries()}
137
+ xAxis={xAxis}
138
+ yAxis={Y_AXIS}
139
+ curve={ChartCurve.MONOTONE}
140
+ heightInPx={300}
141
+ showLegend={false}
142
+ sync={false}
143
+ syncid="x-axis-key-test"
144
+ />
145
+ );
146
+ },
147
+ },
148
+ {
149
+ name: "AreaChart",
150
+ markSelector: "path.recharts-area-area",
151
+ render: (xAxis: ChartXAxis): React.ReactElement => {
152
+ return (
153
+ <AreaChartElement
154
+ data={buildSeries()}
155
+ xAxis={xAxis}
156
+ yAxis={Y_AXIS}
157
+ curve={ChartCurve.MONOTONE}
158
+ heightInPx={300}
159
+ showLegend={false}
160
+ sync={false}
161
+ syncid="x-axis-key-test"
162
+ />
163
+ );
164
+ },
165
+ },
166
+ {
167
+ /*
168
+ * Bars are drawn through a custom `shape` renderer, so the mark is a bare
169
+ * <path> under recharts' own rectangle group rather than a classed one.
170
+ */
171
+ name: "BarChart",
172
+ markSelector: "g.recharts-bar-rectangle path",
173
+ render: (xAxis: ChartXAxis): React.ReactElement => {
174
+ return (
175
+ <BarChartElement
176
+ data={buildSeries()}
177
+ xAxis={xAxis}
178
+ yAxis={Y_AXIS}
179
+ heightInPx={300}
180
+ showLegend={false}
181
+ sync={false}
182
+ syncid="x-axis-key-test"
183
+ />
184
+ );
185
+ },
186
+ },
187
+ ];
188
+
189
+ afterEach((): void => {
190
+ cleanup();
191
+ });
192
+
193
+ describe("Chart x-axis data key", () => {
194
+ test("the canonical key is the literal recharts reads", (): void => {
195
+ /*
196
+ * Pinned, not derived. The wrappers and DataPointUtil both import this
197
+ * constant, so a rename would keep them agreeing with each other while
198
+ * silently changing the shape handed to recharts.
199
+ */
200
+ expect(CHART_DATA_POINT_X_AXIS_KEY).toBe("Time");
201
+ });
202
+
203
+ for (const chart of CHARTS) {
204
+ describe(chart.name, () => {
205
+ test.each(LEGENDS)(
206
+ `draws its series with %s`,
207
+ (_label: string, legend: string): void => {
208
+ const { container } = render(chart.render(buildXAxis(legend)));
209
+
210
+ const marks: Array<Element> = Array.from(
211
+ container.querySelectorAll(chart.markSelector),
212
+ );
213
+
214
+ expect(marks.length).toBeGreaterThan(0);
215
+
216
+ /*
217
+ * Present-but-empty geometry is the exact failure mode here: a
218
+ * <path> with no `d` renders nothing at all, so the element count
219
+ * alone would not have caught the bug.
220
+ */
221
+ for (const mark of marks) {
222
+ expect(mark.getAttribute("d")).toBeTruthy();
223
+ }
224
+ },
225
+ );
226
+
227
+ test("draws the same geometry whatever the legend says", (): void => {
228
+ const canonical: RenderResultContainer = render(
229
+ chart.render(buildXAxis("Time")),
230
+ );
231
+ const canonicalPaths: Array<string | null> = Array.from(
232
+ canonical.container.querySelectorAll(chart.markSelector),
233
+ ).map((element: Element) => {
234
+ return element.getAttribute("d");
235
+ });
236
+
237
+ cleanup();
238
+
239
+ const other: RenderResultContainer = render(
240
+ chart.render(buildXAxis("")),
241
+ );
242
+ const otherPaths: Array<string | null> = Array.from(
243
+ other.container.querySelectorAll(chart.markSelector),
244
+ ).map((element: Element) => {
245
+ return element.getAttribute("d");
246
+ });
247
+
248
+ expect(otherPaths).toEqual(canonicalPaths);
249
+ });
250
+ });
251
+ }
252
+ });
253
+
254
+ interface RenderResultContainer {
255
+ container: HTMLElement;
256
+ }