@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.
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
/*
|
|
4
|
+
* `act` from React itself rather than from @testing-library/react: on React
|
|
5
|
+
* 18.3 the latter still forwards to the deprecated ReactDOMTestUtils.act, which
|
|
6
|
+
* React asks you not to import.
|
|
7
|
+
*/
|
|
8
|
+
import React, { act } from "react";
|
|
9
|
+
import {
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
jest,
|
|
15
|
+
test,
|
|
16
|
+
} from "@jest/globals";
|
|
17
|
+
import type { Mock } from "jest-mock";
|
|
18
|
+
import { Location, MemoryRouter } from "react-router-dom";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* "Add to Project" is the one bulk action on the Users table that cannot run
|
|
22
|
+
* from the bulk bar alone - it needs a project and one of that project's teams
|
|
23
|
+
* first. So the page splits the action in two: the click only opens a picker,
|
|
24
|
+
* and the memberships are created after the picker is submitted.
|
|
25
|
+
*
|
|
26
|
+
* That split is the whole risk. The shared bulk machinery hands the click four
|
|
27
|
+
* callbacks and then waits: the progress modal appears on `onBulkActionStart`,
|
|
28
|
+
* its Close button stays disabled until `onBulkActionEnd`, and only closing it
|
|
29
|
+
* clears the selection and refetches the table. Call them in the wrong order,
|
|
30
|
+
* or miss one on a path, and the admin is left either staring at an empty
|
|
31
|
+
* progress modal behind the picker or at a modal they can never close.
|
|
32
|
+
*
|
|
33
|
+
* None of that is visible from either component on its own - the creates live
|
|
34
|
+
* in BulkAddUsersToProject and the picker only collects - so this file pins the
|
|
35
|
+
* page's sequencing, with both of them stubbed.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
jest.mock("react-i18next", () => {
|
|
39
|
+
return {
|
|
40
|
+
useTranslation: () => {
|
|
41
|
+
return {
|
|
42
|
+
t: (key: string): string => {
|
|
43
|
+
return key;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
jest.mock("../../../UI/Utils/Permission", () => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: {
|
|
54
|
+
getAllPermissions: () => {
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
getProjectPermissions: () => {
|
|
58
|
+
return [];
|
|
59
|
+
},
|
|
60
|
+
getGlobalPermissions: () => {
|
|
61
|
+
return [];
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
jest.mock("../../../UI/Utils/User", () => {
|
|
68
|
+
return {
|
|
69
|
+
__esModule: true,
|
|
70
|
+
default: {
|
|
71
|
+
isMasterAdmin: () => {
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
getUserId: () => {
|
|
75
|
+
return null;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* The real ModelTable would drag in the pager, the URL state and a live fetch.
|
|
83
|
+
* What this file is about is the props the page hands it - here the bulk action
|
|
84
|
+
* declarations - so a stand-in that records them is both enough and more
|
|
85
|
+
* precise.
|
|
86
|
+
*/
|
|
87
|
+
type CapturedTableProps = {
|
|
88
|
+
bulkActions?: { buttons: Array<BulkActionButtonSchema<User>> } | undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let capturedTableProps: CapturedTableProps | null = null;
|
|
92
|
+
|
|
93
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
94
|
+
return {
|
|
95
|
+
__esModule: true,
|
|
96
|
+
default: (props: CapturedTableProps) => {
|
|
97
|
+
capturedTableProps = props;
|
|
98
|
+
return null;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* Every step the page takes, in the order it took it. The three bulk callbacks
|
|
105
|
+
* are recorded here rather than only counted because "was called" is not the
|
|
106
|
+
* property that matters - `onBulkActionEnd` firing before the creates finish
|
|
107
|
+
* would re-enable the progress modal's Close button mid-run, and the selection
|
|
108
|
+
* would clear while memberships were still being created.
|
|
109
|
+
*/
|
|
110
|
+
const recordedEvents: Array<string> = [];
|
|
111
|
+
|
|
112
|
+
const PICKER_TEST_ID: string = "bulk-add-users-to-project-picker";
|
|
113
|
+
|
|
114
|
+
type CapturedModalProps = {
|
|
115
|
+
users: Array<User>;
|
|
116
|
+
onClose: () => void;
|
|
117
|
+
onSubmit: (selection: BulkAddUsersToProjectSelection) => void;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let capturedModalProps: CapturedModalProps | null = null;
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* The picker stands in for the two-step project/team form. Stubbing it lets the
|
|
124
|
+
* test submit a selection directly, and - because the stub renders a node and
|
|
125
|
+
* records its own teardown - lets the test see exactly when the page took the
|
|
126
|
+
* picker off the screen.
|
|
127
|
+
*/
|
|
128
|
+
jest.mock(
|
|
129
|
+
"../../../../App/FeatureSet/AdminDashboard/src/Components/User/BulkAddUsersToProjectModal",
|
|
130
|
+
() => {
|
|
131
|
+
return {
|
|
132
|
+
__esModule: true,
|
|
133
|
+
default: (props: CapturedModalProps) => {
|
|
134
|
+
capturedModalProps = props;
|
|
135
|
+
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
return () => {
|
|
138
|
+
recordedEvents.push("picker-closed");
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
return React.createElement("div", { "data-testid": PICKER_TEST_ID });
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
type BulkAddUsersToProjectFunction = (
|
|
149
|
+
options: BulkAddUsersToProjectOptions,
|
|
150
|
+
) => Promise<BulkAddUsersToProjectResult>;
|
|
151
|
+
|
|
152
|
+
const bulkAddUsersToProjectMock: Mock<BulkAddUsersToProjectFunction> =
|
|
153
|
+
jest.fn<BulkAddUsersToProjectFunction>();
|
|
154
|
+
|
|
155
|
+
/*
|
|
156
|
+
* The creates themselves have their own tests. Stubbing them here leaves only
|
|
157
|
+
* the page's half under test: what it passes down, how it renames the running
|
|
158
|
+
* tally on the way back up, and what it does when the run blows up.
|
|
159
|
+
*/
|
|
160
|
+
jest.mock(
|
|
161
|
+
"../../../../App/FeatureSet/AdminDashboard/src/Components/User/BulkAddUsersToProject",
|
|
162
|
+
() => {
|
|
163
|
+
return {
|
|
164
|
+
__esModule: true,
|
|
165
|
+
bulkAddUsersToProject: (options: BulkAddUsersToProjectOptions) => {
|
|
166
|
+
recordedEvents.push("creates-started");
|
|
167
|
+
return bulkAddUsersToProjectMock(options);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
import Users from "../../../../App/FeatureSet/AdminDashboard/src/Pages/Users/Index";
|
|
174
|
+
import {
|
|
175
|
+
BulkAddUsersToProjectOptions,
|
|
176
|
+
BulkAddUsersToProjectResult,
|
|
177
|
+
} from "../../../../App/FeatureSet/AdminDashboard/src/Components/User/BulkAddUsersToProject";
|
|
178
|
+
import { BulkAddUsersToProjectSelection } from "../../../../App/FeatureSet/AdminDashboard/src/Components/User/BulkAddUsersToProjectModal";
|
|
179
|
+
import {
|
|
180
|
+
BulkActionButtonSchema,
|
|
181
|
+
ProgressInfo,
|
|
182
|
+
} from "../../../UI/Components/BulkUpdate/BulkUpdateForm";
|
|
183
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
184
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
185
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
186
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
187
|
+
|
|
188
|
+
const ADD_TO_PROJECT_TITLE_KEY: string = "pages.users.bulkAddToProject";
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* What the server throws when a selected user is already on the team. It is a
|
|
192
|
+
* normal outcome of a bulk add - a few of the selected users being on the
|
|
193
|
+
* project already must not stop the rest - so it travels as a per-user message
|
|
194
|
+
* rather than as a failure of the run.
|
|
195
|
+
*/
|
|
196
|
+
const ALREADY_INVITED_MESSAGE: string =
|
|
197
|
+
"This user has already been invited to this team";
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* What the run itself failing looks like - the request never left, the session
|
|
201
|
+
* expired. Distinct from ALREADY_INVITED_MESSAGE above, which is a per-user
|
|
202
|
+
* outcome the creates report and carry on from.
|
|
203
|
+
*/
|
|
204
|
+
const RUN_BROKE_MESSAGE: string =
|
|
205
|
+
"Error connecting to server. Please try again.";
|
|
206
|
+
|
|
207
|
+
const SELECTION: BulkAddUsersToProjectSelection = {
|
|
208
|
+
projectId: ObjectID.generate(),
|
|
209
|
+
teamId: ObjectID.generate(),
|
|
210
|
+
hasAcceptedInvitation: true,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* The same pick with the auto-accept checkbox left alone, which is its default.
|
|
215
|
+
* It exists so the flag is exercised in both positions: a page that ignored the
|
|
216
|
+
* picker's answer and always sent `true` would be indistinguishable from a
|
|
217
|
+
* correct one if every test submitted the same selection.
|
|
218
|
+
*/
|
|
219
|
+
const SELECTION_WITHOUT_AUTO_ACCEPT: BulkAddUsersToProjectSelection = {
|
|
220
|
+
projectId: ObjectID.generate(),
|
|
221
|
+
teamId: ObjectID.generate(),
|
|
222
|
+
hasAcceptedInvitation: false,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
type UserFactoryFunction = () => User;
|
|
226
|
+
|
|
227
|
+
const aUser: UserFactoryFunction = (): User => {
|
|
228
|
+
const user: User = new User();
|
|
229
|
+
user.id = ObjectID.generate();
|
|
230
|
+
return user;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let userOne: User = aUser();
|
|
234
|
+
let userTwo: User = aUser();
|
|
235
|
+
let userThree: User = aUser();
|
|
236
|
+
let userFour: User = aUser();
|
|
237
|
+
let userFive: User = aUser();
|
|
238
|
+
|
|
239
|
+
type ResultFactoryFunction = (
|
|
240
|
+
users: Array<User>,
|
|
241
|
+
) => BulkAddUsersToProjectResult;
|
|
242
|
+
|
|
243
|
+
const allSucceeded: ResultFactoryFunction = (
|
|
244
|
+
users: Array<User>,
|
|
245
|
+
): BulkAddUsersToProjectResult => {
|
|
246
|
+
return {
|
|
247
|
+
totalUsers: users,
|
|
248
|
+
inProgressUsers: [],
|
|
249
|
+
succeededUsers: users,
|
|
250
|
+
failedUsers: [],
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
type BulkActionButtonsFunction = () => Array<BulkActionButtonSchema<User>>;
|
|
255
|
+
|
|
256
|
+
const bulkActionButtons: BulkActionButtonsFunction = (): Array<
|
|
257
|
+
BulkActionButtonSchema<User>
|
|
258
|
+
> => {
|
|
259
|
+
return capturedTableProps?.bulkActions?.buttons || [];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
type BulkActionTitledFunction = (title: string) => BulkActionButtonSchema<User>;
|
|
263
|
+
|
|
264
|
+
const bulkActionTitled: BulkActionTitledFunction = (
|
|
265
|
+
title: string,
|
|
266
|
+
): BulkActionButtonSchema<User> => {
|
|
267
|
+
const button: BulkActionButtonSchema<User> | undefined =
|
|
268
|
+
bulkActionButtons().find((candidate: BulkActionButtonSchema<User>) => {
|
|
269
|
+
return candidate.title === title;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (!button) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`No "${title}" bulk action. Bulk actions: ${bulkActionButtons()
|
|
275
|
+
.map((candidate: BulkActionButtonSchema<User>) => {
|
|
276
|
+
return candidate.title;
|
|
277
|
+
})
|
|
278
|
+
.join(", ")}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return button;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/*
|
|
286
|
+
* Stand-ins for the four things the bulk bar hands an action's onClick. Kept as
|
|
287
|
+
* mocks rather than as a real BulkUpdateForm because what is being pinned is
|
|
288
|
+
* which of them the page calls, and when.
|
|
289
|
+
*/
|
|
290
|
+
type BulkActionCallbackMocks = {
|
|
291
|
+
items: Array<User>;
|
|
292
|
+
onProgressInfo: Mock<(progressInfo: ProgressInfo<User>) => void>;
|
|
293
|
+
onBulkActionStart: Mock<() => void>;
|
|
294
|
+
onBulkActionEnd: Mock<() => void>;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
type BulkActionCallbacksFactoryFunction = (
|
|
298
|
+
items: Array<User>,
|
|
299
|
+
) => BulkActionCallbackMocks;
|
|
300
|
+
|
|
301
|
+
const bulkActionCallbacks: BulkActionCallbacksFactoryFunction = (
|
|
302
|
+
items: Array<User>,
|
|
303
|
+
): BulkActionCallbackMocks => {
|
|
304
|
+
return {
|
|
305
|
+
items: items,
|
|
306
|
+
onProgressInfo: jest.fn<(progressInfo: ProgressInfo<User>) => void>(),
|
|
307
|
+
onBulkActionStart: jest.fn<() => void>((): void => {
|
|
308
|
+
recordedEvents.push("bulk-action-start");
|
|
309
|
+
}),
|
|
310
|
+
onBulkActionEnd: jest.fn<() => void>((): void => {
|
|
311
|
+
recordedEvents.push("bulk-action-end");
|
|
312
|
+
}),
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
type InActFunction = (work: () => Promise<void> | void) => Promise<void>;
|
|
317
|
+
|
|
318
|
+
/*
|
|
319
|
+
* `await act(...)` with a hand-attached continuation.
|
|
320
|
+
*
|
|
321
|
+
* act() returns a bare thenable and then, two microtasks later, warns if
|
|
322
|
+
* nothing has called its `then` yet. A plain `await` only attaches that handler
|
|
323
|
+
* on the next microtask, and zone.js - loaded here through the page's
|
|
324
|
+
* dependencies, and it patches Promise - lands the check first. The result is
|
|
325
|
+
* an "act without await" warning on every call that IS awaited, which is
|
|
326
|
+
* exactly the warning a reader of a sequencing test must be able to trust.
|
|
327
|
+
* Calling `then` directly enters the scope's continuation there and then.
|
|
328
|
+
*/
|
|
329
|
+
const inAct: InActFunction = (
|
|
330
|
+
work: () => Promise<void> | void,
|
|
331
|
+
): Promise<void> => {
|
|
332
|
+
const actScope: Promise<void> = act(async () => {
|
|
333
|
+
await work();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return new Promise<void>(
|
|
337
|
+
(
|
|
338
|
+
resolve: (value: void | PromiseLike<void>) => void,
|
|
339
|
+
reject: (reason: unknown) => void,
|
|
340
|
+
): void => {
|
|
341
|
+
actScope.then(resolve, reject);
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
type ClickAddToProjectFunction = (
|
|
347
|
+
callbacks: BulkActionCallbackMocks,
|
|
348
|
+
) => Promise<void>;
|
|
349
|
+
|
|
350
|
+
const clickAddToProject: ClickAddToProjectFunction = async (
|
|
351
|
+
callbacks: BulkActionCallbackMocks,
|
|
352
|
+
): Promise<void> => {
|
|
353
|
+
await inAct(() => {
|
|
354
|
+
return bulkActionTitled(ADD_TO_PROJECT_TITLE_KEY).onClick(callbacks);
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type SubmitPickerFunction = (
|
|
359
|
+
selection?: BulkAddUsersToProjectSelection,
|
|
360
|
+
) => Promise<void>;
|
|
361
|
+
|
|
362
|
+
const submitPicker: SubmitPickerFunction = async (
|
|
363
|
+
selection: BulkAddUsersToProjectSelection = SELECTION,
|
|
364
|
+
): Promise<void> => {
|
|
365
|
+
const picker: CapturedModalProps | null = capturedModalProps;
|
|
366
|
+
|
|
367
|
+
if (!picker) {
|
|
368
|
+
throw new Error("The picker was never rendered.");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await inAct(() => {
|
|
372
|
+
picker.onSubmit(selection);
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
type ClosePickerFunction = () => Promise<void>;
|
|
377
|
+
|
|
378
|
+
const closePicker: ClosePickerFunction = async (): Promise<void> => {
|
|
379
|
+
const picker: CapturedModalProps | null = capturedModalProps;
|
|
380
|
+
|
|
381
|
+
if (!picker) {
|
|
382
|
+
throw new Error("The picker was never rendered.");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await inAct(() => {
|
|
386
|
+
picker.onClose();
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
type CreateOptionsFunction = () => BulkAddUsersToProjectOptions;
|
|
391
|
+
|
|
392
|
+
const createOptions: CreateOptionsFunction =
|
|
393
|
+
(): BulkAddUsersToProjectOptions => {
|
|
394
|
+
const options: BulkAddUsersToProjectOptions | undefined =
|
|
395
|
+
bulkAddUsersToProjectMock.mock.calls[0]?.[0];
|
|
396
|
+
|
|
397
|
+
if (!options) {
|
|
398
|
+
throw new Error("bulkAddUsersToProject was never called.");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return options;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
describe("Admin > Users > Add to Project bulk action", () => {
|
|
405
|
+
beforeEach(async () => {
|
|
406
|
+
capturedTableProps = null;
|
|
407
|
+
capturedModalProps = null;
|
|
408
|
+
recordedEvents.length = 0;
|
|
409
|
+
|
|
410
|
+
userOne = aUser();
|
|
411
|
+
userTwo = aUser();
|
|
412
|
+
userThree = aUser();
|
|
413
|
+
userFour = aUser();
|
|
414
|
+
userFive = aUser();
|
|
415
|
+
|
|
416
|
+
bulkAddUsersToProjectMock.mockReset();
|
|
417
|
+
bulkAddUsersToProjectMock.mockImplementation(
|
|
418
|
+
(
|
|
419
|
+
options: BulkAddUsersToProjectOptions,
|
|
420
|
+
): Promise<BulkAddUsersToProjectResult> => {
|
|
421
|
+
return Promise.resolve(allSucceeded(options.users));
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
/*
|
|
426
|
+
* The page reads the current route to build the row's view link. In the
|
|
427
|
+
* app the router pushes this in; under test nothing does.
|
|
428
|
+
*/
|
|
429
|
+
Navigation.setLocation({
|
|
430
|
+
pathname: "/admin/users",
|
|
431
|
+
search: "",
|
|
432
|
+
hash: "",
|
|
433
|
+
state: null,
|
|
434
|
+
key: "test",
|
|
435
|
+
} as Location);
|
|
436
|
+
|
|
437
|
+
render(
|
|
438
|
+
<MemoryRouter>
|
|
439
|
+
<Users />
|
|
440
|
+
</MemoryRouter>,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(capturedTableProps).not.toBeNull();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
afterEach(() => {
|
|
449
|
+
cleanup();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe("how it is declared", () => {
|
|
453
|
+
/*
|
|
454
|
+
* The bulk menu puts the destructive actions last, behind a divider, so the
|
|
455
|
+
* first entry is the one an admin's eye lands on. Adding users to a project
|
|
456
|
+
* is the everyday reason to select rows here; blocking and deleting are
|
|
457
|
+
* not.
|
|
458
|
+
*/
|
|
459
|
+
test("is the first bulk action offered", () => {
|
|
460
|
+
expect(bulkActionButtons()[0]?.title).toBe(ADD_TO_PROJECT_TITLE_KEY);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* The action was inserted at the head of an existing array. A stray edit
|
|
465
|
+
* there would drop or reorder the actions the page already had, and the
|
|
466
|
+
* bulk menu renders whatever it is given without complaint.
|
|
467
|
+
*/
|
|
468
|
+
test("has not displaced Block, Unblock or Delete", () => {
|
|
469
|
+
const legacyTitles: Array<string> = ["Block", "Unblock", "Delete"];
|
|
470
|
+
|
|
471
|
+
expect(
|
|
472
|
+
bulkActionButtons()
|
|
473
|
+
.map((button: BulkActionButtonSchema<User>) => {
|
|
474
|
+
return button.title;
|
|
475
|
+
})
|
|
476
|
+
.filter((title: string) => {
|
|
477
|
+
return legacyTitles.includes(title);
|
|
478
|
+
}),
|
|
479
|
+
).toEqual(legacyTitles);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
/*
|
|
483
|
+
* BulkUpdateForm shows a ConfirmModal *instead of* running the click when
|
|
484
|
+
* `confirmMessage` is set, and only runs the click once it is submitted.
|
|
485
|
+
* For this action the click is what opens the picker, so a confirm message
|
|
486
|
+
* would put an "are you sure" dialog in front of a form the admin has not
|
|
487
|
+
* even filled in yet - it has nothing to confirm.
|
|
488
|
+
*/
|
|
489
|
+
test("declares no confirm dialog to stack in front of the picker", () => {
|
|
490
|
+
expect(
|
|
491
|
+
bulkActionTitled(ADD_TO_PROJECT_TITLE_KEY).confirmMessage,
|
|
492
|
+
).toBeUndefined();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
/*
|
|
496
|
+
* The title is a translation key, not an English string: this page is
|
|
497
|
+
* rendered in every locale the admin dashboard ships, and a hardcoded label
|
|
498
|
+
* would silently stay English. The icon is what tells the two "add"-ish
|
|
499
|
+
* entries apart in the menu.
|
|
500
|
+
*/
|
|
501
|
+
test("declares its icon and its translated title key", () => {
|
|
502
|
+
expect(bulkActionTitled(ADD_TO_PROJECT_TITLE_KEY).title).toBe(
|
|
503
|
+
"pages.users.bulkAddToProject",
|
|
504
|
+
);
|
|
505
|
+
expect(bulkActionTitled(ADD_TO_PROJECT_TITLE_KEY).icon).toBe(
|
|
506
|
+
IconProp.FolderPlus,
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe("clicking it", () => {
|
|
512
|
+
/*
|
|
513
|
+
* The click is the only thing that opens the picker. The page renders it
|
|
514
|
+
* off a piece of state that starts null, and a regression that rendered it
|
|
515
|
+
* unconditionally would put a project/team form in front of an admin who
|
|
516
|
+
* came only to read the table.
|
|
517
|
+
*
|
|
518
|
+
* Every other test in this file clicks first, so each of them would still
|
|
519
|
+
* find a picker on screen if it had been there all along - which makes
|
|
520
|
+
* "opens the picker" a claim none of them can actually support without
|
|
521
|
+
* this one.
|
|
522
|
+
*/
|
|
523
|
+
test("renders no picker before the action is clicked", () => {
|
|
524
|
+
expect(screen.queryByTestId(PICKER_TEST_ID)).not.toBeInTheDocument();
|
|
525
|
+
expect(capturedModalProps).toBeNull();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
/*
|
|
529
|
+
* `onBulkActionStart` is what puts the progress modal on screen and flips
|
|
530
|
+
* the run to in-progress. Calling it here - before the admin has chosen a
|
|
531
|
+
* project - would park an empty progress bar behind the picker, with a
|
|
532
|
+
* Close button disabled for a run that has not started.
|
|
533
|
+
*/
|
|
534
|
+
test("starts nothing", async () => {
|
|
535
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
536
|
+
userOne,
|
|
537
|
+
userTwo,
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
await clickAddToProject(callbacks);
|
|
541
|
+
|
|
542
|
+
expect(callbacks.onBulkActionStart).not.toHaveBeenCalled();
|
|
543
|
+
expect(bulkAddUsersToProjectMock).not.toHaveBeenCalled();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/*
|
|
547
|
+
* The picker has to be handed the rows the click carried, not the page's
|
|
548
|
+
* own idea of the selection - the page never sees the table's selection
|
|
549
|
+
* state, it only ever gets it through the click. Handing it anything else
|
|
550
|
+
* would create memberships for users the admin did not tick.
|
|
551
|
+
*/
|
|
552
|
+
test("opens the picker over exactly the selected users", async () => {
|
|
553
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
554
|
+
userOne,
|
|
555
|
+
userTwo,
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
/*
|
|
559
|
+
* Pinned here too, and not only in the test above, so that this test's
|
|
560
|
+
* own "opens" is about the click rather than about a picker that was
|
|
561
|
+
* already there.
|
|
562
|
+
*/
|
|
563
|
+
expect(screen.queryByTestId(PICKER_TEST_ID)).not.toBeInTheDocument();
|
|
564
|
+
|
|
565
|
+
await clickAddToProject(callbacks);
|
|
566
|
+
|
|
567
|
+
expect(screen.getByTestId(PICKER_TEST_ID)).toBeInTheDocument();
|
|
568
|
+
expect(capturedModalProps?.users).toBe(callbacks.items);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
/*
|
|
572
|
+
* The bulk bar hands a fresh set of props to every click, and the page has
|
|
573
|
+
* to be looking at the latest one - both when it renders the picker and
|
|
574
|
+
* when it finally runs the creates.
|
|
575
|
+
*
|
|
576
|
+
* An admin who opens the picker, backs out, changes their ticks and opens
|
|
577
|
+
* it again is the ordinary way to get two clicks with different rows. If
|
|
578
|
+
* the page held onto the first click - a stale closure over it, or a picker
|
|
579
|
+
* React reuses across the two opens without re-reading the state - the
|
|
580
|
+
* memberships would be created for rows that are no longer selected, and
|
|
581
|
+
* the progress modal would report against the first click's callbacks while
|
|
582
|
+
* the second click's are left waiting forever.
|
|
583
|
+
*/
|
|
584
|
+
test("reopening after a cancel uses the second click's users, not the first's", async () => {
|
|
585
|
+
const firstClick: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
586
|
+
userOne,
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
await clickAddToProject(firstClick);
|
|
590
|
+
|
|
591
|
+
expect(capturedModalProps?.users).toBe(firstClick.items);
|
|
592
|
+
|
|
593
|
+
await closePicker();
|
|
594
|
+
|
|
595
|
+
expect(screen.queryByTestId(PICKER_TEST_ID)).not.toBeInTheDocument();
|
|
596
|
+
|
|
597
|
+
const secondClick: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
598
|
+
userTwo,
|
|
599
|
+
userThree,
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
await clickAddToProject(secondClick);
|
|
603
|
+
|
|
604
|
+
expect(screen.getByTestId(PICKER_TEST_ID)).toBeInTheDocument();
|
|
605
|
+
expect(capturedModalProps?.users).toBe(secondClick.items);
|
|
606
|
+
|
|
607
|
+
await submitPicker();
|
|
608
|
+
|
|
609
|
+
expect(createOptions().users).toBe(secondClick.items);
|
|
610
|
+
expect(secondClick.onBulkActionStart).toHaveBeenCalledTimes(1);
|
|
611
|
+
expect(secondClick.onBulkActionEnd).toHaveBeenCalledTimes(1);
|
|
612
|
+
|
|
613
|
+
/*
|
|
614
|
+
* The abandoned click's run must stay unstarted and unended: ending it
|
|
615
|
+
* would clear a selection the admin never submitted.
|
|
616
|
+
*/
|
|
617
|
+
expect(firstClick.onBulkActionStart).not.toHaveBeenCalled();
|
|
618
|
+
expect(firstClick.onBulkActionEnd).not.toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe("closing the picker without submitting", () => {
|
|
623
|
+
/*
|
|
624
|
+
* Backing out is not a run. Starting one would leave a progress modal the
|
|
625
|
+
* admin never asked for, and - worse - ending one that never started makes
|
|
626
|
+
* the table clear the selection, throwing away the rows they picked. The
|
|
627
|
+
* selection has to survive so they can reopen the picker and try again.
|
|
628
|
+
*/
|
|
629
|
+
test("leaves the run unstarted and unended", async () => {
|
|
630
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
631
|
+
userOne,
|
|
632
|
+
userTwo,
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
await clickAddToProject(callbacks);
|
|
636
|
+
|
|
637
|
+
const picker: CapturedModalProps | null = capturedModalProps;
|
|
638
|
+
|
|
639
|
+
await inAct(() => {
|
|
640
|
+
picker?.onClose();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(screen.queryByTestId(PICKER_TEST_ID)).not.toBeInTheDocument();
|
|
644
|
+
expect(bulkAddUsersToProjectMock).not.toHaveBeenCalled();
|
|
645
|
+
expect(callbacks.onBulkActionStart).not.toHaveBeenCalled();
|
|
646
|
+
expect(callbacks.onBulkActionEnd).not.toHaveBeenCalled();
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe("submitting the picker", () => {
|
|
651
|
+
/*
|
|
652
|
+
* The order is the feature, but read the asserted list carefully: it is
|
|
653
|
+
* NOT evidence that the picker leaves the screen before the creates begin,
|
|
654
|
+
* and no test could be.
|
|
655
|
+
*
|
|
656
|
+
* Dismissing the picker and starting the run are two state updates made
|
|
657
|
+
* from one handler, so React commits them together. "picker-closed" is the
|
|
658
|
+
* stub's unmount effect, which runs when that single commit flushes - after
|
|
659
|
+
* the handler has already called `onBulkActionStart` and reached the
|
|
660
|
+
* creates synchronously. Hence start, creates, picker-closed. Both updates
|
|
661
|
+
* landing in one commit is the actual guarantee: the admin never sees a
|
|
662
|
+
* frame with the picker up and the run already going.
|
|
663
|
+
*
|
|
664
|
+
* What this pins is the part that does matter. `onBulkActionStart` runs
|
|
665
|
+
* before the creates, and `onBulkActionEnd` only once they settle - the
|
|
666
|
+
* mid-run assertions below hold the creates open to prove `end` has not
|
|
667
|
+
* fired yet. Ending early re-enables the progress modal's Close button, and
|
|
668
|
+
* closing it is what clears the selection and refetches the table, so an
|
|
669
|
+
* early end throws away the selection while memberships are still being
|
|
670
|
+
* created. And by the time the run is observably in flight the picker is
|
|
671
|
+
* already off the screen, so the progress modal is not stacked behind it.
|
|
672
|
+
*/
|
|
673
|
+
test("closes the picker, then runs start -> creates -> end in order", async () => {
|
|
674
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
675
|
+
userOne,
|
|
676
|
+
userTwo,
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
await clickAddToProject(callbacks);
|
|
680
|
+
|
|
681
|
+
/*
|
|
682
|
+
* Held open so the assertions can look at the page mid-run, which is the
|
|
683
|
+
* only moment at which "the picker is gone but the run is not finished"
|
|
684
|
+
* is a distinguishable state.
|
|
685
|
+
*/
|
|
686
|
+
const run: { finish: () => void } = {
|
|
687
|
+
finish: (): void => {
|
|
688
|
+
throw new Error("The creates were never started.");
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
bulkAddUsersToProjectMock.mockImplementation(
|
|
693
|
+
(
|
|
694
|
+
options: BulkAddUsersToProjectOptions,
|
|
695
|
+
): Promise<BulkAddUsersToProjectResult> => {
|
|
696
|
+
return new Promise<BulkAddUsersToProjectResult>(
|
|
697
|
+
(resolve: (result: BulkAddUsersToProjectResult) => void) => {
|
|
698
|
+
run.finish = (): void => {
|
|
699
|
+
resolve(allSucceeded(options.users));
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
);
|
|
703
|
+
},
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
await submitPicker();
|
|
707
|
+
|
|
708
|
+
expect(screen.queryByTestId(PICKER_TEST_ID)).not.toBeInTheDocument();
|
|
709
|
+
expect(callbacks.onBulkActionEnd).not.toHaveBeenCalled();
|
|
710
|
+
|
|
711
|
+
await inAct(() => {
|
|
712
|
+
run.finish();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
expect(callbacks.onBulkActionEnd).toHaveBeenCalledTimes(1);
|
|
716
|
+
expect(recordedEvents).toEqual([
|
|
717
|
+
"bulk-action-start",
|
|
718
|
+
"creates-started",
|
|
719
|
+
"picker-closed",
|
|
720
|
+
"bulk-action-end",
|
|
721
|
+
]);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
/*
|
|
725
|
+
* The selection the picker collected has to reach the creates intact, and
|
|
726
|
+
* against the users the click carried. `hasAcceptedInvitation` in
|
|
727
|
+
* particular is the difference between adding members outright and sending
|
|
728
|
+
* invitations nobody asked for - only a master admin may do the former, so
|
|
729
|
+
* dropping the flag silently downgrades the whole batch to pending.
|
|
730
|
+
*/
|
|
731
|
+
test("hands the creates the clicked users and the picked project, team and auto-accept", async () => {
|
|
732
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
733
|
+
userOne,
|
|
734
|
+
userTwo,
|
|
735
|
+
]);
|
|
736
|
+
|
|
737
|
+
await clickAddToProject(callbacks);
|
|
738
|
+
await submitPicker();
|
|
739
|
+
|
|
740
|
+
expect(createOptions().users).toBe(callbacks.items);
|
|
741
|
+
expect(createOptions().projectId).toBe(SELECTION.projectId);
|
|
742
|
+
expect(createOptions().teamId).toBe(SELECTION.teamId);
|
|
743
|
+
expect(createOptions().hasAcceptedInvitation).toBe(true);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
/*
|
|
747
|
+
* The other half of that flag, and the reason it needs its own test: the
|
|
748
|
+
* checkbox defaults to unticked, so `false` is what an ordinary submit
|
|
749
|
+
* carries. A page that ignored `selection.hasAcceptedInvitation` and always
|
|
750
|
+
* sent `true` would satisfy the test above and every other test in this
|
|
751
|
+
* file, because they all submit the same auto-accept selection.
|
|
752
|
+
*
|
|
753
|
+
* Sending `true` there is a consent bug, not a cosmetic one. `true` makes
|
|
754
|
+
* each user a member of the project outright - the invitation they were
|
|
755
|
+
* meant to accept for themselves is never offered, and they are on a
|
|
756
|
+
* project they never agreed to join. Unticked has to reach the creates as
|
|
757
|
+
* unticked.
|
|
758
|
+
*/
|
|
759
|
+
test("forwards an unticked auto-accept as false rather than assuming true", async () => {
|
|
760
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
761
|
+
userOne,
|
|
762
|
+
userTwo,
|
|
763
|
+
]);
|
|
764
|
+
|
|
765
|
+
await clickAddToProject(callbacks);
|
|
766
|
+
await submitPicker(SELECTION_WITHOUT_AUTO_ACCEPT);
|
|
767
|
+
|
|
768
|
+
expect(createOptions().hasAcceptedInvitation).toBe(false);
|
|
769
|
+
|
|
770
|
+
/*
|
|
771
|
+
* The rest of the pick travels from the same selection object, so pin it
|
|
772
|
+
* here too: reading the flag off one selection and the ids off another
|
|
773
|
+
* would be a stranger bug, but a silent one.
|
|
774
|
+
*/
|
|
775
|
+
expect(createOptions().projectId).toBe(
|
|
776
|
+
SELECTION_WITHOUT_AUTO_ACCEPT.projectId,
|
|
777
|
+
);
|
|
778
|
+
expect(createOptions().teamId).toBe(SELECTION_WITHOUT_AUTO_ACCEPT.teamId);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
/*
|
|
782
|
+
* The two sides speak different dialects: the creates count users, the
|
|
783
|
+
* shared progress modal counts items. The page is the only translator, and
|
|
784
|
+
* every one of the four names differs. Get one wrong and the field arrives
|
|
785
|
+
* `undefined` - the progress bar reads 0 of 0, or the per-user failure list
|
|
786
|
+
* renders empty, which is exactly the list an admin came here to read.
|
|
787
|
+
*/
|
|
788
|
+
test("renames the running tally into the shared progress modal's shape", async () => {
|
|
789
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
790
|
+
userOne,
|
|
791
|
+
userTwo,
|
|
792
|
+
]);
|
|
793
|
+
|
|
794
|
+
await clickAddToProject(callbacks);
|
|
795
|
+
await submitPicker();
|
|
796
|
+
|
|
797
|
+
createOptions().onProgress?.({
|
|
798
|
+
totalUsers: [userOne, userTwo],
|
|
799
|
+
inProgressUsers: [userTwo],
|
|
800
|
+
succeededUsers: [userOne],
|
|
801
|
+
failedUsers: [
|
|
802
|
+
{
|
|
803
|
+
user: userTwo,
|
|
804
|
+
failedMessage: ALREADY_INVITED_MESSAGE,
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(callbacks.onProgressInfo).toHaveBeenCalledTimes(1);
|
|
810
|
+
expect(callbacks.onProgressInfo).toHaveBeenCalledWith({
|
|
811
|
+
totalItems: [userOne, userTwo],
|
|
812
|
+
inProgressItems: [userTwo],
|
|
813
|
+
successItems: [userOne],
|
|
814
|
+
failed: [
|
|
815
|
+
{
|
|
816
|
+
item: userTwo,
|
|
817
|
+
failedMessage: ALREADY_INVITED_MESSAGE,
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
/*
|
|
824
|
+
* A user who is already on the team comes back as a per-user failure, so
|
|
825
|
+
* reaching the catch means the run itself broke - the request never left,
|
|
826
|
+
* the session expired, something unforeseen. The progress modal's Close
|
|
827
|
+
* button is disabled for as long as the action is in progress, and closing
|
|
828
|
+
* it is the only thing that clears the selection and refetches the table.
|
|
829
|
+
* So a missed `onBulkActionEnd` here is not a lost error message, it is a
|
|
830
|
+
* modal the admin can never dismiss.
|
|
831
|
+
*
|
|
832
|
+
* This is the reject that happens before a single user was attempted, so
|
|
833
|
+
* "nobody was added" is the truth and the whole selection is reported
|
|
834
|
+
* failed. The test below covers the reject that lands part-way through,
|
|
835
|
+
* where reporting the whole selection would be a lie.
|
|
836
|
+
*/
|
|
837
|
+
test("still ends the run, and reports every user failed, when the creates reject before any user is attempted", async () => {
|
|
838
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
839
|
+
userOne,
|
|
840
|
+
userTwo,
|
|
841
|
+
]);
|
|
842
|
+
|
|
843
|
+
await clickAddToProject(callbacks);
|
|
844
|
+
|
|
845
|
+
bulkAddUsersToProjectMock.mockImplementation(
|
|
846
|
+
(): Promise<BulkAddUsersToProjectResult> => {
|
|
847
|
+
return Promise.reject(new Error(RUN_BROKE_MESSAGE));
|
|
848
|
+
},
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
await submitPicker();
|
|
852
|
+
|
|
853
|
+
expect(callbacks.onBulkActionEnd).toHaveBeenCalledTimes(1);
|
|
854
|
+
expect(callbacks.onProgressInfo).toHaveBeenCalledWith({
|
|
855
|
+
totalItems: callbacks.items,
|
|
856
|
+
inProgressItems: [],
|
|
857
|
+
successItems: [],
|
|
858
|
+
failed: [
|
|
859
|
+
{
|
|
860
|
+
item: userOne,
|
|
861
|
+
failedMessage: RUN_BROKE_MESSAGE,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
item: userTwo,
|
|
865
|
+
failedMessage: RUN_BROKE_MESSAGE,
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
* The creates run one user at a time and report after each, so a run that
|
|
873
|
+
* breaks half way has already created real memberships. Those users are on
|
|
874
|
+
* the project whatever happens next.
|
|
875
|
+
*
|
|
876
|
+
* The final report is the only record the admin gets - the progress modal
|
|
877
|
+
* is what they read, and closing it clears the selection and refetches. So
|
|
878
|
+
* the recovery has to keep what the last report said: the users already
|
|
879
|
+
* added stay added, the per-user failure already recorded (here: already on
|
|
880
|
+
* the team) keeps its own message rather than being overwritten by the
|
|
881
|
+
* run's, and only the users never attempted are added as failures. Blanket
|
|
882
|
+
* "everything failed" would send the admin to re-add users who are already
|
|
883
|
+
* members, and blanket "everything succeeded" would leave the not-attempted
|
|
884
|
+
* ones off the project with nothing on screen saying so.
|
|
885
|
+
*
|
|
886
|
+
* `onBulkActionEnd` still has to run on this path too, for the same reason
|
|
887
|
+
* as above - otherwise the Close button never unlocks.
|
|
888
|
+
*/
|
|
889
|
+
test("keeps the already-added users and fails only the not-yet-attempted ones when the creates reject mid-run", async () => {
|
|
890
|
+
const callbacks: BulkActionCallbackMocks = bulkActionCallbacks([
|
|
891
|
+
userOne,
|
|
892
|
+
userTwo,
|
|
893
|
+
userThree,
|
|
894
|
+
userFour,
|
|
895
|
+
userFive,
|
|
896
|
+
]);
|
|
897
|
+
|
|
898
|
+
await clickAddToProject(callbacks);
|
|
899
|
+
|
|
900
|
+
bulkAddUsersToProjectMock.mockImplementation(
|
|
901
|
+
(
|
|
902
|
+
options: BulkAddUsersToProjectOptions,
|
|
903
|
+
): Promise<BulkAddUsersToProjectResult> => {
|
|
904
|
+
/*
|
|
905
|
+
* Three users dealt with - two added, one already on the team - and
|
|
906
|
+
* then the run breaks with userFour and userFive never attempted.
|
|
907
|
+
*/
|
|
908
|
+
options.onProgress?.({
|
|
909
|
+
totalUsers: [userOne, userTwo, userThree, userFour, userFive],
|
|
910
|
+
inProgressUsers: [userFour, userFive],
|
|
911
|
+
succeededUsers: [userOne, userTwo],
|
|
912
|
+
failedUsers: [
|
|
913
|
+
{
|
|
914
|
+
user: userThree,
|
|
915
|
+
failedMessage: ALREADY_INVITED_MESSAGE,
|
|
916
|
+
},
|
|
917
|
+
],
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
return Promise.reject(new Error(RUN_BROKE_MESSAGE));
|
|
921
|
+
},
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
await submitPicker();
|
|
925
|
+
|
|
926
|
+
expect(callbacks.onBulkActionEnd).toHaveBeenCalledTimes(1);
|
|
927
|
+
|
|
928
|
+
/*
|
|
929
|
+
* Once for the mid-run tally, once for the recovery. The recovery report
|
|
930
|
+
* is what the admin is left looking at, so it is the last one that has to
|
|
931
|
+
* be right.
|
|
932
|
+
*/
|
|
933
|
+
expect(callbacks.onProgressInfo).toHaveBeenCalledTimes(2);
|
|
934
|
+
expect(callbacks.onProgressInfo).toHaveBeenLastCalledWith({
|
|
935
|
+
totalItems: [userOne, userTwo, userThree, userFour, userFive],
|
|
936
|
+
inProgressItems: [],
|
|
937
|
+
successItems: [userOne, userTwo],
|
|
938
|
+
failed: [
|
|
939
|
+
{
|
|
940
|
+
item: userThree,
|
|
941
|
+
failedMessage: ALREADY_INVITED_MESSAGE,
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
item: userFour,
|
|
945
|
+
failedMessage: RUN_BROKE_MESSAGE,
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
item: userFive,
|
|
949
|
+
failedMessage: RUN_BROKE_MESSAGE,
|
|
950
|
+
},
|
|
951
|
+
],
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
});
|