@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
@@ -9,6 +9,7 @@ import {
9
9
  } from "@jest/globals";
10
10
  import {
11
11
  cleanup,
12
+ fireEvent,
12
13
  render,
13
14
  screen,
14
15
  waitFor,
@@ -159,6 +160,34 @@ const getItemMock: MockFunction = getJestMockFunction();
159
160
  const getCommonHeadersMock: MockFunction = getJestMockFunction();
160
161
  const apiGetMock: MockFunction = getJestMockFunction();
161
162
 
163
+ /*
164
+ * How many mocked reads are still in flight.
165
+ *
166
+ * This page issues its fetches from three places that do not know about each
167
+ * other - the identity read, the readiness read, and one severity read per rule
168
+ * table - and nothing orders them. Waiting on any single one of them, which is
169
+ * what this file used to do, leaves the others free to land after the assertions
170
+ * run: see `waitForSettledPage` for what that cost.
171
+ *
172
+ * A counter over the mocks themselves is the one signal that covers all three
173
+ * without the test having to know which fetch each surface makes.
174
+ */
175
+ let pendingRequestCount: number = 0;
176
+
177
+ type TrackRequestFunction = (result: unknown) => unknown;
178
+
179
+ const trackRequest: TrackRequestFunction = (result: unknown): unknown => {
180
+ if (!(result instanceof Promise)) {
181
+ return result;
182
+ }
183
+
184
+ pendingRequestCount++;
185
+
186
+ return result.finally((): void => {
187
+ pendingRequestCount--;
188
+ });
189
+ };
190
+
162
191
  /*
163
192
  * The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
164
193
  * requires, so the consts above are still in their temporal dead zone when the
@@ -169,10 +198,10 @@ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
169
198
  __esModule: true,
170
199
  default: {
171
200
  getList: (...args: Array<any>) => {
172
- return getListMock(...args);
201
+ return trackRequest(getListMock(...args));
173
202
  },
174
203
  getItem: (...args: Array<any>) => {
175
- return getItemMock(...args);
204
+ return trackRequest(getItemMock(...args));
176
205
  },
177
206
  getCommonHeaders: (...args: Array<any>) => {
178
207
  return getCommonHeadersMock(...args);
@@ -186,7 +215,7 @@ jest.mock("../../../UI/Utils/API/API", () => {
186
215
  __esModule: true,
187
216
  default: {
188
217
  get: (...args: Array<any>) => {
189
- return apiGetMock(...args);
218
+ return trackRequest(apiGetMock(...args));
190
219
  },
191
220
  /*
192
221
  * Both spellings are used by the page and they are handed different
@@ -273,13 +302,63 @@ interface CapturedTableProps {
273
302
  cardProps: { title: string; description: string };
274
303
  }
275
304
 
305
+ /*
306
+ * One entry per MOUNTED table, holding the props it was last rendered with.
307
+ *
308
+ * This was an append-only log of every render, and that is what made this file
309
+ * flaky. The page draws its tables as soon as it knows who it is about and draws
310
+ * them again when readiness lands, so the LENGTH of a render log is a transient:
311
+ * it passes through 8 on the way to 16, and which of the two a test observes
312
+ * depends on whether the readiness read or the severity reads win a race nothing
313
+ * in the page orders. `waitFor(() => expect(capturedTables).toHaveLength(8))`
314
+ * then asserted equality against a counter that is only briefly 8 - so whenever
315
+ * the second render landed first, 8 never came back, and the wait burned its
316
+ * full timeout before failing. A different handful of tests lost that race on
317
+ * every run, each costing a second of wall clock on the way out.
318
+ *
319
+ * Keyed by the table's own identity, the count settles at 8 and stays there, and
320
+ * every test reads the props the table actually ended up with.
321
+ */
276
322
  let capturedTables: Array<CapturedTableProps> = [];
277
323
 
324
+ type TableIdentityFunction = (props: CapturedTableProps) => string;
325
+
326
+ /*
327
+ * What makes a table THAT table: the rule type and the severity band it is
328
+ * mounted for. Deliberately not `userPreferencesKey`, even though that is
329
+ * shorter and unique - that key is itself under test below, where eight tables
330
+ * on one route must not share one, and keying the capture on the thing being
331
+ * asserted would quietly turn that assertion into a tautology.
332
+ */
333
+ const tableIdentity: TableIdentityFunction = (
334
+ props: CapturedTableProps,
335
+ ): string => {
336
+ const severityId: ObjectID | NotificationRuleType | undefined =
337
+ props.query["incidentSeverityId"] ?? props.query["alertSeverityId"];
338
+
339
+ return `${String(props.query["ruleType"])}::${
340
+ severityId ? severityId.toString() : "no-severity"
341
+ }`;
342
+ };
343
+
278
344
  jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
279
345
  return {
280
346
  __esModule: true,
281
347
  default: (props: CapturedTableProps) => {
282
- capturedTables.push(props);
348
+ const identity: string = tableIdentity(props);
349
+
350
+ const existingIndex: number = capturedTables.findIndex(
351
+ (candidate: CapturedTableProps): boolean => {
352
+ return tableIdentity(candidate) === identity;
353
+ },
354
+ );
355
+
356
+ if (existingIndex === -1) {
357
+ capturedTables.push(props);
358
+ } else {
359
+ capturedTables[existingIndex] = props;
360
+ }
361
+
283
362
  return null;
284
363
  },
285
364
  };
@@ -612,11 +691,67 @@ const pageProps: PageComponentProps = {
612
691
  hasPaymentMethod: false,
613
692
  };
614
693
 
694
+ const ADMIN_TABLE_COUNT: number = 8;
695
+ const SELF_SERVE_TABLE_COUNT: number = 2;
696
+
697
+ type WaitForSettledPageFunction = (expectedTableCount: number) => Promise<void>;
698
+
699
+ /*
700
+ * Wait until the page has stopped moving, rather than until one thing about it
701
+ * has happened.
702
+ *
703
+ * Two conditions, and neither is sufficient alone. `pendingRequestCount` reaches
704
+ * zero at the start, before anything has been asked for, and it dips to zero
705
+ * between the identity read finishing and the severity reads it unblocks being
706
+ * issued. The table count only reaches its total once every rule table has its
707
+ * severity list. Together they describe exactly one moment: every read this page
708
+ * makes has landed and every table it draws has been drawn with the result.
709
+ *
710
+ * The old wait was `expect(apiGetMock).toHaveBeenCalled()` - satisfied the
711
+ * instant the readiness request was ISSUED, with its response still in flight -
712
+ * followed by an exact-length check on a growing render log. Assertions
713
+ * therefore ran against whatever the page happened to look like mid-load, which
714
+ * is why several of them had to reach for the most recent render by hand, and
715
+ * why the ones that did not were the ones that failed on a busy machine.
716
+ *
717
+ * Both conditions are stable once true: nothing here issues a fetch after the
718
+ * tables are up, and the capture is keyed by table identity, so the count stops
719
+ * at its total instead of climbing past it.
720
+ */
721
+ const waitForSettledPage: WaitForSettledPageFunction = async (
722
+ expectedTableCount: number,
723
+ ): Promise<void> => {
724
+ await waitFor(
725
+ (): void => {
726
+ expect(pendingRequestCount).toBe(0);
727
+ expect(capturedTables).toHaveLength(expectedTableCount);
728
+ },
729
+ /*
730
+ * Everything here resolves from a mock, so a healthy page settles in
731
+ * milliseconds and this budget is never spent. It is above the one-second
732
+ * default because the runner this has to be green on is a shared CI box
733
+ * running the whole Common suite in band, and a page that settles slowly
734
+ * because the machine is busy is not a page that is broken. It stays below
735
+ * Jest's own five-second test timeout so a page that really is stuck still
736
+ * fails here, naming the count it reached, rather than as a bare timeout.
737
+ */
738
+ { timeout: 4000 },
739
+ );
740
+ };
741
+
615
742
  type RenderAdminPageFunction = (
616
743
  targetUserId?: string | undefined,
617
744
  ) => Promise<HTMLElement>;
618
745
 
619
746
  /*
747
+ * Render the admin page and return once it has finished loading.
748
+ *
749
+ * There is deliberately no shorter variant that returns earlier. This used to be
750
+ * two helpers - one that waited for the readiness request to be issued and one
751
+ * that waited for the tables on top of it - and every test that reached for the
752
+ * first was reading a page still mid-load. There is nothing here worth asserting
753
+ * before the page has settled, so the wait is not optional.
754
+ *
620
755
  * The user id is read off the URL at offset 1 - this is a sub-page, so the last
621
756
  * segment is "notification-rules" and the id is the one before it. Driving that
622
757
  * through a real jsdom URL rather than a stub is deliberate: reading the wrong
@@ -634,26 +769,11 @@ const renderAdminPage: RenderAdminPageFunction = async (
634
769
 
635
770
  const { container } = render(<UserViewNotificationRules {...pageProps} />);
636
771
 
637
- await waitFor((): void => {
638
- expect(apiGetMock).toHaveBeenCalled();
639
- });
772
+ await waitForSettledPage(ADMIN_TABLE_COUNT);
640
773
 
641
774
  return container;
642
775
  };
643
776
 
644
- type RenderAdminPageWithTablesFunction = () => Promise<HTMLElement>;
645
-
646
- const renderAdminPageWithTables: RenderAdminPageWithTablesFunction =
647
- async (): Promise<HTMLElement> => {
648
- const container: HTMLElement = await renderAdminPage();
649
-
650
- await waitFor((): void => {
651
- expect(capturedTables).toHaveLength(8);
652
- });
653
-
654
- return container;
655
- };
656
-
657
777
  type RenderSelfServePageFunction = (
658
778
  Page: FunctionComponent<PageComponentProps>,
659
779
  ) => Promise<void>;
@@ -663,9 +783,7 @@ const renderSelfServePage: RenderSelfServePageFunction = async (
663
783
  ): Promise<void> => {
664
784
  render(<Page {...pageProps} />);
665
785
 
666
- await waitFor((): void => {
667
- expect(capturedTables).toHaveLength(2);
668
- });
786
+ await waitForSettledPage(SELF_SERVE_TABLE_COUNT);
669
787
  };
670
788
 
671
789
  type TablesForFunction = (
@@ -699,32 +817,6 @@ const getCapturedTables: GetCapturedTablesFunction =
699
817
  return capturedTables;
700
818
  };
701
819
 
702
- /*
703
- * The most recent props each table was rendered with, keyed by the preferences
704
- * key that already uniquely identifies a table on this page.
705
- *
706
- * The page renders its tables as soon as it knows WHO it is about and re-renders
707
- * them when readiness lands, so `capturedTables` legitimately holds two entries
708
- * per table. Assertions about copy that depends on the readiness payload - the
709
- * empty-state sentence, which changes sign with the project's fallback switch -
710
- * have to read the settled render rather than whichever one happened to be
711
- * first.
712
- */
713
- type LatestTablesFunction = () => Array<CapturedTableProps>;
714
-
715
- const latestTables: LatestTablesFunction = (): Array<CapturedTableProps> => {
716
- const byKey: Map<string, CapturedTableProps> = new Map<
717
- string,
718
- CapturedTableProps
719
- >();
720
-
721
- for (const table of capturedTables) {
722
- byKey.set(table.userPreferencesKey, table);
723
- }
724
-
725
- return [...byKey.values()];
726
- };
727
-
728
820
  type FormFieldForFunction = (
729
821
  table: CapturedTableProps,
730
822
  key: string,
@@ -975,6 +1067,7 @@ const NO_ITEMS_MESSAGE: string =
975
1067
 
976
1068
  beforeEach((): void => {
977
1069
  capturedTables = [];
1070
+ pendingRequestCount = 0;
978
1071
 
979
1072
  getListMock.mockReset();
980
1073
  getItemMock.mockReset();
@@ -998,15 +1091,234 @@ beforeEach((): void => {
998
1091
  .mockReturnValue([Permission.ProjectAdmin]);
999
1092
  });
1000
1093
 
1001
- afterEach((): void => {
1094
+ afterEach(async (): Promise<void> => {
1002
1095
  cleanup();
1096
+
1097
+ /*
1098
+ * Unmounting stops the RENDERING, not the fetching. A rule table that belongs
1099
+ * to the viewer loads the seven notification-method models one after another,
1100
+ * so a test that reads its own page leaves a chain of awaits running after its
1101
+ * last assertion - and every one of those calls is recorded on the same
1102
+ * `getListMock` the next test resets and then asserts against.
1103
+ *
1104
+ * That is how "never reads a notification method model" comes to fail on a
1105
+ * page that never asked for one: the request was the previous test's. Letting
1106
+ * the chain finish here, while the mocks are still the ones that started it,
1107
+ * keeps each test's call log its own.
1108
+ */
1109
+ for (
1110
+ let attempt: number = 0;
1111
+ pendingRequestCount > 0 && attempt < 100;
1112
+ attempt++
1113
+ ) {
1114
+ await new Promise<void>((resolve: () => void): void => {
1115
+ setTimeout(resolve, 0);
1116
+ });
1117
+ }
1118
+
1003
1119
  jest.restoreAllMocks();
1004
1120
  });
1005
1121
 
1122
+ /*
1123
+ * ------------------------------------------------------------------ *
1124
+ * The harness, pinned
1125
+ * ------------------------------------------------------------------
1126
+ *
1127
+ * Everything below this block reads the page through `capturedTables`, and for
1128
+ * a while what it read depended on the weather.
1129
+ *
1130
+ * This page issues three kinds of read that nothing sequences: the identity, the
1131
+ * readiness summary, and one severity list per rule table. The tables appear
1132
+ * when the severity lists land; their props change again when readiness lands.
1133
+ * The file used to wait for the readiness request to be ISSUED and then for the
1134
+ * render log to be exactly eight entries long - a value that is true on the way
1135
+ * past, not at the end - so on a loaded machine the second redraw got there
1136
+ * first, eight never came back, and the wait spent its full second before
1137
+ * failing. Whichever tests happened to lose that race were the ones that went
1138
+ * red, which is why the failing set moved around between runs.
1139
+ *
1140
+ * These tests force both orderings on purpose rather than waiting to be unlucky
1141
+ * in CI. They are about the harness, not about the product, and they are here so
1142
+ * that the next person to reach for a render count in this file finds out
1143
+ * immediately.
1144
+ */
1145
+ describe("the page is read at rest, whichever order its fetches land in", () => {
1146
+ const SLOW_FETCH_MS: number = 25;
1147
+
1148
+ type AfterEverythingElseFunction = (value: unknown) => Promise<unknown>;
1149
+
1150
+ const afterEverythingElse: AfterEverythingElseFunction = (
1151
+ value: unknown,
1152
+ ): Promise<unknown> => {
1153
+ return new Promise((resolve: (settled: unknown) => void): void => {
1154
+ setTimeout((): void => {
1155
+ resolve(value);
1156
+ }, SLOW_FETCH_MS);
1157
+ });
1158
+ };
1159
+
1160
+ type DelayReadinessFunction = () => void;
1161
+
1162
+ /* Readiness lands last: the tables are drawn, then redrawn with the methods. */
1163
+ const readinessLandsAfterTheTables: DelayReadinessFunction = (): void => {
1164
+ const response: HTTPResponse<JSONObject> = new HTTPResponse<JSONObject>(
1165
+ 200,
1166
+ readinessJson(),
1167
+ {},
1168
+ );
1169
+
1170
+ apiGetMock.mockImplementation((): Promise<unknown> => {
1171
+ return afterEverythingElse(response);
1172
+ });
1173
+ };
1174
+
1175
+ type DelaySeveritiesFunction = () => void;
1176
+
1177
+ /* And the other way round: readiness is sitting there before a table exists. */
1178
+ const readinessLandsBeforeTheTables: DelaySeveritiesFunction = (): void => {
1179
+ const immediate: (data: any) => any =
1180
+ getListMock.getMockImplementation() as (data: any) => any;
1181
+
1182
+ getListMock.mockImplementation((data: any): unknown => {
1183
+ const result: Promise<unknown> = immediate(data) as Promise<unknown>;
1184
+
1185
+ if (
1186
+ data.modelType === IncidentSeverity ||
1187
+ data.modelType === AlertSeverity
1188
+ ) {
1189
+ return result.then((value: unknown): Promise<unknown> => {
1190
+ return afterEverythingElse(value);
1191
+ });
1192
+ }
1193
+
1194
+ return result;
1195
+ });
1196
+ };
1197
+
1198
+ type AssertSettledFunction = () => void;
1199
+
1200
+ /*
1201
+ * What "settled" has to mean, spelled out: not merely eight tables, but eight
1202
+ * tables holding the props they end up with. The two below are the props that
1203
+ * only exist once readiness and the project setting have both landed, so a
1204
+ * table captured mid-load fails them - which is exactly what several tests in
1205
+ * this file were quietly doing whenever they read `capturedTables[0]`.
1206
+ */
1207
+ const assertEveryTableIsSettled: AssertSettledFunction = (): void => {
1208
+ expect(capturedTables).toHaveLength(ADMIN_TABLE_COUNT);
1209
+
1210
+ capturedTables.forEach((table: CapturedTableProps) => {
1211
+ expect(methodOptionsFor(table)).toEqual([
1212
+ { label: `Email: ${MASKED_EMAIL}`, value: EMAIL_METHOD_ID },
1213
+ { label: `SMS: ${MASKED_PHONE}`, value: SMS_METHOD_ID },
1214
+ ]);
1215
+
1216
+ expect(table.noItemsMessage).toContain(
1217
+ "fall back to whatever verified method they have",
1218
+ );
1219
+ expect(table.noItemsMessage).not.toContain("could not be read");
1220
+ });
1221
+ };
1222
+
1223
+ test("captures one entry per mounted table, not one per redraw", async () => {
1224
+ await renderAdminPage();
1225
+
1226
+ const identities: Set<string> = new Set<string>(
1227
+ capturedTables.map((table: CapturedTableProps): string => {
1228
+ return `${String(table.query["ruleType"])}::${table.userPreferencesKey}`;
1229
+ }),
1230
+ );
1231
+
1232
+ /*
1233
+ * Four rule types, two severity bands each. If this ever reads more than the
1234
+ * number of tables actually on the route, the capture has gone back to being
1235
+ * a render log and every length assertion in this file is a race again.
1236
+ */
1237
+ expect(identities.size).toBe(ADMIN_TABLE_COUNT);
1238
+ expect(capturedTables).toHaveLength(ADMIN_TABLE_COUNT);
1239
+ });
1240
+
1241
+ test("a redraw replaces what a table captured rather than appending to it", async () => {
1242
+ await renderAdminPage();
1243
+
1244
+ const before: CapturedTableProps = capturedTables[0]!;
1245
+
1246
+ /*
1247
+ * Recheck refetches readiness and redraws all eight tables - the same second
1248
+ * pass that used to arrive uninvited and break the count. Driving it
1249
+ * deliberately is the cheapest way to prove the capture survives one.
1250
+ */
1251
+ fireEvent.click(screen.getByRole("button", { name: /Recheck/i }));
1252
+
1253
+ await waitForSettledPage(ADMIN_TABLE_COUNT);
1254
+
1255
+ expect(capturedTables).toHaveLength(ADMIN_TABLE_COUNT);
1256
+ expect(capturedTables[0]).not.toBe(before);
1257
+ expect(capturedTables[0]!.userPreferencesKey).toBe(
1258
+ before.userPreferencesKey,
1259
+ );
1260
+ });
1261
+
1262
+ test("hands the test a page with nothing still in flight", async () => {
1263
+ await renderAdminPage();
1264
+
1265
+ /*
1266
+ * The old wait was satisfied by the readiness request being ISSUED. A test
1267
+ * that then asserted on readiness-derived copy was asserting against a
1268
+ * response that had not arrived.
1269
+ */
1270
+ expect(pendingRequestCount).toBe(0);
1271
+ assertEveryTableIsSettled();
1272
+ });
1273
+
1274
+ test("reads the same page when readiness lands after the rule tables", async () => {
1275
+ readinessLandsAfterTheTables();
1276
+
1277
+ await renderAdminPage();
1278
+
1279
+ expect(pendingRequestCount).toBe(0);
1280
+ assertEveryTableIsSettled();
1281
+ });
1282
+
1283
+ test("reads the same page when readiness lands before the rule tables", async () => {
1284
+ readinessLandsBeforeTheTables();
1285
+
1286
+ await renderAdminPage();
1287
+
1288
+ expect(pendingRequestCount).toBe(0);
1289
+ assertEveryTableIsSettled();
1290
+ });
1291
+
1292
+ test("waits out the seven method reads a self-serve page makes for its owner", async () => {
1293
+ // A self-serve page is always the viewer's own, so this is the owner path.
1294
+ await renderSelfServePage(IncidentOnCallRules);
1295
+
1296
+ /*
1297
+ * A rule table belonging to the viewer loads the seven notification-method
1298
+ * models one after another, and unmounting the page does not stop that
1299
+ * chain - it stops the rendering. Any of those seven still running when the
1300
+ * next test resets `getListMock` is recorded as that test's request, which
1301
+ * is how "never reads a notification method model" comes to fail on a page
1302
+ * that never asked for one.
1303
+ *
1304
+ * Returning only once the chain is done is what keeps each test's call log
1305
+ * its own; the drain in `afterEach` is the same guarantee for a test that
1306
+ * renders without going through these helpers.
1307
+ */
1308
+ expect(pendingRequestCount).toBe(0);
1309
+
1310
+ const requested: Array<unknown> = requestedModelTypes();
1311
+
1312
+ for (const methodModel of NOTIFICATION_METHOD_MODELS) {
1313
+ expect(requested).toContain(methodModel);
1314
+ }
1315
+ });
1316
+ });
1317
+
1006
1318
  describe("the shared rules table, as the admin page wires it", () => {
1007
1319
  for (const testCase of RULE_TYPE_CASES) {
1008
1320
  test(`${testCase.label} rules are banded by their own severity model and foreign key column`, async () => {
1009
- await renderAdminPageWithTables();
1321
+ await renderAdminPage();
1010
1322
 
1011
1323
  const tables: Array<CapturedTableProps> = tablesFor(testCase.ruleType);
1012
1324
 
@@ -1038,7 +1350,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1038
1350
  });
1039
1351
 
1040
1352
  test(`${testCase.label} rules stamp the same column on a newly created rule`, async () => {
1041
- await renderAdminPageWithTables();
1353
+ await renderAdminPage();
1042
1354
 
1043
1355
  const table: CapturedTableProps = tablesFor(testCase.ruleType)[0]!;
1044
1356
 
@@ -1066,7 +1378,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1066
1378
  }
1067
1379
 
1068
1380
  test("every table is scoped to the user in the URL, never to the signed-in admin", async () => {
1069
- await renderAdminPageWithTables();
1381
+ await renderAdminPage();
1070
1382
 
1071
1383
  capturedTables.forEach((table: CapturedTableProps) => {
1072
1384
  expect(table.query["userId"]?.toString()).toBe(TARGET_USER_ID_STRING);
@@ -1091,7 +1403,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1091
1403
  });
1092
1404
 
1093
1405
  test("never reads a notification method model", async () => {
1094
- await renderAdminPageWithTables();
1406
+ await renderAdminPage();
1095
1407
 
1096
1408
  /*
1097
1409
  * The redesign, in one assertion.
@@ -1121,7 +1433,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1121
1433
  });
1122
1434
 
1123
1435
  test("asks the server for no column of a method model either", async () => {
1124
- await renderAdminPageWithTables();
1436
+ await renderAdminPage();
1125
1437
 
1126
1438
  /*
1127
1439
  * The same rule as the test above, applied to the OTHER way this page could
@@ -1143,7 +1455,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1143
1455
  * So the readiness card masked every identifier and the table one card below
1144
1456
  * printed them in full.
1145
1457
  */
1146
- for (const table of latestTables()) {
1458
+ for (const table of capturedTables) {
1147
1459
  const projection: Array<string> = Object.keys(table.selectMoreFields);
1148
1460
 
1149
1461
  for (const column of table.columns) {
@@ -1165,10 +1477,10 @@ describe("the shared rules table, as the admin page wires it", () => {
1165
1477
  });
1166
1478
 
1167
1479
  test("labels a listed rule with the masked identifier, not the raw one", async () => {
1168
- await renderAdminPageWithTables();
1480
+ await renderAdminPage();
1169
1481
 
1170
1482
  const methodColumn: CapturedColumn = columnNamed(
1171
- latestTables()[0]!,
1483
+ capturedTables[0]!,
1172
1484
  "Notification Method",
1173
1485
  );
1174
1486
 
@@ -1189,9 +1501,9 @@ describe("the shared rules table, as the admin page wires it", () => {
1189
1501
  });
1190
1502
 
1191
1503
  test("builds the rule form's method dropdown out of the masked readiness payload", async () => {
1192
- await renderAdminPageWithTables();
1504
+ await renderAdminPage();
1193
1505
 
1194
- latestTables().forEach((table: CapturedTableProps) => {
1506
+ capturedTables.forEach((table: CapturedTableProps) => {
1195
1507
  /*
1196
1508
  * Masked label, real foreign key. The admin picks "SMS: +1 ••• ••• 4821"
1197
1509
  * and the form submits `userSmsId`, so the page can point a rule at a
@@ -1205,9 +1517,9 @@ describe("the shared rules table, as the admin page wires it", () => {
1205
1517
  });
1206
1518
 
1207
1519
  test("the dropdown withholds the unverified method", async () => {
1208
- await renderAdminPageWithTables();
1520
+ await renderAdminPage();
1209
1521
 
1210
- const values: Array<string> = methodOptionsFor(latestTables()[0]!).map(
1522
+ const values: Array<string> = methodOptionsFor(capturedTables[0]!).map(
1211
1523
  (option: { label: string; value: string }) => {
1212
1524
  return option.value;
1213
1525
  },
@@ -1223,10 +1535,10 @@ describe("the shared rules table, as the admin page wires it", () => {
1223
1535
  });
1224
1536
 
1225
1537
  test("a rule created from that dropdown points at the method's own column", async () => {
1226
- await renderAdminPageWithTables();
1538
+ await renderAdminPage();
1227
1539
 
1228
1540
  const created: UserNotificationRule =
1229
- await latestTables()[0]!.onBeforeCreate(new UserNotificationRule(), {
1541
+ await capturedTables[0]!.onBeforeCreate(new UserNotificationRule(), {
1230
1542
  notificationMethod: SMS_METHOD_ID,
1231
1543
  });
1232
1544
 
@@ -1246,7 +1558,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1246
1558
  new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
1247
1559
  );
1248
1560
 
1249
- await renderAdminPageWithTables();
1561
+ await renderAdminPage();
1250
1562
 
1251
1563
  /*
1252
1564
  * The failure mode this seam had to be designed around. Readiness is the
@@ -1259,14 +1571,14 @@ describe("the shared rules table, as the admin page wires it", () => {
1259
1571
  expect(requestedModelTypes()).not.toContain(methodModel);
1260
1572
  }
1261
1573
 
1262
- latestTables().forEach((table: CapturedTableProps) => {
1574
+ capturedTables.forEach((table: CapturedTableProps) => {
1263
1575
  expect(methodOptionsFor(table)).toEqual([]);
1264
1576
  expect(table.isCreateable).toBe(true);
1265
1577
  });
1266
1578
  });
1267
1579
 
1268
1580
  test("keeps the severity id in every preferences key and namespaces the admin surface away from user settings", async () => {
1269
- await renderAdminPageWithTables();
1581
+ await renderAdminPage();
1270
1582
 
1271
1583
  const adminKeys: Array<string> = capturedTables.map(
1272
1584
  (table: CapturedTableProps) => {
@@ -1320,7 +1632,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1320
1632
  });
1321
1633
 
1322
1634
  test("never mounts a table over a notification method model", async () => {
1323
- await renderAdminPageWithTables();
1635
+ await renderAdminPage();
1324
1636
 
1325
1637
  /*
1326
1638
  * The structural form of "an admin may not add a method for somebody else".
@@ -1466,7 +1778,7 @@ describe("the self-serve settings pages are unchanged by the extraction", () =>
1466
1778
 
1467
1779
  describe("the on-behalf-of banner", () => {
1468
1780
  test("names the person being edited and says the change is recorded and disclosed", async () => {
1469
- const container: HTMLElement = await renderAdminPageWithTables();
1781
+ const container: HTMLElement = await renderAdminPage();
1470
1782
 
1471
1783
  expect(container.textContent).toContain(
1472
1784
  `You are editing on behalf of ${TARGET_USER_NAME}`,
@@ -1489,7 +1801,7 @@ describe("the on-behalf-of banner", () => {
1489
1801
  .spyOn(PermissionUtil, "getAllPermissions")
1490
1802
  .mockReturnValue([Permission.ReadProjectUserNotificationRule]);
1491
1803
 
1492
- const container: HTMLElement = await renderAdminPageWithTables();
1804
+ const container: HTMLElement = await renderAdminPage();
1493
1805
 
1494
1806
  expect(container.textContent).toContain(
1495
1807
  `You are viewing ${TARGET_USER_NAME}`,
@@ -1523,7 +1835,7 @@ describe("the on-behalf-of banner", () => {
1523
1835
  .spyOn(PermissionUtil, "getAllPermissions")
1524
1836
  .mockReturnValue([Permission.ProjectOwner]);
1525
1837
 
1526
- await renderAdminPageWithTables();
1838
+ await renderAdminPage();
1527
1839
 
1528
1840
  capturedTables.forEach((table: CapturedTableProps) => {
1529
1841
  expect(table.isCreateable).toBe(true);
@@ -1537,7 +1849,7 @@ describe("the on-behalf-of banner", () => {
1537
1849
  .mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
1538
1850
  jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
1539
1851
 
1540
- const container: HTMLElement = await renderAdminPageWithTables();
1852
+ const container: HTMLElement = await renderAdminPage();
1541
1853
 
1542
1854
  expect(container.textContent).toContain(
1543
1855
  "These are your own notification rules.",
@@ -1584,7 +1896,7 @@ describe("the on-behalf-of banner", () => {
1584
1896
  new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
1585
1897
  );
1586
1898
 
1587
- const container: HTMLElement = await renderAdminPageWithTables();
1899
+ const container: HTMLElement = await renderAdminPage();
1588
1900
 
1589
1901
  expect(container.textContent).toContain("readiness is down");
1590
1902
  expect(container.textContent).toContain(
@@ -1617,7 +1929,7 @@ describe("the on-behalf-of banner", () => {
1617
1929
 
1618
1930
  describe("notification methods are read-only", () => {
1619
1931
  test("lists each method masked, with its verification state", async () => {
1620
- await renderAdminPageWithTables();
1932
+ await renderAdminPage();
1621
1933
 
1622
1934
  const methods: HTMLElement = cardNamed("Notification methods");
1623
1935
 
@@ -1634,7 +1946,7 @@ describe("notification methods are read-only", () => {
1634
1946
  });
1635
1947
 
1636
1948
  test("offers no control for adding a method on the user's behalf", async () => {
1637
- await renderAdminPageWithTables();
1949
+ await renderAdminPage();
1638
1950
 
1639
1951
  const methods: HTMLElement = cardNamed("Notification methods");
1640
1952
 
@@ -1662,7 +1974,7 @@ describe("notification methods are read-only", () => {
1662
1974
  test("the no-methods empty state offers a reminder rather than a form", async () => {
1663
1975
  respondWithReadiness(NO_METHODS_READINESS);
1664
1976
 
1665
- await renderAdminPageWithTables();
1977
+ await renderAdminPage();
1666
1978
 
1667
1979
  const methods: HTMLElement = cardNamed("Notification methods");
1668
1980
 
@@ -1694,7 +2006,7 @@ describe("notification methods are read-only", () => {
1694
2006
  test("the rule tables stay editable while the methods stay locked", async () => {
1695
2007
  respondWithReadiness(NO_METHODS_READINESS);
1696
2008
 
1697
- await renderAdminPageWithTables();
2009
+ await renderAdminPage();
1698
2010
 
1699
2011
  /*
1700
2012
  * The whole design of this phase in one assertion: rules are the admin's to
@@ -1727,14 +2039,14 @@ describe("notification methods are read-only", () => {
1727
2039
  */
1728
2040
  describe("the coverage grid, now shared with the readiness page", () => {
1729
2041
  test("draws one matrix per severity kind", async () => {
1730
- await renderAdminPageWithTables();
2042
+ await renderAdminPage();
1731
2043
 
1732
2044
  expect(matrixFor("Incident severities")).toBeInTheDocument();
1733
2045
  expect(matrixFor("Alert severities")).toBeInTheDocument();
1734
2046
  });
1735
2047
 
1736
2048
  test("neither kind's severities appear under the other kind's columns", async () => {
1737
- await renderAdminPageWithTables();
2049
+ await renderAdminPage();
1738
2050
 
1739
2051
  const incidentMatrix: HTMLElement = matrixFor("Incident severities");
1740
2052
  const alertMatrix: HTMLElement = matrixFor("Alert severities");
@@ -1755,7 +2067,7 @@ describe("the coverage grid, now shared with the readiness page", () => {
1755
2067
  });
1756
2068
 
1757
2069
  test("marks the severity with no rule as a gap rather than as covered", async () => {
1758
- await renderAdminPageWithTables();
2070
+ await renderAdminPage();
1759
2071
 
1760
2072
  const incidentMatrix: HTMLElement = matrixFor("Incident severities");
1761
2073
 
@@ -1770,7 +2082,7 @@ describe("the coverage grid, now shared with the readiness page", () => {
1770
2082
  });
1771
2083
 
1772
2084
  test("a severity-less rule type is listed apart from the grids", async () => {
1773
- const container: HTMLElement = await renderAdminPageWithTables();
2085
+ const container: HTMLElement = await renderAdminPage();
1774
2086
 
1775
2087
  /*
1776
2088
  * WHEN_USER_GOES_ON_CALL carries no severity at all. Putting it in a
@@ -1794,7 +2106,7 @@ describe("the coverage grid, now shared with the readiness page", () => {
1794
2106
  */
1795
2107
  describe("the rules are editable, not merely addable and removable", () => {
1796
2108
  test("an admin gets the row editor on every table", async () => {
1797
- await renderAdminPageWithTables();
2109
+ await renderAdminPage();
1798
2110
 
1799
2111
  capturedTables.forEach((table: CapturedTableProps) => {
1800
2112
  expect(table.isEditable).toBe(true);
@@ -1807,7 +2119,7 @@ describe("the rules are editable, not merely addable and removable", () => {
1807
2119
  .mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
1808
2120
  jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
1809
2121
 
1810
- await renderAdminPageWithTables();
2122
+ await renderAdminPage();
1811
2123
 
1812
2124
  capturedTables.forEach((table: CapturedTableProps) => {
1813
2125
  expect(table.isEditable).toBe(true);
@@ -1815,7 +2127,7 @@ describe("the rules are editable, not merely addable and removable", () => {
1815
2127
  });
1816
2128
 
1817
2129
  test("the form offers the delay, which is what the server lets an editor change", async () => {
1818
- await renderAdminPageWithTables();
2130
+ await renderAdminPage();
1819
2131
 
1820
2132
  const table: CapturedTableProps = capturedTables[0]!;
1821
2133
  const delayField: CapturedFormField = formFieldFor(
@@ -1834,7 +2146,7 @@ describe("the rules are editable, not merely addable and removable", () => {
1834
2146
  });
1835
2147
 
1836
2148
  test("the form withholds the method dropdown when editing, because update cannot carry it", async () => {
1837
- await renderAdminPageWithTables();
2149
+ await renderAdminPage();
1838
2150
 
1839
2151
  const methodField: CapturedFormField = formFieldFor(
1840
2152
  capturedTables[0]!,
@@ -1855,7 +2167,7 @@ describe("the rules are editable, not merely addable and removable", () => {
1855
2167
  });
1856
2168
 
1857
2169
  test("the form offers nothing the model refuses to update", async () => {
1858
- await renderAdminPageWithTables();
2170
+ await renderAdminPage();
1859
2171
 
1860
2172
  const keys: Array<string> = formFieldKeys(capturedTables[0]!);
1861
2173
 
@@ -1884,7 +2196,7 @@ describe("the rules are editable, not merely addable and removable", () => {
1884
2196
  */
1885
2197
  describe("the page says the same thing at the moment of the write", () => {
1886
2198
  test("the modal, the create button and the delete prompt all name the person", async () => {
1887
- await renderAdminPageWithTables();
2199
+ await renderAdminPage();
1888
2200
 
1889
2201
  /*
1890
2202
  * ModelTable derives the modal title, the create button and the delete
@@ -1901,7 +2213,7 @@ describe("the page says the same thing at the moment of the write", () => {
1901
2213
  });
1902
2214
 
1903
2215
  test("the form fields stop speaking in the first person", async () => {
1904
- await renderAdminPageWithTables();
2216
+ await renderAdminPage();
1905
2217
 
1906
2218
  const table: CapturedTableProps = capturedTables[0]!;
1907
2219
 
@@ -1927,7 +2239,7 @@ describe("the page says the same thing at the moment of the write", () => {
1927
2239
  .spyOn(UserUtil, "getUserId")
1928
2240
  .mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
1929
2241
 
1930
- await renderAdminPageWithTables();
2242
+ await renderAdminPage();
1931
2243
 
1932
2244
  const table: CapturedTableProps = capturedTables[0]!;
1933
2245
 
@@ -1938,7 +2250,7 @@ describe("the page says the same thing at the moment of the write", () => {
1938
2250
  });
1939
2251
 
1940
2252
  test("an opt-out row is labelled rather than left blank", async () => {
1941
- await renderAdminPageWithTables();
2253
+ await renderAdminPage();
1942
2254
 
1943
2255
  const methodColumn: CapturedColumn = columnNamed(
1944
2256
  capturedTables[0]!,
@@ -1963,7 +2275,7 @@ describe("the page says the same thing at the moment of the write", () => {
1963
2275
  });
1964
2276
 
1965
2277
  test("an ordinary rule is not labelled as muted", async () => {
1966
- await renderAdminPageWithTables();
2278
+ await renderAdminPage();
1967
2279
 
1968
2280
  const methodColumn: CapturedColumn = columnNamed(
1969
2281
  capturedTables[0]!,
@@ -1979,7 +2291,7 @@ describe("the page says the same thing at the moment of the write", () => {
1979
2291
  });
1980
2292
 
1981
2293
  test("the opt-out row is selected for, or the label could never fire", async () => {
1982
- await renderAdminPageWithTables();
2294
+ await renderAdminPage();
1983
2295
 
1984
2296
  /*
1985
2297
  * The cell can only branch on a column the table actually asked the API
@@ -1993,9 +2305,9 @@ describe("the page says the same thing at the moment of the write", () => {
1993
2305
  });
1994
2306
 
1995
2307
  test("the empty state promises a fallback only where the project has one", async () => {
1996
- await renderAdminPageWithTables();
2308
+ await renderAdminPage();
1997
2309
 
1998
- latestTables().forEach((table: CapturedTableProps) => {
2310
+ capturedTables.forEach((table: CapturedTableProps) => {
1999
2311
  expect(table.noItemsMessage).toContain(
2000
2312
  "fall back to whatever verified method they have",
2001
2313
  );
@@ -2007,7 +2319,7 @@ describe("the page says the same thing at the moment of the write", () => {
2007
2319
  project.disableOnCallNotificationFallback = true;
2008
2320
  getItemMock.mockResolvedValue(project as never);
2009
2321
 
2010
- await renderAdminPageWithTables();
2322
+ await renderAdminPage();
2011
2323
 
2012
2324
  /*
2013
2325
  * The same hole means two different things depending on one project
@@ -2016,11 +2328,9 @@ describe("the page says the same thing at the moment of the write", () => {
2016
2328
  * on one screen. With the switch off a missing rule is not a late page, it
2017
2329
  * is no page.
2018
2330
  */
2019
- await waitFor((): void => {
2020
- latestTables().forEach((table: CapturedTableProps) => {
2021
- expect(table.noItemsMessage).toContain("dropped");
2022
- expect(table.noItemsMessage).not.toContain("fall back to whatever");
2023
- });
2331
+ capturedTables.forEach((table: CapturedTableProps) => {
2332
+ expect(table.noItemsMessage).toContain("dropped");
2333
+ expect(table.noItemsMessage).not.toContain("fall back to whatever");
2024
2334
  });
2025
2335
  });
2026
2336
 
@@ -2029,7 +2339,7 @@ describe("the page says the same thing at the moment of the write", () => {
2029
2339
  new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
2030
2340
  );
2031
2341
 
2032
- await renderAdminPageWithTables();
2342
+ await renderAdminPage();
2033
2343
 
2034
2344
  /*
2035
2345
  * A failed read is not a project with the fallback off. Saying "your pages
@@ -2037,12 +2347,10 @@ describe("the page says the same thing at the moment of the write", () => {
2037
2347
  * claim, and saying they fall back is the comforting version of the same
2038
2348
  * mistake.
2039
2349
  */
2040
- await waitFor((): void => {
2041
- latestTables().forEach((table: CapturedTableProps) => {
2042
- expect(table.noItemsMessage).toContain("could not be read");
2043
- expect(table.noItemsMessage).not.toContain("dropped");
2044
- expect(table.noItemsMessage).not.toContain("fall back to whatever");
2045
- });
2350
+ capturedTables.forEach((table: CapturedTableProps) => {
2351
+ expect(table.noItemsMessage).toContain("could not be read");
2352
+ expect(table.noItemsMessage).not.toContain("dropped");
2353
+ expect(table.noItemsMessage).not.toContain("fall back to whatever");
2046
2354
  });
2047
2355
  });
2048
2356
  });
@@ -2070,7 +2378,7 @@ describe("no unmasked identifier reaches the DOM", () => {
2070
2378
  };
2071
2379
 
2072
2380
  test("the admin page renders masked identifiers only", async () => {
2073
- const container: HTMLElement = await renderAdminPageWithTables();
2381
+ const container: HTMLElement = await renderAdminPage();
2074
2382
 
2075
2383
  // The masked forms are present, so this is not passing by rendering nothing.
2076
2384
  expect(container.textContent).toContain(MASKED_EMAIL);
@@ -2082,7 +2390,7 @@ describe("no unmasked identifier reaches the DOM", () => {
2082
2390
  test("an unreachable user's empty state leaks nothing either", async () => {
2083
2391
  respondWithReadiness(NO_METHODS_READINESS);
2084
2392
 
2085
- const container: HTMLElement = await renderAdminPageWithTables();
2393
+ const container: HTMLElement = await renderAdminPage();
2086
2394
 
2087
2395
  expect(container.textContent).toContain(
2088
2396
  "has no notification methods at all",
@@ -2098,11 +2406,11 @@ describe("no unmasked identifier reaches the DOM", () => {
2098
2406
  * they are checked at the prop.
2099
2407
  */
2100
2408
  test("no dropdown label carries a raw identifier", async () => {
2101
- await renderAdminPageWithTables();
2409
+ await renderAdminPage();
2102
2410
 
2103
2411
  const labels: Array<string> = [];
2104
2412
 
2105
- latestTables().forEach((table: CapturedTableProps) => {
2413
+ capturedTables.forEach((table: CapturedTableProps) => {
2106
2414
  methodOptionsFor(table).forEach(
2107
2415
  (option: { label: string; value: string }) => {
2108
2416
  labels.push(option.label);
@@ -2133,7 +2441,7 @@ describe("no unmasked identifier reaches the DOM", () => {
2133
2441
  test("the mail draft carries the login address and no method identifier", async () => {
2134
2442
  respondWithReadiness(NO_METHODS_READINESS);
2135
2443
 
2136
- await renderAdminPageWithTables();
2444
+ await renderAdminPage();
2137
2445
 
2138
2446
  const href: string = mailtoHrefIn(cardNamed("Notification methods"));
2139
2447