From a4eb6f947088a3e88be8c0d61e97aad4140b9bde Mon Sep 17 00:00:00 2001 From: soleksy-splunk <143183665+soleksy-splunk@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:14:01 +0100 Subject: [PATCH] fix: pass disabled props for radio bar component (#997) --- .../schema/schema.json | 3 + .../globalConfig.json | 2 +- .../CheckboxGroup/CheckboxGroup.test.tsx | 176 ++++++++++++++++++ .../CheckboxGroup/CheckboxGroup.tsx | 5 +- .../CheckboxGroup/CheckboxRowWrapper.tsx | 3 + .../CheckboxGroup/CheckboxSubGroup.tsx | 4 +- .../CheckboxGroup/checkboxGroup.utils.ts | 1 + .../FileInputComponent.test.tsx | 30 +++ .../MultiInputComponent.stories.tsx | 0 .../MultiInputComponent.test.tsx | 53 ++++++ .../MultiInputComponent.tsx | 6 +- .../RadioComponent.stories.ts | 1 + .../RadioComponent/RadioComponent.test.tsx | 110 +++++++++++ .../{ => RadioComponent}/RadioComponent.tsx | 8 +- ui/src/constants/ControlTypeMap.ts | 4 +- 15 files changed, 396 insertions(+), 10 deletions(-) create mode 100644 ui/src/components/CheckboxGroup/CheckboxGroup.test.tsx rename ui/src/components/{ => MultiInputComponent}/MultiInputComponent.stories.tsx (100%) create mode 100644 ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx rename ui/src/components/{ => MultiInputComponent}/MultiInputComponent.tsx (96%) rename ui/src/components/{ => RadioComponent}/RadioComponent.stories.ts (97%) create mode 100644 ui/src/components/RadioComponent/RadioComponent.test.tsx rename ui/src/components/{ => RadioComponent}/RadioComponent.tsx (80%) diff --git a/splunk_add_on_ucc_framework/schema/schema.json b/splunk_add_on_ucc_framework/schema/schema.json index 6598c2860..7f17c0fdf 100644 --- a/splunk_add_on_ucc_framework/schema/schema.json +++ b/splunk_add_on_ucc_framework/schema/schema.json @@ -936,6 +936,9 @@ "additionalProperties": false }, "minItems": 1 + }, + "disableonEdit": { + "type": "boolean" } }, "required": ["rows"], diff --git a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json index b6a339c4c..0eefdc20f 100644 --- a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json +++ b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json @@ -1375,7 +1375,7 @@ "meta": { "name": "Splunk_TA_UCCExample", "restRoot": "splunk_ta_uccexample", - "version": "5.35.1Ree62a73b", + "version": "5.35.1R7fe3d58d", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.3" } diff --git a/ui/src/components/CheckboxGroup/CheckboxGroup.test.tsx b/ui/src/components/CheckboxGroup/CheckboxGroup.test.tsx new file mode 100644 index 000000000..a1d2704c4 --- /dev/null +++ b/ui/src/components/CheckboxGroup/CheckboxGroup.test.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CheckboxGroup from './CheckboxGroup'; +import { CheckboxGroupProps } from './checkboxGroup.utils'; +import { MODE_CREATE } from '../../constants/modes'; + +const handleChange = jest.fn(); + +const defaultCheckboxProps: CheckboxGroupProps = { + mode: MODE_CREATE, + field: 'api', + value: 'collect_collaboration/1200,collect_file/1,collect_task/1', + controlOptions: { + rows: [ + { + field: 'collect_collaboration', + checkbox: { + label: 'Collect folder collaboration', + }, + input: { + defaultValue: 1200, + required: false, + }, + }, + { + field: 'collect_file', + checkbox: { + label: 'Collect file metadata', + }, + input: { + defaultValue: 1, + required: true, + }, + }, + { + field: 'collect_task', + checkbox: { + label: 'Collect tasks and comments', + }, + input: { + defaultValue: 1, + required: true, + }, + }, + ], + }, + handleChange, +}; + +const renderFeature = (additionalProps?: Partial) => { + const props = { + ...defaultCheckboxProps, + ...additionalProps, + }; + render(); +}; + +it('renders checkbox group correctly', async () => { + renderFeature(); + defaultCheckboxProps.controlOptions.rows.forEach((row) => { + const optionWithCorrectText = screen.getByText(row?.checkbox?.label || 'unexisting string'); + expect(optionWithCorrectText).toBeInTheDocument(); + expect(optionWithCorrectText.dataset?.disabled).toBeUndefined(); + }); +}); + +it('renders disabled checkbox group correctly', async () => { + renderFeature({ disabled: true }); + defaultCheckboxProps.controlOptions.rows.forEach((row) => { + const optionWithCorrectText = screen.getByText(row?.checkbox?.label || 'unexisting string'); + expect(optionWithCorrectText).toBeInTheDocument(); + expect(optionWithCorrectText.dataset.disabled).toEqual('true'); + }); +}); + +it('increment value correctly', async () => { + renderFeature(); + const incrementButtons = screen.getAllByTestId('increment'); + const [firstIncrementer, secondIncrementer, thirdIncrementer] = incrementButtons; + + await userEvent.click(firstIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1201,collect_file/1,collect_task/1', + 'checkboxGroup' + ); + await userEvent.click(secondIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1201,collect_file/2,collect_task/1', + 'checkboxGroup' + ); + await userEvent.click(thirdIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1201,collect_file/2,collect_task/2', + 'checkboxGroup' + ); +}); + +it('decrement value correctly', async () => { + renderFeature(); + const decrementButtons = screen.getAllByTestId('decrement'); + const [firstDecrement, secondDecrement, thirdDecrement] = decrementButtons; + + await userEvent.click(firstDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/1,collect_task/1', + 'checkboxGroup' + ); + await userEvent.click(secondDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/0,collect_task/1', + 'checkboxGroup' + ); + await userEvent.click(thirdDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/0,collect_task/0', + 'checkboxGroup' + ); +}); + +it('mixed incrementing and decrementing value correctly', async () => { + renderFeature(); + const incrementButtons = screen.getAllByTestId('increment'); + const [firstIncrementer, secondIncrementer, thirdIncrementer] = incrementButtons; + + const decrementButtons = screen.getAllByTestId('decrement'); + const [firstDecrement, secondDecrement, thirdDecrement] = decrementButtons; + + await userEvent.click(firstDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/1,collect_task/1', + 'checkboxGroup' + ); + + await userEvent.click(thirdIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/1,collect_task/2', + 'checkboxGroup' + ); + + await userEvent.click(secondDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1199,collect_file/0,collect_task/2', + 'checkboxGroup' + ); + + await userEvent.click(firstIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1200,collect_file/0,collect_task/2', + 'checkboxGroup' + ); + + await userEvent.click(thirdDecrement); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + 'collect_collaboration/1200,collect_file/0,collect_task/1', + 'checkboxGroup' + ); + + await userEvent.click(secondIncrementer); + expect(handleChange).toHaveBeenCalledWith( + defaultCheckboxProps.field, + defaultCheckboxProps.value, + 'checkboxGroup' + ); +}); diff --git a/ui/src/components/CheckboxGroup/CheckboxGroup.tsx b/ui/src/components/CheckboxGroup/CheckboxGroup.tsx index ed3e8275c..bf8e9e0dd 100644 --- a/ui/src/components/CheckboxGroup/CheckboxGroup.tsx +++ b/ui/src/components/CheckboxGroup/CheckboxGroup.tsx @@ -17,8 +17,7 @@ import { useValidation } from './checkboxGroupValidation'; import { MODE_CREATE } from '../../constants/modes'; function CheckboxGroup(props: CheckboxGroupProps) { - const { field, handleChange, controlOptions, addCustomValidator } = props; - + const { field, handleChange, controlOptions, addCustomValidator, disabled } = props; const flattenedRowsWithGroups = getFlattenRowsWithGroups(controlOptions); const shouldUseDefaultValue = props.mode === MODE_CREATE && props.value === null; const value = shouldUseDefaultValue @@ -71,6 +70,7 @@ function CheckboxGroup(props: CheckboxGroupProps) { group={row} values={values} handleRowChange={handleRowChange} + disabled={disabled} /> ); @@ -81,6 +81,7 @@ function CheckboxGroup(props: CheckboxGroupProps) { row={row} values={values} handleRowChange={handleRowChange} + disabled={disabled} /> ); diff --git a/ui/src/components/CheckboxGroup/CheckboxRowWrapper.tsx b/ui/src/components/CheckboxGroup/CheckboxRowWrapper.tsx index b1554d606..9aabaec57 100644 --- a/ui/src/components/CheckboxGroup/CheckboxRowWrapper.tsx +++ b/ui/src/components/CheckboxGroup/CheckboxRowWrapper.tsx @@ -6,10 +6,12 @@ function CheckboxRowWrapper({ row, values, handleRowChange, + disabled, }: { row: Row; values: ValueByField; handleRowChange: (newValue: { field: string; checkbox: boolean; text?: string }) => void; + disabled?: boolean; }) { const valueForField = values.get(row.field); return ( @@ -19,6 +21,7 @@ function CheckboxRowWrapper({ checkbox={!!valueForField?.checkbox} input={valueForField ? valueForField.inputValue : row.input?.defaultValue} handleChange={handleRowChange} + disabled={disabled} /> ); } diff --git a/ui/src/components/CheckboxGroup/CheckboxSubGroup.tsx b/ui/src/components/CheckboxGroup/CheckboxSubGroup.tsx index 25f738a98..4d6e636bc 100644 --- a/ui/src/components/CheckboxGroup/CheckboxSubGroup.tsx +++ b/ui/src/components/CheckboxGroup/CheckboxSubGroup.tsx @@ -14,9 +14,10 @@ interface CheckboxSubGroupProps { group: GroupWithRows; values: ValueByField; handleRowChange: (newValue: { field: string; checkbox: boolean; text?: string }) => void; + disabled?: boolean; } -function CheckboxSubGroup({ group, values, handleRowChange }: CheckboxSubGroupProps) { +function CheckboxSubGroup({ group, values, handleRowChange, disabled }: CheckboxSubGroupProps) { const checkedCheckboxesCount = getCheckedCheckboxesCount(group, values); return ( {group.rows.map((rowInsideGroup) => ( void ) => void; handleChange: (field: string, value: string, componentType?: 'checkboxGroup') => void; + disabled?: boolean; } export function isGroupWithRows(item: GroupWithRows | Row): item is GroupWithRows { diff --git a/ui/src/components/FileInputComponent/FileInputComponent.test.tsx b/ui/src/components/FileInputComponent/FileInputComponent.test.tsx index d3a87b109..c48f9f505 100644 --- a/ui/src/components/FileInputComponent/FileInputComponent.test.tsx +++ b/ui/src/components/FileInputComponent/FileInputComponent.test.tsx @@ -316,3 +316,33 @@ test('Default error message disappears when reuploading encrypted file', async ( const nullHelpElement = await screen.queryByTestId('help'); expect(nullHelpElement).toBeNull(); }); + +it('File input disabled - rendered correctly with disabled attr', () => { + // throws error Could not parse CSS styleshee, which only obscures tests + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + + const field = 'testFileField'; + const disabled = true; + const controlOptions = { + supportedFileTypes: ['json'], + maxFileSize: 10, + }; + const handleChange = jest.fn(); + const testFileName = 'testFileName.json'; + + render( + + ); + + const fileComponent = screen.getByTestId('file-input'); + expect(fileComponent).toBeInTheDocument(); + expect(fileComponent).toHaveAttribute('disabled'); + consoleSpy.mockRestore(); +}); diff --git a/ui/src/components/MultiInputComponent.stories.tsx b/ui/src/components/MultiInputComponent/MultiInputComponent.stories.tsx similarity index 100% rename from ui/src/components/MultiInputComponent.stories.tsx rename to ui/src/components/MultiInputComponent/MultiInputComponent.stories.tsx diff --git a/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx b/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx new file mode 100644 index 000000000..6051bd45d --- /dev/null +++ b/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import MultiInputComponent, { MultiInputComponentProps } from './MultiInputComponent'; + +const handleChange = jest.fn(); + +const defaultInputProps: MultiInputComponentProps = { + field: 'defaultFieldName', + controlOptions: { + delimiter: ',', + createSearchChoice: true, + referenceName: 'referenceName', + dependencies: undefined, + endpointUrl: undefined, + labelField: 'labelField', + items: [ + { label: 'label1', value: 'value1' }, + { label: 'label2', value: 'value2' }, + { label: 'label3', value: 'value3' }, + ], + }, + disabled: false, + value: 'defaultValue', + error: false, + dependencyValues: {}, + handleChange, +}; + +const renderFeature = (additionalProps?: Partial) => { + const props = { + ...defaultInputProps, + ...additionalProps, + }; + render(); +}; + +it('renders correctly', () => { + renderFeature(); + const inputComponent = screen.getByTestId('multiselect'); + expect(inputComponent).toBeInTheDocument(); + expect(inputComponent.getAttribute('data-test-values')).toEqual( + // eslint-disable-next-line no-useless-escape + `[\"${defaultInputProps.value}\"]` + ); +}); + +it('renders as disabled correctly', () => { + renderFeature({ disabled: true }); + const inputComponent = screen.getByTestId('multiselect'); + expect(inputComponent).toBeInTheDocument(); + expect(inputComponent.getAttribute('aria-disabled')).toEqual('true'); +}); diff --git a/ui/src/components/MultiInputComponent.tsx b/ui/src/components/MultiInputComponent/MultiInputComponent.tsx similarity index 96% rename from ui/src/components/MultiInputComponent.tsx rename to ui/src/components/MultiInputComponent/MultiInputComponent.tsx index 5a202c049..4a6caef3b 100644 --- a/ui/src/components/MultiInputComponent.tsx +++ b/ui/src/components/MultiInputComponent/MultiInputComponent.tsx @@ -4,8 +4,8 @@ import styled from 'styled-components'; import axios from 'axios'; import WaitSpinner from '@splunk/react-ui/WaitSpinner'; -import { axiosCallWrapper } from '../util/axiosCallWrapper'; -import { filterResponse } from '../util/util'; +import { axiosCallWrapper } from '../../util/axiosCallWrapper'; +import { filterResponse } from '../../util/util'; const MultiSelectWrapper = styled(Multiselect)` width: 320px !important; @@ -15,7 +15,7 @@ const WaitSpinnerWrapper = styled(WaitSpinner)` margin-left: 5px; `; -interface MultiInputComponentProps { +export interface MultiInputComponentProps { handleChange: (field: string, data: string) => void; field: string; controlOptions: { diff --git a/ui/src/components/RadioComponent.stories.ts b/ui/src/components/RadioComponent/RadioComponent.stories.ts similarity index 97% rename from ui/src/components/RadioComponent.stories.ts rename to ui/src/components/RadioComponent/RadioComponent.stories.ts index 74358a340..a68cf27dc 100644 --- a/ui/src/components/RadioComponent.stories.ts +++ b/ui/src/components/RadioComponent/RadioComponent.stories.ts @@ -29,5 +29,6 @@ export const Base: Story = { }, ], }, + disabled: false, }, }; diff --git a/ui/src/components/RadioComponent/RadioComponent.test.tsx b/ui/src/components/RadioComponent/RadioComponent.test.tsx new file mode 100644 index 000000000..7f4f7c384 --- /dev/null +++ b/ui/src/components/RadioComponent/RadioComponent.test.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import RadioComponent from './RadioComponent'; + +const handleChange = jest.fn(); + +interface AdditionalRadioProps { + value?: string; + field?: string; + controlOptions?: { + items: { + value: string; + label: string; + }[]; + }; + disabled?: boolean; +} + +const defaultRadioProps = { + value: 'unexistingDefaultValue', + field: 'testRadioField', + controlOptions: { + items: [ + { + value: 'uniqueValue1', + label: 'label1', + }, + { + value: 'uniqueValue2', + label: 'label2', + }, + { + value: 'uniqueValue3', + label: 'label3', + }, + ], + }, + disabled: false, + handleChange, +}; + +const renderFeature = (additionalProps?: AdditionalRadioProps) => { + const props = { + ...defaultRadioProps, + ...additionalProps, + }; + render(); +}; + +it('renders correctly', () => { + renderFeature(); + defaultRadioProps.controlOptions.items.forEach((option) => { + const optionWithCorrectText = screen.getByText(option.label); + expect(optionWithCorrectText).toBeInTheDocument(); + expect(optionWithCorrectText).not.toHaveAttribute('disabled'); + }); +}); + +it('renders disabled', () => { + renderFeature({ disabled: true }); + defaultRadioProps.controlOptions.items.forEach((option) => { + const optionWithCorrectText = screen.getByText(option.label); + expect(optionWithCorrectText).toBeInTheDocument(); + expect(optionWithCorrectText.parentElement).toHaveAttribute('disabled'); + }); +}); + +it('clicks invoke callback correctly', async () => { + renderFeature(); + const allRadioOptions = screen.getAllByRole('radio'); + const secondOption = allRadioOptions[1]; + + expect(secondOption).toBeInTheDocument(); + await userEvent.click(secondOption); + + expect(handleChange).toHaveBeenCalledWith( + defaultRadioProps.field, + defaultRadioProps.controlOptions.items[1].value + ); + + const thirdOption = allRadioOptions[2]; + await userEvent.click(thirdOption); + + expect(handleChange).toHaveBeenCalledWith( + defaultRadioProps.field, + defaultRadioProps.controlOptions.items[2].value + ); + + const firstOption = allRadioOptions[0]; + await userEvent.click(firstOption); + + expect(handleChange).toHaveBeenCalledWith( + defaultRadioProps.field, + defaultRadioProps.controlOptions.items[0].value + ); +}); + +it('do not call change callback when already selected option clicked', async () => { + renderFeature({ value: defaultRadioProps.controlOptions.items[0].value }); + + const allRadioOptions = screen.getAllByRole('radio'); + + await userEvent.click(allRadioOptions[1]); + await userEvent.click(allRadioOptions[2]); + await userEvent.click(allRadioOptions[0]); // selected option so no callback, as we never update state + + expect(handleChange).toHaveBeenCalledTimes(2); +}); diff --git a/ui/src/components/RadioComponent.tsx b/ui/src/components/RadioComponent/RadioComponent.tsx similarity index 80% rename from ui/src/components/RadioComponent.tsx rename to ui/src/components/RadioComponent/RadioComponent.tsx index c0e054ebe..8505077b7 100644 --- a/ui/src/components/RadioComponent.tsx +++ b/ui/src/components/RadioComponent/RadioComponent.tsx @@ -20,6 +20,7 @@ interface RadioComponentProps { label: string; }[]; }; + disabled: boolean; } class RadioComponent extends Component { @@ -36,7 +37,12 @@ class RadioComponent extends Component { key={this.props.field} > {this.props.controlOptions.items.map((item) => ( - + ))} ); diff --git a/ui/src/constants/ControlTypeMap.ts b/ui/src/constants/ControlTypeMap.ts index e1618c49d..2190ef897 100644 --- a/ui/src/constants/ControlTypeMap.ts +++ b/ui/src/constants/ControlTypeMap.ts @@ -2,9 +2,9 @@ import HelpLinkComponent from '../components/HelpLinkComponent'; import TextComponent from '../components/TextComponent/TextComponent'; import TextAreaComponent from '../components/TextAreaComponent/TextAreaComponent'; import SingleInputComponent from '../components/SingleInputComponent'; -import MultiInputComponent from '../components/MultiInputComponent'; +import MultiInputComponent from '../components/MultiInputComponent/MultiInputComponent'; import CheckBoxComponent from '../components/CheckBoxComponent/CheckBoxComponent'; -import RadioComponent from '../components/RadioComponent'; +import RadioComponent from '../components/RadioComponent/RadioComponent'; import PlaceholderComponent from '../components/PlaceholderComponent'; import CustomControl from '../components/CustomControl'; import FileInputComponent from '../components/FileInputComponent/FileInputComponent';