@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,238 +1,747 @@
1
1
  import Pagination, {
2
2
  ComponentProps,
3
3
  } from "../../../UI/Components/Pagination/Pagination";
4
- import { describe, expect, jest } from "@jest/globals";
4
+ import { describe, expect, it, jest } from "@jest/globals";
5
5
  import "@testing-library/jest-dom";
6
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
6
+ import { fireEvent, render, screen } from "@testing-library/react";
7
7
  import React from "react";
8
8
  import getJestMockFunction, { MockFunction } from "../../../Tests/MockType";
9
9
 
10
+ type RenderPaginationFunction = (
11
+ overrides?: Partial<ComponentProps>,
12
+ ) => MockFunction;
13
+
14
+ const baseProps: ComponentProps = {
15
+ currentPageNumber: 1,
16
+ totalItemsCount: 240,
17
+ itemsOnPage: 10,
18
+ onNavigateToPage: jest.fn(),
19
+ isLoading: false,
20
+ isError: false,
21
+ singularLabel: "Monitor",
22
+ pluralLabel: "Monitors",
23
+ };
24
+
25
+ /*
26
+ * Renders the control and hands back the navigate spy, which is what almost
27
+ * every assertion below is really about.
28
+ */
29
+ const renderPagination: RenderPaginationFunction = (
30
+ overrides?: Partial<ComponentProps>,
31
+ ): MockFunction => {
32
+ const onNavigateToPage: MockFunction = getJestMockFunction();
33
+
34
+ render(
35
+ <Pagination
36
+ {...baseProps}
37
+ onNavigateToPage={onNavigateToPage}
38
+ {...overrides}
39
+ />,
40
+ );
41
+
42
+ return onNavigateToPage;
43
+ };
44
+
10
45
  describe("Pagination", () => {
11
- it("renders Component", () => {
12
- const props: ComponentProps = {
13
- currentPageNumber: 1,
14
- totalItemsCount: 20,
15
- itemsOnPage: 10,
16
- onNavigateToPage: jest.fn(),
17
- isLoading: false,
18
- isError: false,
19
- singularLabel: "Item",
20
- pluralLabel: "Items",
21
- };
46
+ describe("summary", () => {
47
+ it("prints the range and the total", () => {
48
+ renderPagination();
22
49
 
23
- render(<Pagination {...props} />);
50
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
51
+ "Showing 1-10 of 240 monitors",
52
+ );
53
+ });
24
54
 
25
- expect(
26
- screen.getByText(/Showing 1 to 10 on this page/i),
27
- ).toBeInTheDocument();
28
- });
55
+ it("offsets the range by the pages already passed", () => {
56
+ renderPagination({ currentPageNumber: 4, itemsOnPage: 25 });
29
57
 
30
- it("renders with 1 item", () => {
31
- const props: ComponentProps = {
32
- currentPageNumber: 1,
33
- totalItemsCount: 1,
34
- itemsOnPage: 10,
35
- onNavigateToPage: jest.fn(),
36
- isLoading: false,
37
- isError: false,
38
- singularLabel: "Item",
39
- pluralLabel: "Items",
40
- };
58
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
59
+ "Showing 76-100 of 240 monitors",
60
+ );
61
+ });
62
+
63
+ /*
64
+ * The label used to multiply the page number by the page size, so the
65
+ * last page of a 19-row list claimed to be showing rows 11 to 20.
66
+ */
67
+ it("stops the last page at the last row that exists", () => {
68
+ renderPagination({ currentPageNumber: 2, totalItemsCount: 19 });
69
+
70
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
71
+ "Showing 11-19 of 19 monitors",
72
+ );
73
+ });
41
74
 
42
- render(<Pagination {...props} />);
75
+ it("uses the singular label for a list of one", () => {
76
+ renderPagination({ totalItemsCount: 1 });
43
77
 
44
- expect(screen.getByText(/1 Item/i)).toBeInTheDocument();
78
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
79
+ "Showing 1 of 1 monitor",
80
+ );
81
+ });
82
+
83
+ it("says so when there is nothing to show", () => {
84
+ renderPagination({ totalItemsCount: 0 });
85
+
86
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
87
+ "No monitors",
88
+ );
89
+ });
90
+
91
+ it("groups the digits of a large total", () => {
92
+ renderPagination({ totalItemsCount: 1234567 });
93
+
94
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
95
+ `Showing 1-10 of ${(1234567).toLocaleString()} monitors`,
96
+ );
97
+ });
98
+
99
+ it("clamps the range to the rows the page rendered", () => {
100
+ renderPagination({
101
+ currentPageNumber: 24,
102
+ totalItemsCount: 236,
103
+ itemsOnCurrentPage: 6,
104
+ });
105
+
106
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
107
+ "Showing 231-236 of 236 monitors",
108
+ );
109
+ });
110
+
111
+ it("says it is loading rather than printing a stale range", () => {
112
+ renderPagination({ isLoading: true });
113
+
114
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
115
+ "Loading",
116
+ );
117
+ });
45
118
  });
46
119
 
47
- it("calls onNavigateToPage when Next link is clicked", async () => {
48
- const mockOnNavigateToPage: MockFunction = getJestMockFunction();
49
- const props: ComponentProps = {
50
- currentPageNumber: 1,
51
- totalItemsCount: 19,
52
- itemsOnPage: 10,
53
- onNavigateToPage: mockOnNavigateToPage,
54
- isLoading: false,
55
- isError: false,
56
- singularLabel: "Item",
57
- pluralLabel: "Items",
58
- };
120
+ describe("page numbers", () => {
121
+ it("renders a button for every page of a short list", () => {
122
+ renderPagination({ totalItemsCount: 30 });
123
+
124
+ expect(screen.getByTestId("pagination-page-1")).toBeInTheDocument();
125
+ expect(screen.getByTestId("pagination-page-2")).toBeInTheDocument();
126
+ expect(screen.getByTestId("pagination-page-3")).toBeInTheDocument();
127
+ expect(screen.queryByTestId("pagination-page-4")).toBeNull();
128
+ });
129
+
130
+ it("navigates to the page that was clicked", () => {
131
+ const onNavigateToPage: MockFunction = renderPagination({
132
+ totalItemsCount: 30,
133
+ });
134
+
135
+ fireEvent.click(screen.getByTestId("pagination-page-3"));
136
+
137
+ expect(onNavigateToPage).toHaveBeenCalledWith(3, 10);
138
+ });
139
+
140
+ it("keeps the page size when jumping to a page", () => {
141
+ const onNavigateToPage: MockFunction = renderPagination({
142
+ totalItemsCount: 250,
143
+ itemsOnPage: 50,
144
+ });
145
+
146
+ fireEvent.click(screen.getByTestId("pagination-page-4"));
147
+
148
+ expect(onNavigateToPage).toHaveBeenCalledWith(4, 50);
149
+ });
150
+
151
+ it("marks the current page for assistive technology", () => {
152
+ renderPagination({ currentPageNumber: 3, totalItemsCount: 30 });
153
+
154
+ expect(screen.getByTestId("pagination-page-3")).toHaveAttribute(
155
+ "aria-current",
156
+ "page",
157
+ );
158
+ expect(screen.getByTestId("pagination-page-1")).not.toHaveAttribute(
159
+ "aria-current",
160
+ );
161
+ });
162
+
163
+ it("does not re-fetch the page the user is already on", () => {
164
+ const onNavigateToPage: MockFunction = renderPagination({
165
+ currentPageNumber: 2,
166
+ totalItemsCount: 30,
167
+ });
168
+
169
+ fireEvent.click(screen.getByTestId("pagination-page-2"));
170
+
171
+ expect(onNavigateToPage).not.toHaveBeenCalled();
172
+ });
173
+
174
+ /*
175
+ * Five slots, not a row of a dozen buttons: where you are, both ends,
176
+ * and a gap on either side for everything else.
177
+ */
178
+ it("collapses the middle of a long list but keeps both ends", () => {
179
+ renderPagination({ currentPageNumber: 12, totalItemsCount: 240 });
180
+
181
+ expect(screen.getByTestId("pagination-page-1")).toBeInTheDocument();
182
+ expect(screen.getByTestId("pagination-page-12")).toBeInTheDocument();
183
+ expect(screen.getByTestId("pagination-page-24")).toBeInTheDocument();
184
+ expect(screen.queryByTestId("pagination-page-11")).toBeNull();
185
+ expect(screen.queryByTestId("pagination-page-13")).toBeNull();
186
+ expect(
187
+ screen.getByTestId("pagination-ellipsis-start"),
188
+ ).toBeInTheDocument();
189
+ expect(screen.getByTestId("pagination-ellipsis-end")).toBeInTheDocument();
190
+ });
191
+
192
+ it("never draws more than five page slots", () => {
193
+ renderPagination({ currentPageNumber: 12, totalItemsCount: 10000 });
194
+
195
+ expect(
196
+ screen
197
+ .getByRole("navigation")
198
+ .querySelectorAll(
199
+ '[data-testid^="pagination-page-"], [data-testid^="pagination-ellipsis-"]',
200
+ ),
201
+ ).toHaveLength(5);
202
+ });
203
+
204
+ it("jumps to the last page in one click", () => {
205
+ const onNavigateToPage: MockFunction = renderPagination({
206
+ currentPageNumber: 12,
207
+ totalItemsCount: 240,
208
+ });
209
+
210
+ fireEvent.click(screen.getByTestId("pagination-page-24"));
211
+
212
+ expect(onNavigateToPage).toHaveBeenCalledWith(24, 10);
213
+ });
214
+
215
+ /*
216
+ * An even split used to produce one page too many, so a 20-row list with
217
+ * 10 rows to a page offered a page 3 that could only render empty.
218
+ */
219
+ it("does not offer a page past the end of an even split", () => {
220
+ renderPagination({ totalItemsCount: 20 });
59
221
 
60
- render(<Pagination {...props} />);
222
+ expect(screen.getByTestId("pagination-page-2")).toBeInTheDocument();
223
+ expect(screen.queryByTestId("pagination-page-3")).toBeNull();
224
+ });
61
225
 
62
- // There are multiple "Next" elements (mobile and desktop), get the first one
63
- const nextButtons: HTMLElement[] = screen.getAllByText("Next");
64
- fireEvent.click(nextButtons[0]!);
226
+ it("renders a single page for an empty list", () => {
227
+ renderPagination({ totalItemsCount: 0 });
65
228
 
66
- await waitFor(() => {
67
- expect(mockOnNavigateToPage).toHaveBeenCalledWith(2, 10);
229
+ expect(screen.getByTestId("pagination-page-1")).toBeInTheDocument();
230
+ expect(screen.queryByTestId("pagination-page-2")).toBeNull();
231
+ });
232
+
233
+ it("does not navigate from a page number while loading", () => {
234
+ const onNavigateToPage: MockFunction = renderPagination({
235
+ totalItemsCount: 30,
236
+ isLoading: true,
237
+ });
238
+
239
+ fireEvent.click(screen.getByTestId("pagination-page-2"));
240
+
241
+ expect(onNavigateToPage).not.toHaveBeenCalled();
68
242
  });
69
243
  });
70
244
 
71
- it("calls onNavigateToPage when Previous link is clicked", async () => {
72
- const mockOnNavigateToPage: MockFunction = getJestMockFunction();
73
- const props: ComponentProps = {
74
- currentPageNumber: 2,
75
- totalItemsCount: 19,
76
- itemsOnPage: 10,
77
- onNavigateToPage: mockOnNavigateToPage,
78
- isLoading: false,
79
- isError: false,
80
- singularLabel: "Item",
81
- pluralLabel: "Items",
82
- };
245
+ describe("previous and next", () => {
246
+ it("moves forward a page", () => {
247
+ const onNavigateToPage: MockFunction = renderPagination();
248
+
249
+ fireEvent.click(screen.getByTestId("pagination-next-button"));
250
+
251
+ expect(onNavigateToPage).toHaveBeenCalledWith(2, 10);
252
+ });
253
+
254
+ it("moves back a page", () => {
255
+ const onNavigateToPage: MockFunction = renderPagination({
256
+ currentPageNumber: 5,
257
+ });
83
258
 
84
- render(<Pagination {...props} />);
259
+ fireEvent.click(screen.getByTestId("pagination-previous-button"));
85
260
 
86
- // There are multiple "Previous" elements (mobile and desktop), get the first one
87
- const prevButtons: HTMLElement[] = screen.getAllByText("Previous");
88
- fireEvent.click(prevButtons[0]!);
261
+ expect(onNavigateToPage).toHaveBeenCalledWith(4, 10);
262
+ });
263
+
264
+ it("disables previous on the first page", () => {
265
+ renderPagination();
266
+
267
+ expect(screen.getByTestId("pagination-previous-button")).toBeDisabled();
268
+ expect(screen.getByTestId("pagination-next-button")).toBeEnabled();
269
+ });
270
+
271
+ it("disables next on the last page", () => {
272
+ renderPagination({ currentPageNumber: 24 });
273
+
274
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
275
+ expect(screen.getByTestId("pagination-previous-button")).toBeEnabled();
276
+ });
277
+
278
+ it("disables next when the last page is only partly full", () => {
279
+ renderPagination({ currentPageNumber: 2, totalItemsCount: 19 });
89
280
 
90
- await waitFor(() => {
91
- expect(mockOnNavigateToPage).toHaveBeenCalledWith(1, 10);
281
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
282
+ });
283
+
284
+ it("disables both directions for an empty list", () => {
285
+ renderPagination({ totalItemsCount: 0 });
286
+
287
+ expect(screen.getByTestId("pagination-previous-button")).toBeDisabled();
288
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
289
+ });
290
+
291
+ it("disables both directions while loading", () => {
292
+ renderPagination({ currentPageNumber: 5, isLoading: true });
293
+
294
+ expect(screen.getByTestId("pagination-previous-button")).toBeDisabled();
295
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
296
+ });
297
+
298
+ it("disables both directions after an error", () => {
299
+ renderPagination({ currentPageNumber: 5, isError: true });
300
+
301
+ expect(screen.getByTestId("pagination-previous-button")).toBeDisabled();
302
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
303
+ });
304
+
305
+ it("disables both directions when the caller asks", () => {
306
+ renderPagination({ currentPageNumber: 5, isDisabled: true });
307
+
308
+ expect(screen.getByTestId("pagination-previous-button")).toBeDisabled();
309
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
92
310
  });
93
311
  });
94
312
 
95
- it("shows Pagination Modal when button modal is clicked", async () => {
96
- const props: ComponentProps = {
97
- currentPageNumber: 1,
98
- totalItemsCount: 0,
99
- itemsOnPage: 10,
100
- onNavigateToPage: jest.fn(),
101
- isLoading: false,
102
- isError: false,
103
- singularLabel: "Item",
104
- pluralLabel: "Items",
105
- };
313
+ describe("rows per page", () => {
314
+ it("is visible on the page rather than behind a menu", () => {
315
+ renderPagination();
316
+
317
+ expect(screen.getByLabelText("Rows per page")).toBeInTheDocument();
318
+ expect(screen.getByTestId("pagination-items-on-page-select")).toHaveValue(
319
+ "10",
320
+ );
321
+ });
322
+
323
+ it("offers the standard page sizes", () => {
324
+ renderPagination();
325
+
326
+ const options: Array<HTMLOptionElement> = Array.from(
327
+ screen
328
+ .getByTestId("pagination-items-on-page-select")
329
+ .querySelectorAll("option"),
330
+ );
331
+
332
+ expect(
333
+ options.map((option: HTMLOptionElement) => {
334
+ return option.value;
335
+ }),
336
+ ).toEqual(["10", "20", "25", "50", "100"]);
337
+ });
338
+
339
+ it("changes the page size", () => {
340
+ const onNavigateToPage: MockFunction = renderPagination();
106
341
 
107
- render(<Pagination {...props} />);
342
+ fireEvent.change(screen.getByTestId("pagination-items-on-page-select"), {
343
+ target: { value: "50" },
344
+ });
108
345
 
109
- fireEvent.click(screen.getByTestId("show-pagination-modal-button"));
346
+ expect(onNavigateToPage).toHaveBeenCalledWith(1, 50);
347
+ });
348
+
349
+ /*
350
+ * Row 400 of the old page size is not row 400 of the new one, so a
351
+ * resize that kept the page number could land on a page that no longer
352
+ * exists.
353
+ */
354
+ it("returns to the first page when the size changes", () => {
355
+ const onNavigateToPage: MockFunction = renderPagination({
356
+ currentPageNumber: 20,
357
+ });
358
+
359
+ fireEvent.change(screen.getByTestId("pagination-items-on-page-select"), {
360
+ target: { value: "100" },
361
+ });
362
+
363
+ expect(onNavigateToPage).toHaveBeenCalledWith(1, 100);
364
+ });
365
+
366
+ it("does nothing when the size is unchanged", () => {
367
+ const onNavigateToPage: MockFunction = renderPagination();
368
+
369
+ fireEvent.change(screen.getByTestId("pagination-items-on-page-select"), {
370
+ target: { value: "10" },
371
+ });
372
+
373
+ expect(onNavigateToPage).not.toHaveBeenCalled();
374
+ });
375
+
376
+ /*
377
+ * A shared link can carry any page size. A select whose value is missing
378
+ * from its options renders as the first option instead, so the table
379
+ * would claim 10 rows a page while showing 37.
380
+ */
381
+ it("shows a page size that came from a link", () => {
382
+ renderPagination({ itemsOnPage: 37 });
110
383
 
111
- await waitFor(() => {
112
- expect(screen.getByText("Navigate to Page")).toBeInTheDocument();
384
+ expect(screen.getByTestId("pagination-items-on-page-select")).toHaveValue(
385
+ "37",
386
+ );
387
+ });
388
+
389
+ it("honours a caller's own page sizes", () => {
390
+ renderPagination({ itemsOnPage: 50, itemsOnPageOptions: [50, 250] });
391
+
392
+ const options: Array<HTMLOptionElement> = Array.from(
393
+ screen
394
+ .getByTestId("pagination-items-on-page-select")
395
+ .querySelectorAll("option"),
396
+ );
397
+
398
+ expect(
399
+ options.map((option: HTMLOptionElement) => {
400
+ return option.value;
401
+ }),
402
+ ).toEqual(["50", "250"]);
403
+ });
404
+
405
+ it("is frozen while loading", () => {
406
+ renderPagination({ isLoading: true });
407
+
408
+ expect(
409
+ screen.getByTestId("pagination-items-on-page-select"),
410
+ ).toBeDisabled();
113
411
  });
114
412
  });
115
413
 
116
- it("shows Pagination Modal when current page link is clicked", async () => {
117
- const props: ComponentProps = {
118
- currentPageNumber: 2,
119
- totalItemsCount: 19,
120
- itemsOnPage: 10,
121
- onNavigateToPage: jest.fn(),
122
- isLoading: false,
123
- isError: false,
124
- singularLabel: "Item",
125
- pluralLabel: "Items",
414
+ /*
415
+ * The jump box costs nothing on the bar until it is asked for: the
416
+ * collapsed gap is the button that opens it, which is also where the
417
+ * pages it can reach were hidden.
418
+ */
419
+ describe("go to page", () => {
420
+ type OpenGoToPageModalFunction = (
421
+ overrides?: Partial<ComponentProps>,
422
+ ) => MockFunction;
423
+
424
+ const openGoToPageModal: OpenGoToPageModalFunction = (
425
+ overrides?: Partial<ComponentProps>,
426
+ ): MockFunction => {
427
+ const onNavigateToPage: MockFunction = renderPagination(overrides);
428
+ fireEvent.click(screen.getByTestId("pagination-ellipsis-end"));
429
+ return onNavigateToPage;
126
430
  };
127
431
 
128
- render(<Pagination {...props} />);
432
+ it("takes up no room on the bar until it is asked for", () => {
433
+ renderPagination();
434
+
435
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
436
+ expect(screen.queryByTestId("modal")).toBeNull();
437
+ });
438
+
439
+ it("opens from a collapsed gap", () => {
440
+ openGoToPageModal();
441
+
442
+ expect(
443
+ screen.getByTestId("pagination-go-to-page-input"),
444
+ ).toBeInTheDocument();
445
+ expect(screen.getByTestId("modal-title")).toHaveTextContent("Go to page");
446
+ });
447
+
448
+ it("says how many pages there are to choose from", () => {
449
+ openGoToPageModal();
450
+
451
+ expect(screen.getByTestId("modal-description")).toHaveTextContent(
452
+ "This list has 24 pages",
453
+ );
454
+ });
455
+
456
+ it("has no gap to open it from when every page is one click away", () => {
457
+ renderPagination({ totalItemsCount: 30 });
129
458
 
130
- fireEvent.click(screen.getByTestId("current-page-link"));
459
+ expect(screen.queryByTestId("pagination-ellipsis-end")).toBeNull();
460
+ expect(screen.queryByTestId("pagination-ellipsis-start")).toBeNull();
461
+ });
462
+
463
+ it("opens from the gap before the current page too", () => {
464
+ renderPagination({ currentPageNumber: 12 });
131
465
 
132
- await waitFor(() => {
133
- expect(screen.getByText("Navigate to Page")).toBeInTheDocument();
466
+ fireEvent.click(screen.getByTestId("pagination-ellipsis-start"));
467
+
468
+ expect(
469
+ screen.getByTestId("pagination-go-to-page-input"),
470
+ ).toBeInTheDocument();
134
471
  });
135
472
 
136
- fireEvent.click(screen.getByTestId("close-button"));
473
+ it("jumps to the page that was typed", () => {
474
+ const onNavigateToPage: MockFunction = openGoToPageModal();
475
+
476
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
477
+ target: { value: "17" },
478
+ });
479
+ fireEvent.click(screen.getByTestId("modal-footer-submit-button"));
137
480
 
138
- await waitFor(() => {
139
- expect(screen.queryByTestId("pagination-modal")).toBeNull();
481
+ expect(onNavigateToPage).toHaveBeenCalledWith(17, 10);
482
+ });
483
+
484
+ it("jumps on Enter as well as on the button", () => {
485
+ const onNavigateToPage: MockFunction = openGoToPageModal();
486
+
487
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
488
+ target: { value: "9" },
489
+ });
490
+ fireEvent.keyDown(screen.getByTestId("pagination-go-to-page-input"), {
491
+ key: "Enter",
492
+ });
493
+
494
+ expect(onNavigateToPage).toHaveBeenCalledWith(9, 10);
495
+ });
496
+
497
+ it("closes itself once it has navigated", () => {
498
+ openGoToPageModal();
499
+
500
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
501
+ target: { value: "17" },
502
+ });
503
+ fireEvent.click(screen.getByTestId("modal-footer-submit-button"));
504
+
505
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
506
+ });
507
+
508
+ it("pulls a page past the end back to the last page", () => {
509
+ const onNavigateToPage: MockFunction = openGoToPageModal();
510
+
511
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
512
+ target: { value: "9999" },
513
+ });
514
+ fireEvent.click(screen.getByTestId("modal-footer-submit-button"));
515
+
516
+ expect(onNavigateToPage).toHaveBeenCalledWith(24, 10);
517
+ });
518
+
519
+ it("pulls a page before the start back to the first page", () => {
520
+ const onNavigateToPage: MockFunction = openGoToPageModal({
521
+ currentPageNumber: 5,
522
+ });
523
+
524
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
525
+ target: { value: "-3" },
526
+ });
527
+ fireEvent.click(screen.getByTestId("modal-footer-submit-button"));
528
+
529
+ expect(onNavigateToPage).toHaveBeenCalledWith(1, 10);
530
+ });
531
+
532
+ it("cannot be submitted empty", () => {
533
+ const onNavigateToPage: MockFunction = openGoToPageModal();
534
+
535
+ expect(screen.getByTestId("modal-footer-submit-button")).toBeDisabled();
536
+ expect(onNavigateToPage).not.toHaveBeenCalled();
537
+ });
538
+
539
+ it("ignores text that is not a page number", () => {
540
+ const onNavigateToPage: MockFunction = openGoToPageModal();
541
+
542
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
543
+ target: { value: "abc" },
544
+ });
545
+ fireEvent.keyDown(screen.getByTestId("pagination-go-to-page-input"), {
546
+ key: "Enter",
547
+ });
548
+
549
+ expect(onNavigateToPage).not.toHaveBeenCalled();
550
+ });
551
+
552
+ it("can be dismissed without navigating", () => {
553
+ const onNavigateToPage: MockFunction = openGoToPageModal();
554
+
555
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
556
+ target: { value: "17" },
557
+ });
558
+ fireEvent.click(screen.getByTestId("modal-footer-close-button"));
559
+
560
+ expect(onNavigateToPage).not.toHaveBeenCalled();
561
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
562
+ });
563
+
564
+ it("does not remember the last page typed", () => {
565
+ openGoToPageModal();
566
+
567
+ fireEvent.change(screen.getByTestId("pagination-go-to-page-input"), {
568
+ target: { value: "17" },
569
+ });
570
+ fireEvent.click(screen.getByTestId("modal-footer-close-button"));
571
+ fireEvent.click(screen.getByTestId("pagination-ellipsis-end"));
572
+
573
+ expect(screen.getByTestId("pagination-go-to-page-input")).toHaveValue(
574
+ null,
575
+ );
576
+ });
577
+
578
+ it("closes when the page changes underneath it", () => {
579
+ const { rerender } = render(
580
+ <Pagination {...baseProps} onNavigateToPage={jest.fn()} />,
581
+ );
582
+
583
+ fireEvent.click(screen.getByTestId("pagination-ellipsis-end"));
584
+
585
+ expect(
586
+ screen.getByTestId("pagination-go-to-page-input"),
587
+ ).toBeInTheDocument();
588
+
589
+ rerender(
590
+ <Pagination
591
+ {...baseProps}
592
+ currentPageNumber={17}
593
+ onNavigateToPage={jest.fn()}
594
+ />,
595
+ );
596
+
597
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
598
+ });
599
+
600
+ it("does not open while loading", () => {
601
+ renderPagination({ isLoading: true });
602
+
603
+ fireEvent.click(screen.getByTestId("pagination-ellipsis-end"));
604
+
605
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
140
606
  });
141
607
  });
142
608
 
143
- it("shows Pagination Modal and submit with go to page", async () => {
144
- const mockOnNavigateToPage: MockFunction = getJestMockFunction();
145
- const props: ComponentProps = {
146
- currentPageNumber: 1,
147
- totalItemsCount: 20,
148
- itemsOnPage: 10,
149
- onNavigateToPage: mockOnNavigateToPage,
150
- isLoading: false,
151
- isError: false,
152
- singularLabel: "Item",
153
- pluralLabel: "Items",
154
- };
609
+ describe("small screens", () => {
610
+ /*
611
+ * The numbered list is hidden by a media query on narrow screens, so the
612
+ * control still has to say where the reader is.
613
+ */
614
+ it("carries a page-of-pages indicator", () => {
615
+ renderPagination({ currentPageNumber: 12 });
616
+
617
+ expect(
618
+ screen.getByTestId("pagination-current-page-indicator"),
619
+ ).toHaveTextContent("Page 12 of 24");
620
+ });
155
621
 
156
- render(<Pagination {...props} />);
622
+ it("groups the digits of a large page count", () => {
623
+ renderPagination({ totalItemsCount: 1000000 });
624
+
625
+ expect(
626
+ screen.getByTestId("pagination-current-page-indicator"),
627
+ ).toHaveTextContent(`Page 1 of ${(100000).toLocaleString()}`);
628
+ });
629
+ });
157
630
 
158
- fireEvent.click(screen.getByTestId("current-page-link"));
631
+ describe("accessibility", () => {
632
+ it("names the region for screen readers", () => {
633
+ renderPagination();
159
634
 
160
- await waitFor(() => {
161
- expect(screen.getByText("Navigate to Page")).toBeInTheDocument();
635
+ expect(
636
+ screen.getByRole("navigation", { name: "Pagination for Monitors" }),
637
+ ).toBeInTheDocument();
162
638
  });
163
639
 
164
- fireEvent.click(screen.getByText("Go to Page"));
640
+ it("labels the arrows", () => {
641
+ renderPagination();
165
642
 
166
- await waitFor(() => {
167
- expect(mockOnNavigateToPage).toHaveBeenCalledWith(1, 10);
643
+ expect(
644
+ screen.getByRole("button", { name: "Go to previous page" }),
645
+ ).toBeInTheDocument();
646
+ expect(
647
+ screen.getByRole("button", { name: "Go to next page" }),
648
+ ).toBeInTheDocument();
649
+ });
650
+
651
+ it("labels a page number as a jump target", () => {
652
+ renderPagination({ totalItemsCount: 30 });
653
+
654
+ expect(
655
+ screen.getByRole("button", { name: "Go to page 2" }),
656
+ ).toBeInTheDocument();
657
+ });
658
+
659
+ it("says what a collapsed gap does rather than reading as three dots", () => {
660
+ renderPagination({ currentPageNumber: 12 });
661
+
662
+ expect(
663
+ screen.getAllByRole("button", { name: "Go to a page in between" }),
664
+ ).toHaveLength(2);
665
+ });
666
+
667
+ it("passes the caller's test id through to the region", () => {
668
+ renderPagination({ dataTestId: "list-pagination" });
669
+
670
+ expect(screen.getByTestId("list-pagination")).toBeInTheDocument();
168
671
  });
169
672
  });
170
673
 
171
674
  /*
172
675
  * Has-more mode. The analytics list endpoints skip COUNT(*) and instead
173
676
  * over-fetch one probe row, so `totalItemsCount` is a lower bound that
174
- * includes a row the response dropped. The printed range has to come
175
- * from the rows the page really rendered.
677
+ * includes a row the response dropped. There is no last page to link to
678
+ * and the printed range has to come from the rows the page rendered.
176
679
  */
177
680
  describe("has-more mode", () => {
178
- const hasMoreProps: ComponentProps = {
681
+ const hasMoreProps: Partial<ComponentProps> = {
179
682
  currentPageNumber: 1,
180
683
  totalItemsCount: 11,
181
684
  itemsOnPage: 10,
182
685
  itemsOnCurrentPage: 10,
183
686
  hasMore: true,
184
- onNavigateToPage: jest.fn(),
185
- isLoading: false,
186
- isError: false,
187
687
  singularLabel: "Trace",
188
688
  pluralLabel: "Traces",
189
689
  };
190
690
 
191
691
  it("does not print the probe row the response dropped", () => {
192
- render(<Pagination {...hasMoreProps} />);
692
+ renderPagination(hasMoreProps);
193
693
 
194
- expect(screen.getByText("Showing 1 to 10+ traces.")).toBeInTheDocument();
195
- expect(screen.queryByText(/to 11\+/i)).toBeNull();
694
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
695
+ "Showing 1-10+ traces",
696
+ );
697
+ });
698
+
699
+ it("does not claim a total it cannot know", () => {
700
+ renderPagination(hasMoreProps);
701
+
702
+ expect(screen.getByTestId("pagination-summary")).not.toHaveTextContent(
703
+ "of 11",
704
+ );
196
705
  });
197
706
 
198
707
  it("keeps the range on the page when paging past the first page", () => {
199
- const props: ComponentProps = {
708
+ renderPagination({
200
709
  ...hasMoreProps,
201
710
  currentPageNumber: 3,
202
711
  totalItemsCount: 31,
203
- };
204
-
205
- render(<Pagination {...props} />);
712
+ });
206
713
 
207
- expect(screen.getByText("Showing 21 to 30+ traces.")).toBeInTheDocument();
714
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
715
+ "Showing 21-30+ traces",
716
+ );
208
717
  });
209
718
 
210
719
  it("prints a partial last page without the trailing plus", () => {
211
- const props: ComponentProps = {
720
+ renderPagination({
212
721
  ...hasMoreProps,
213
722
  currentPageNumber: 2,
214
723
  totalItemsCount: 13,
215
724
  itemsOnCurrentPage: 3,
216
725
  hasMore: false,
217
- };
218
-
219
- render(<Pagination {...props} />);
726
+ });
220
727
 
221
- expect(screen.getByText("Showing 11 to 13 traces.")).toBeInTheDocument();
728
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
729
+ "Showing 11-13 traces",
730
+ );
222
731
  });
223
732
 
224
733
  it("has no range to print for an empty page", () => {
225
- const props: ComponentProps = {
734
+ renderPagination({
226
735
  ...hasMoreProps,
227
736
  currentPageNumber: 2,
228
737
  totalItemsCount: 10,
229
738
  itemsOnCurrentPage: 0,
230
739
  hasMore: false,
231
- };
740
+ });
232
741
 
233
- render(<Pagination {...props} />);
234
-
235
- expect(screen.getByText("No traces.")).toBeInTheDocument();
742
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
743
+ "No traces",
744
+ );
236
745
  });
237
746
 
238
747
  /*
@@ -240,16 +749,82 @@ describe("Pagination", () => {
240
749
  * passes skip + rendered rows) are left to the count alone.
241
750
  */
242
751
  it("falls back to the count when the rendered row count is absent", () => {
243
- const props: ComponentProps = {
752
+ renderPagination({
244
753
  ...hasMoreProps,
245
754
  currentPageNumber: 2,
246
755
  totalItemsCount: 17,
247
756
  itemsOnCurrentPage: undefined,
248
- };
757
+ });
758
+
759
+ expect(screen.getByTestId("pagination-summary")).toHaveTextContent(
760
+ "Showing 11-17+ traces",
761
+ );
762
+ });
763
+
764
+ it("renders no page numbers, because there is no last page", () => {
765
+ renderPagination(hasMoreProps);
766
+
767
+ expect(screen.queryByTestId("pagination-page-1")).toBeNull();
768
+ expect(screen.queryByTestId("pagination-page-2")).toBeNull();
769
+ });
770
+
771
+ it("offers no gap to jump from, because the page count is unknown", () => {
772
+ renderPagination(hasMoreProps);
773
+
774
+ expect(screen.queryByTestId("pagination-ellipsis-start")).toBeNull();
775
+ expect(screen.queryByTestId("pagination-ellipsis-end")).toBeNull();
776
+ expect(screen.queryByTestId("pagination-go-to-page-input")).toBeNull();
777
+ });
778
+
779
+ it("still offers the page size inline", () => {
780
+ const onNavigateToPage: MockFunction = renderPagination(hasMoreProps);
781
+
782
+ fireEvent.change(screen.getByTestId("pagination-items-on-page-select"), {
783
+ target: { value: "25" },
784
+ });
785
+
786
+ expect(onNavigateToPage).toHaveBeenCalledWith(1, 25);
787
+ });
788
+
789
+ it("shows the page it is on without a page count", () => {
790
+ renderPagination({ ...hasMoreProps, currentPageNumber: 4 });
791
+
792
+ expect(
793
+ screen.getByTestId("pagination-current-page-indicator-desktop"),
794
+ ).toHaveTextContent("Page 4");
795
+ expect(
796
+ screen.getByTestId("pagination-current-page-indicator-desktop"),
797
+ ).not.toHaveTextContent("of");
798
+ });
799
+
800
+ it("pages forward while there is more to fetch", () => {
801
+ const onNavigateToPage: MockFunction = renderPagination(hasMoreProps);
802
+
803
+ fireEvent.click(screen.getByTestId("pagination-next-button"));
804
+
805
+ expect(onNavigateToPage).toHaveBeenCalledWith(2, 10);
806
+ });
807
+
808
+ it("stops at the page that reports no more rows", () => {
809
+ renderPagination({ ...hasMoreProps, hasMore: false });
810
+
811
+ expect(screen.getByTestId("pagination-next-button")).toBeDisabled();
812
+ });
813
+
814
+ /*
815
+ * The count is a lower bound, so it can be smaller than the rows already
816
+ * paged past. That must not disable the way back.
817
+ */
818
+ it("keeps the way back open on a later page", () => {
819
+ const onNavigateToPage: MockFunction = renderPagination({
820
+ ...hasMoreProps,
821
+ currentPageNumber: 3,
822
+ totalItemsCount: 31,
823
+ });
249
824
 
250
- render(<Pagination {...props} />);
825
+ fireEvent.click(screen.getByTestId("pagination-previous-button"));
251
826
 
252
- expect(screen.getByText("Showing 11 to 17+ traces.")).toBeInTheDocument();
827
+ expect(onNavigateToPage).toHaveBeenCalledWith(2, 10);
253
828
  });
254
829
  });
255
830
  });