@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,595 @@
|
|
|
1
|
+
import DictionaryForm, {
|
|
2
|
+
ComponentProps,
|
|
3
|
+
ValueType,
|
|
4
|
+
} from "../../../UI/Components/Dictionary/Dictionary";
|
|
5
|
+
import { DictionaryEntryValue } from "../../../UI/Components/Dictionary/DictionaryFilterOperator";
|
|
6
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
7
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
8
|
+
/*
|
|
9
|
+
* The main entry, not "/extend-expect": the latter no longer ships type
|
|
10
|
+
* declarations, so every jest-dom matcher in this file fails to typecheck and
|
|
11
|
+
* the whole suite is skipped before a single assertion runs.
|
|
12
|
+
*/
|
|
13
|
+
import "@testing-library/jest-dom";
|
|
14
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* What a DictionaryForm row offers the user in its value box comes from exactly
|
|
20
|
+
* two props: `valuePlaceholder` (one hint for the whole form) and
|
|
21
|
+
* `valueSuggestions` (a list per key). There used to be two more —
|
|
22
|
+
* `valuePlaceholders`, a per-key placeholder overriding the form-wide one, and
|
|
23
|
+
* `defaultValueSuggestions`, a list offered on any row whose key had no list of
|
|
24
|
+
* its own. Both existed only for the workflow record editor, which now renders
|
|
25
|
+
* its own schema-driven editor and never mounts this component, so both were
|
|
26
|
+
* removed along with the merging logic behind them.
|
|
27
|
+
*
|
|
28
|
+
* These tests pin what survives that removal, and they are written so that
|
|
29
|
+
* re-introducing either fallback fails them:
|
|
30
|
+
*
|
|
31
|
+
* - the placeholder is form-wide, so two rows with different keys show the
|
|
32
|
+
* same hint, and a row shows it whether or not its key is known;
|
|
33
|
+
* - suggestions are strictly per key, so a key with no list — or an empty
|
|
34
|
+
* one, or no key typed yet — offers nothing at all rather than falling back
|
|
35
|
+
* to some other list.
|
|
36
|
+
*
|
|
37
|
+
* Everything else here is the ordinary behaviour of the value box those two
|
|
38
|
+
* props drive: filtering, picking, switching key, the multi-select an
|
|
39
|
+
* "is any of" operator turns the box into, the loading row, and the
|
|
40
|
+
* `onValueSearch` callback that lets a parent refine the list server-side.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const VALUE_PLACEHOLDER: string = "Attribute value";
|
|
44
|
+
const KEY_PLACEHOLDER: string = "Attribute key";
|
|
45
|
+
const ADD_BUTTON_TITLE: string = "Add Attribute";
|
|
46
|
+
|
|
47
|
+
const VALUE_SUGGESTIONS: Record<string, Array<string>> = {
|
|
48
|
+
env: ["production", "staging", "development"],
|
|
49
|
+
region: ["us-east-1", "eu-west-2"],
|
|
50
|
+
// A key that is known but has no values to offer yet.
|
|
51
|
+
team: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
interface RenderResult {
|
|
55
|
+
emitted: Array<Dictionary<DictionaryEntryValue>>;
|
|
56
|
+
searches: Array<{ key: string; searchText: string }>;
|
|
57
|
+
lastEmitted: () => Dictionary<DictionaryEntryValue> | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type RenderDictionaryFunction = (
|
|
61
|
+
overrides?: Partial<ComponentProps>,
|
|
62
|
+
) => RenderResult;
|
|
63
|
+
|
|
64
|
+
const renderDictionary: RenderDictionaryFunction = (
|
|
65
|
+
overrides: Partial<ComponentProps> = {},
|
|
66
|
+
): RenderResult => {
|
|
67
|
+
const emitted: Array<Dictionary<DictionaryEntryValue>> = [];
|
|
68
|
+
const searches: Array<{ key: string; searchText: string }> = [];
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
<DictionaryForm
|
|
72
|
+
addButtonSuffix="Attribute"
|
|
73
|
+
keyPlaceholder={KEY_PLACEHOLDER}
|
|
74
|
+
valuePlaceholder={VALUE_PLACEHOLDER}
|
|
75
|
+
valueSuggestions={VALUE_SUGGESTIONS}
|
|
76
|
+
onChange={(value: Dictionary<DictionaryEntryValue>) => {
|
|
77
|
+
emitted.push(value);
|
|
78
|
+
}}
|
|
79
|
+
onValueSearch={(key: string, searchText: string) => {
|
|
80
|
+
searches.push({ key: key, searchText: searchText });
|
|
81
|
+
}}
|
|
82
|
+
{...overrides}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
emitted: emitted,
|
|
88
|
+
searches: searches,
|
|
89
|
+
lastEmitted: () => {
|
|
90
|
+
return emitted.length > 0 ? emitted[emitted.length - 1] : undefined;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Rows are the only `items-start` flex containers the form renders, so they are
|
|
97
|
+
* the one stable handle on "this row's inputs" — indexing a flat list of inputs
|
|
98
|
+
* breaks as soon as a row changes shape (a Number row has one autocomplete, a
|
|
99
|
+
* Text row two).
|
|
100
|
+
*/
|
|
101
|
+
type RowsFunction = () => Array<HTMLElement>;
|
|
102
|
+
|
|
103
|
+
const rows: RowsFunction = (): Array<HTMLElement> => {
|
|
104
|
+
return Array.from(
|
|
105
|
+
document.querySelectorAll<HTMLElement>("div.flex.items-start"),
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* AutocompleteTextInput and react-select both report role="combobox", so the
|
|
111
|
+
* only stable way to single out the hand-rolled ones is the listbox they own.
|
|
112
|
+
*/
|
|
113
|
+
type AutocompletesInRowFunction = (rowIndex: number) => Array<HTMLInputElement>;
|
|
114
|
+
|
|
115
|
+
const autocompletesInRow: AutocompletesInRowFunction = (
|
|
116
|
+
rowIndex: number,
|
|
117
|
+
): Array<HTMLInputElement> => {
|
|
118
|
+
return Array.from(
|
|
119
|
+
rows()[rowIndex]!.querySelectorAll<HTMLInputElement>(
|
|
120
|
+
'input[aria-controls^="autocomplete-suggestions-"]',
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type RowInputFunction = (rowIndex?: number) => HTMLInputElement;
|
|
126
|
+
|
|
127
|
+
const keyInput: RowInputFunction = (rowIndex: number = 0): HTMLInputElement => {
|
|
128
|
+
return autocompletesInRow(rowIndex)[0]!;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const valueInput: RowInputFunction = (
|
|
132
|
+
rowIndex: number = 0,
|
|
133
|
+
): HTMLInputElement => {
|
|
134
|
+
return autocompletesInRow(rowIndex)[1]!;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type NumberInputFunction = () => HTMLInputElement;
|
|
138
|
+
|
|
139
|
+
const numberInput: NumberInputFunction = (): HTMLInputElement => {
|
|
140
|
+
return document.querySelector<HTMLInputElement>('input[type="number"]')!;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* Column order inside a row: key autocomplete, then the operator select when
|
|
145
|
+
* operators are enabled, then the value control. The value control is always
|
|
146
|
+
* the last combobox in the row, whichever shape it currently has.
|
|
147
|
+
*/
|
|
148
|
+
type RowComboboxesFunction = (rowIndex: number) => Array<HTMLElement>;
|
|
149
|
+
|
|
150
|
+
const rowComboboxes: RowComboboxesFunction = (
|
|
151
|
+
rowIndex: number,
|
|
152
|
+
): Array<HTMLElement> => {
|
|
153
|
+
return Array.from(
|
|
154
|
+
rows()[rowIndex]!.querySelectorAll<HTMLElement>('[role="combobox"]'),
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
type RowComboboxFunction = (rowIndex?: number) => HTMLElement;
|
|
159
|
+
|
|
160
|
+
const operatorSelect: RowComboboxFunction = (
|
|
161
|
+
rowIndex: number = 0,
|
|
162
|
+
): HTMLElement => {
|
|
163
|
+
return rowComboboxes(rowIndex)[1]!;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const valueSelect: RowComboboxFunction = (
|
|
167
|
+
rowIndex: number = 0,
|
|
168
|
+
): HTMLElement => {
|
|
169
|
+
const comboboxes: Array<HTMLElement> = rowComboboxes(rowIndex);
|
|
170
|
+
return comboboxes[comboboxes.length - 1]!;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* react-select opens on ArrowDown and its options are portalled to
|
|
175
|
+
* document.body, so they are reachable by role from `screen` but never from
|
|
176
|
+
* inside the form's own subtree.
|
|
177
|
+
*/
|
|
178
|
+
type SelectOptionFunction = (combobox: HTMLElement, optionText: string) => void;
|
|
179
|
+
|
|
180
|
+
const selectOption: SelectOptionFunction = (
|
|
181
|
+
combobox: HTMLElement,
|
|
182
|
+
optionText: string,
|
|
183
|
+
): void => {
|
|
184
|
+
fireEvent.keyDown(combobox, { key: "ArrowDown" });
|
|
185
|
+
const option: HTMLElement = screen.getByText(optionText);
|
|
186
|
+
fireEvent.mouseDown(option);
|
|
187
|
+
fireEvent.click(option);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Both the autocomplete menu and react-select's menu report role="option", and
|
|
192
|
+
* only one of them is ever open at a time in these tests.
|
|
193
|
+
*/
|
|
194
|
+
type OpenOptionsFunction = () => Array<string>;
|
|
195
|
+
|
|
196
|
+
const openOptions: OpenOptionsFunction = (): Array<string> => {
|
|
197
|
+
return screen.queryAllByRole("option").map((option: HTMLElement) => {
|
|
198
|
+
return option.textContent || "";
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
type AddRowFunction = () => void;
|
|
203
|
+
|
|
204
|
+
const addRow: AddRowFunction = (): void => {
|
|
205
|
+
fireEvent.click(screen.getByText(ADD_BUTTON_TITLE));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
type TypeKeyFunction = (key: string, rowIndex?: number) => void;
|
|
209
|
+
|
|
210
|
+
const typeKey: TypeKeyFunction = (key: string, rowIndex: number = 0): void => {
|
|
211
|
+
fireEvent.change(keyInput(rowIndex), { target: { value: key } });
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type AddRowWithKeyFunction = (key: string) => void;
|
|
215
|
+
|
|
216
|
+
const addRowWithKey: AddRowWithKeyFunction = (key: string): void => {
|
|
217
|
+
addRow();
|
|
218
|
+
typeKey(key, rows().length - 1);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
describe("Dictionary — the value box", () => {
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
cleanup();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("the value placeholder", () => {
|
|
227
|
+
test("shows the form-wide placeholder on a row with a known key", () => {
|
|
228
|
+
renderDictionary();
|
|
229
|
+
addRowWithKey("env");
|
|
230
|
+
|
|
231
|
+
expect(valueInput().getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("shows it on a row whose key is still blank", () => {
|
|
235
|
+
renderDictionary();
|
|
236
|
+
addRow();
|
|
237
|
+
|
|
238
|
+
expect(valueInput().getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("shows it on a row whose key has no suggestions of its own", () => {
|
|
242
|
+
renderDictionary();
|
|
243
|
+
addRowWithKey("some.unknown.attribute");
|
|
244
|
+
|
|
245
|
+
expect(valueInput().getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* The pin for the removed per-key `valuePlaceholders`: the hint is a
|
|
250
|
+
* property of the form, not of the column, so two rows sitting on different
|
|
251
|
+
* keys read exactly the same.
|
|
252
|
+
*/
|
|
253
|
+
test("is the same on every row, whatever each row's key is", () => {
|
|
254
|
+
renderDictionary();
|
|
255
|
+
addRowWithKey("env");
|
|
256
|
+
addRowWithKey("region");
|
|
257
|
+
|
|
258
|
+
expect(valueInput(0).getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
259
|
+
expect(valueInput(1).getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("does not leak into the key input, which keeps its own", () => {
|
|
263
|
+
renderDictionary();
|
|
264
|
+
addRowWithKey("env");
|
|
265
|
+
|
|
266
|
+
expect(keyInput().getAttribute("placeholder")).toBe(KEY_PLACEHOLDER);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a Number row uses it too", () => {
|
|
270
|
+
renderDictionary({
|
|
271
|
+
valueTypes: [ValueType.Text, ValueType.Number],
|
|
272
|
+
initialValue: { latency: 200 },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(numberInput().getAttribute("placeholder")).toBe(VALUE_PLACEHOLDER);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("an 'is any of' row uses it as the multi-select's placeholder", () => {
|
|
279
|
+
renderDictionary({ enableOperators: true });
|
|
280
|
+
addRowWithKey("env");
|
|
281
|
+
|
|
282
|
+
selectOption(operatorSelect(), "is any of");
|
|
283
|
+
|
|
284
|
+
expect(screen.getByText(VALUE_PLACEHOLDER)).toBeInTheDocument();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("a numeric operator overrides it with 'Number'", () => {
|
|
288
|
+
renderDictionary({ enableOperators: true });
|
|
289
|
+
addRowWithKey("env");
|
|
290
|
+
|
|
291
|
+
selectOption(operatorSelect(), ">");
|
|
292
|
+
|
|
293
|
+
expect(valueInput().getAttribute("placeholder")).toBe("Number");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a value-less operator labels the inert box with the operator", () => {
|
|
297
|
+
renderDictionary({ enableOperators: true });
|
|
298
|
+
addRowWithKey("env");
|
|
299
|
+
|
|
300
|
+
selectOption(operatorSelect(), "is empty");
|
|
301
|
+
|
|
302
|
+
const inertInput: HTMLInputElement = screen.getByPlaceholderText(
|
|
303
|
+
"is empty",
|
|
304
|
+
) as HTMLInputElement;
|
|
305
|
+
expect(inertInput.readOnly).toBe(true);
|
|
306
|
+
expect(screen.queryByPlaceholderText(VALUE_PLACEHOLDER)).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("with no placeholder prop the value input simply has none", () => {
|
|
310
|
+
/*
|
|
311
|
+
* Rendered directly rather than through renderDictionary: the prop is
|
|
312
|
+
* declared `valuePlaceholder?: string`, and under
|
|
313
|
+
* exactOptionalPropertyTypes passing an explicit `undefined` is not the
|
|
314
|
+
* same as leaving it off.
|
|
315
|
+
*/
|
|
316
|
+
render(
|
|
317
|
+
<DictionaryForm
|
|
318
|
+
addButtonSuffix="Attribute"
|
|
319
|
+
keyPlaceholder={KEY_PLACEHOLDER}
|
|
320
|
+
valueSuggestions={VALUE_SUGGESTIONS}
|
|
321
|
+
/>,
|
|
322
|
+
);
|
|
323
|
+
addRowWithKey("env");
|
|
324
|
+
|
|
325
|
+
expect(valueInput().getAttribute("placeholder")).toBeNull();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("per-key value suggestions", () => {
|
|
330
|
+
test("focusing the value box offers that key's values", () => {
|
|
331
|
+
renderDictionary();
|
|
332
|
+
addRowWithKey("env");
|
|
333
|
+
|
|
334
|
+
fireEvent.focus(valueInput());
|
|
335
|
+
|
|
336
|
+
expect(openOptions()).toEqual(["production", "staging", "development"]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a different key offers a different set", () => {
|
|
340
|
+
renderDictionary();
|
|
341
|
+
addRowWithKey("region");
|
|
342
|
+
|
|
343
|
+
fireEvent.focus(valueInput());
|
|
344
|
+
|
|
345
|
+
expect(openOptions()).toEqual(["us-east-1", "eu-west-2"]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
* The pin for the removed `defaultValueSuggestions`: a key nobody has
|
|
350
|
+
* values for offers nothing, rather than falling back to a form-wide list.
|
|
351
|
+
*/
|
|
352
|
+
test("a key with no list of its own offers nothing", () => {
|
|
353
|
+
renderDictionary();
|
|
354
|
+
addRowWithKey("some.unknown.attribute");
|
|
355
|
+
|
|
356
|
+
fireEvent.focus(valueInput());
|
|
357
|
+
|
|
358
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
359
|
+
expect(openOptions()).toEqual([]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a key whose list is empty offers nothing", () => {
|
|
363
|
+
renderDictionary();
|
|
364
|
+
addRowWithKey("team");
|
|
365
|
+
|
|
366
|
+
fireEvent.focus(valueInput());
|
|
367
|
+
|
|
368
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
369
|
+
expect(openOptions()).toEqual([]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a row with no key yet offers nothing", () => {
|
|
373
|
+
renderDictionary();
|
|
374
|
+
addRow();
|
|
375
|
+
|
|
376
|
+
fireEvent.focus(valueInput());
|
|
377
|
+
|
|
378
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
379
|
+
expect(openOptions()).toEqual([]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("with no valueSuggestions prop at all, nothing is offered", () => {
|
|
383
|
+
renderDictionary({ valueSuggestions: undefined });
|
|
384
|
+
addRowWithKey("env");
|
|
385
|
+
|
|
386
|
+
fireEvent.focus(valueInput());
|
|
387
|
+
|
|
388
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
389
|
+
expect(openOptions()).toEqual([]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("typing narrows the list to matching values", () => {
|
|
393
|
+
renderDictionary();
|
|
394
|
+
addRowWithKey("env");
|
|
395
|
+
|
|
396
|
+
fireEvent.change(valueInput(), { target: { value: "st" } });
|
|
397
|
+
|
|
398
|
+
expect(openOptions()).toEqual(["staging"]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("picking a value fills the box and emits it", () => {
|
|
402
|
+
const rendered: RenderResult = renderDictionary();
|
|
403
|
+
addRowWithKey("env");
|
|
404
|
+
|
|
405
|
+
fireEvent.focus(valueInput());
|
|
406
|
+
fireEvent.click(screen.getByText("staging"));
|
|
407
|
+
|
|
408
|
+
expect(valueInput().value).toBe("staging");
|
|
409
|
+
expect(rendered.lastEmitted()).toEqual({ env: "staging" });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("changing the key swaps the suggestion set", () => {
|
|
413
|
+
renderDictionary();
|
|
414
|
+
addRowWithKey("env");
|
|
415
|
+
|
|
416
|
+
fireEvent.focus(valueInput());
|
|
417
|
+
expect(openOptions()).toContain("production");
|
|
418
|
+
|
|
419
|
+
typeKey("region");
|
|
420
|
+
fireEvent.focus(valueInput());
|
|
421
|
+
|
|
422
|
+
expect(openOptions()).toEqual(["us-east-1", "eu-west-2"]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("changing to a key with no list closes the offer", () => {
|
|
426
|
+
renderDictionary();
|
|
427
|
+
addRowWithKey("env");
|
|
428
|
+
|
|
429
|
+
fireEvent.focus(valueInput());
|
|
430
|
+
expect(openOptions()).toContain("production");
|
|
431
|
+
|
|
432
|
+
typeKey("some.unknown.attribute");
|
|
433
|
+
|
|
434
|
+
expect(openOptions()).toEqual([]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("each row is offered its own key's values", () => {
|
|
438
|
+
renderDictionary();
|
|
439
|
+
addRowWithKey("env");
|
|
440
|
+
addRowWithKey("region");
|
|
441
|
+
|
|
442
|
+
fireEvent.focus(valueInput(0));
|
|
443
|
+
expect(openOptions()).toEqual(["production", "staging", "development"]);
|
|
444
|
+
|
|
445
|
+
fireEvent.blur(valueInput(0));
|
|
446
|
+
fireEvent.keyDown(valueInput(0), { key: "Escape" });
|
|
447
|
+
|
|
448
|
+
fireEvent.focus(valueInput(1));
|
|
449
|
+
expect(openOptions()).toContain("us-east-1");
|
|
450
|
+
expect(openOptions()).not.toContain("production");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("suggestions do not stop the user typing a value of their own", () => {
|
|
454
|
+
const rendered: RenderResult = renderDictionary();
|
|
455
|
+
addRowWithKey("env");
|
|
456
|
+
|
|
457
|
+
fireEvent.change(valueInput(), { target: { value: "canary" } });
|
|
458
|
+
|
|
459
|
+
expect(valueInput().value).toBe("canary");
|
|
460
|
+
expect(rendered.lastEmitted()).toEqual({ env: "canary" });
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("the key input keeps offering keys, not values", () => {
|
|
464
|
+
renderDictionary({ keys: ["env", "region"] });
|
|
465
|
+
addRow();
|
|
466
|
+
|
|
467
|
+
fireEvent.focus(keyInput());
|
|
468
|
+
|
|
469
|
+
expect(openOptions()).toEqual(["env", "region"]);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("the multi-select an 'is any of' row turns the value box into", () => {
|
|
474
|
+
test("offers the key's values as its options", () => {
|
|
475
|
+
renderDictionary({ enableOperators: true });
|
|
476
|
+
addRowWithKey("env");
|
|
477
|
+
|
|
478
|
+
selectOption(operatorSelect(), "is any of");
|
|
479
|
+
fireEvent.keyDown(valueSelect(), { key: "ArrowDown" });
|
|
480
|
+
|
|
481
|
+
expect(openOptions()).toEqual(["production", "staging", "development"]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("offers nothing for a key with no list of its own", () => {
|
|
485
|
+
renderDictionary({ enableOperators: true });
|
|
486
|
+
addRowWithKey("some.unknown.attribute");
|
|
487
|
+
|
|
488
|
+
selectOption(operatorSelect(), "is any of");
|
|
489
|
+
fireEvent.keyDown(valueSelect(), { key: "ArrowDown" });
|
|
490
|
+
|
|
491
|
+
expect(openOptions()).toEqual([]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("emits the picked values as an Includes", () => {
|
|
495
|
+
const rendered: RenderResult = renderDictionary({
|
|
496
|
+
enableOperators: true,
|
|
497
|
+
});
|
|
498
|
+
addRowWithKey("env");
|
|
499
|
+
|
|
500
|
+
selectOption(operatorSelect(), "is any of");
|
|
501
|
+
selectOption(valueSelect(), "production");
|
|
502
|
+
|
|
503
|
+
const emittedValue: DictionaryEntryValue =
|
|
504
|
+
rendered.lastEmitted()!["env"]!;
|
|
505
|
+
expect(emittedValue).toBeInstanceOf(Includes);
|
|
506
|
+
expect((emittedValue as Includes).values).toEqual(["production"]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("a picked value becomes a chip and leaves the rest on offer", () => {
|
|
510
|
+
renderDictionary({ enableOperators: true });
|
|
511
|
+
addRowWithKey("env");
|
|
512
|
+
|
|
513
|
+
selectOption(operatorSelect(), "is any of");
|
|
514
|
+
selectOption(valueSelect(), "production");
|
|
515
|
+
|
|
516
|
+
// The picked value is now a chip, so the menu offers only what is left.
|
|
517
|
+
expect(screen.getByText("production")).toBeInTheDocument();
|
|
518
|
+
|
|
519
|
+
fireEvent.keyDown(valueSelect(), { key: "ArrowDown" });
|
|
520
|
+
|
|
521
|
+
expect(openOptions()).toEqual(["staging", "development"]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("a stored value that is no longer suggested is still shown", () => {
|
|
525
|
+
/*
|
|
526
|
+
* Saved filters outlive the suggestion list they were picked from — the
|
|
527
|
+
* options are the union of the key's suggestions and whatever the row
|
|
528
|
+
* already holds, so a value the server no longer returns still renders
|
|
529
|
+
* rather than silently vanishing from the user's filter.
|
|
530
|
+
*/
|
|
531
|
+
renderDictionary({
|
|
532
|
+
enableOperators: true,
|
|
533
|
+
initialValue: { env: new Includes(["retired-environment"]) },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
expect(screen.getByText("retired-environment")).toBeInTheDocument();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe("loading values from the server", () => {
|
|
541
|
+
test("a key being loaded shows the loading row", () => {
|
|
542
|
+
renderDictionary({
|
|
543
|
+
valueSuggestions: {},
|
|
544
|
+
loadingValueKeys: ["env"],
|
|
545
|
+
});
|
|
546
|
+
addRowWithKey("env");
|
|
547
|
+
|
|
548
|
+
fireEvent.focus(valueInput());
|
|
549
|
+
|
|
550
|
+
expect(screen.getByText("Loading values...")).toBeInTheDocument();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("only the row whose key is loading shows it", () => {
|
|
554
|
+
renderDictionary({
|
|
555
|
+
valueSuggestions: {},
|
|
556
|
+
loadingValueKeys: ["env"],
|
|
557
|
+
});
|
|
558
|
+
addRowWithKey("region");
|
|
559
|
+
|
|
560
|
+
fireEvent.focus(valueInput());
|
|
561
|
+
|
|
562
|
+
expect(screen.queryByText("Loading values...")).not.toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("typing a value asks the parent to refine the list", () => {
|
|
566
|
+
const rendered: RenderResult = renderDictionary();
|
|
567
|
+
addRowWithKey("env");
|
|
568
|
+
|
|
569
|
+
fireEvent.change(valueInput(), { target: { value: "prod" } });
|
|
570
|
+
|
|
571
|
+
expect(rendered.searches).toEqual([{ key: "env", searchText: "prod" }]);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("a row with no key does not ask", () => {
|
|
575
|
+
const rendered: RenderResult = renderDictionary();
|
|
576
|
+
addRow();
|
|
577
|
+
|
|
578
|
+
fireEvent.change(valueInput(), { target: { value: "prod" } });
|
|
579
|
+
|
|
580
|
+
expect(rendered.searches).toEqual([]);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("a numeric operator does not ask — it has no values to narrow", () => {
|
|
584
|
+
const rendered: RenderResult = renderDictionary({
|
|
585
|
+
enableOperators: true,
|
|
586
|
+
});
|
|
587
|
+
addRowWithKey("env");
|
|
588
|
+
|
|
589
|
+
selectOption(operatorSelect(), ">");
|
|
590
|
+
fireEvent.change(valueInput(), { target: { value: "500" } });
|
|
591
|
+
|
|
592
|
+
expect(rendered.searches).toEqual([]);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Icon from "../../../UI/Components/Icon/Icon";
|
|
2
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
3
|
+
import { describe, expect, it } from "@jest/globals";
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
type GetIconPathFunction = (icon: IconProp) => string;
|
|
9
|
+
|
|
10
|
+
const getIconPath: GetIconPathFunction = (icon: IconProp): string => {
|
|
11
|
+
const { container } = render(<Icon icon={icon} />);
|
|
12
|
+
return container.querySelector("path")?.getAttribute("d") || "";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The chevrons are the whole vocabulary of the pagination arrows and of every
|
|
17
|
+
* "back" button in the dashboard, and they are picked out of one long
|
|
18
|
+
* if/else chain of hand-written path data - which is exactly how ChevronLeft
|
|
19
|
+
* came to be drawing the bulleted-list glyph.
|
|
20
|
+
*/
|
|
21
|
+
describe("Icon chevrons", () => {
|
|
22
|
+
it("draws a left-pointing chevron for ChevronLeft", () => {
|
|
23
|
+
expect(getIconPath(IconProp.ChevronLeft)).toBe(
|
|
24
|
+
"M15.75 19.5L8.25 12l7.5-7.5",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("draws a right-pointing chevron for ChevronRight", () => {
|
|
29
|
+
expect(getIconPath(IconProp.ChevronRight)).toBe(
|
|
30
|
+
"M8.25 4.5l7.5 7.5-7.5 7.5",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("draws a down-pointing chevron for ChevronDown", () => {
|
|
35
|
+
expect(getIconPath(IconProp.ChevronDown)).toBe(
|
|
36
|
+
"M19.5 8.25l-7.5 7.5-7.5-7.5",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does not draw a list where a chevron was asked for", () => {
|
|
41
|
+
const listPath: string = getIconPath(IconProp.List);
|
|
42
|
+
|
|
43
|
+
expect(getIconPath(IconProp.ChevronLeft)).not.toBe(listPath);
|
|
44
|
+
expect(getIconPath(IconProp.ChevronRight)).not.toBe(listPath);
|
|
45
|
+
expect(getIconPath(IconProp.ChevronDown)).not.toBe(listPath);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("gives each chevron its own direction", () => {
|
|
49
|
+
const paths: Array<string> = [
|
|
50
|
+
getIconPath(IconProp.ChevronLeft),
|
|
51
|
+
getIconPath(IconProp.ChevronRight),
|
|
52
|
+
getIconPath(IconProp.ChevronDown),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -92,10 +92,27 @@ describe("List", () => {
|
|
|
92
92
|
|
|
93
93
|
it("handles onNavigateToPage callback", () => {
|
|
94
94
|
render(<List {...defaultProps} />);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
|
|
96
|
+
fireEvent.click(screen.getByTestId("pagination-next-button"));
|
|
97
|
+
|
|
98
|
+
expect(defaultProps.onNavigateToPage).toHaveBeenCalledWith(2, 5);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("jumps straight to a page from the list footer", () => {
|
|
102
|
+
render(<List {...defaultProps} />);
|
|
103
|
+
|
|
104
|
+
fireEvent.click(screen.getByTestId("pagination-page-2"));
|
|
98
105
|
|
|
99
106
|
expect(defaultProps.onNavigateToPage).toHaveBeenCalledWith(2, 5);
|
|
100
107
|
});
|
|
108
|
+
|
|
109
|
+
it("changes the page size from the list footer", () => {
|
|
110
|
+
render(<List {...defaultProps} />);
|
|
111
|
+
|
|
112
|
+
fireEvent.change(screen.getByTestId("pagination-items-on-page-select"), {
|
|
113
|
+
target: { value: "25" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(defaultProps.onNavigateToPage).toHaveBeenCalledWith(1, 25);
|
|
117
|
+
});
|
|
101
118
|
});
|