mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 15:06:02 -03:30
Adds tests and refines chip interaction in MultiSelect component
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
import { Chip, ChipGroup } from '@components/Chip';
|
import { Chip, ChipGroup } from '@components/Chip';
|
||||||
import {
|
import {
|
||||||
Dropdown as PFDropdown,
|
Dropdown as PFDropdown,
|
||||||
@@ -123,7 +125,7 @@ class MultiSelect extends Component {
|
|||||||
removeChip(e, item) {
|
removeChip(e, item) {
|
||||||
const { onRemoveItem } = this.props;
|
const { onRemoveItem } = this.props;
|
||||||
const { chipItems } = this.state;
|
const { chipItems } = this.state;
|
||||||
const chips = chipItems.filter(chip => chip.name !== item.name);
|
const chips = chipItems.filter(chip => chip.id !== item.id);
|
||||||
|
|
||||||
this.setState({ chipItems: chips });
|
this.setState({ chipItems: chips });
|
||||||
onRemoveItem(item);
|
onRemoveItem(item);
|
||||||
@@ -199,4 +201,5 @@ class MultiSelect extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default MultiSelect;
|
export { MultiSelect as _MultiSelect };
|
||||||
|
export default withI18n()(withRouter(MultiSelect));
|
||||||
|
|||||||
86
awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
Normal file
86
awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MultiSelect, { _MultiSelect } from './MultiSelect';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
describe('<MultiSelect />', () => {
|
||||||
|
const associatedItems = [
|
||||||
|
{ name: 'Foo', id: 1, organization: 1 },
|
||||||
|
{ name: 'Bar', id: 2, organization: 1 },
|
||||||
|
];
|
||||||
|
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
|
||||||
|
|
||||||
|
test('Initially render successfully', () => {
|
||||||
|
const renderChips = jest.spyOn(_MultiSelect.prototype, 'renderChips');
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={jest.fn()}
|
||||||
|
onRemoveItem={jest.fn()}
|
||||||
|
associatedItems={associatedItems}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const component = wrapper.find('MultiSelect');
|
||||||
|
|
||||||
|
expect(renderChips).toBeCalled();
|
||||||
|
expect(component.state().chipItems.length).toBe(2);
|
||||||
|
});
|
||||||
|
test('handleSelection add item to chipItems', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={jest.fn()}
|
||||||
|
onRemoveItem={jest.fn()}
|
||||||
|
associatedItems={associatedItems}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const event = { preventDefault: () => {} };
|
||||||
|
const component = wrapper.find('MultiSelect');
|
||||||
|
component.instance().handleSelection(event, { name: 'Apollo', id: 5 });
|
||||||
|
expect(component.state().chipItems.length).toBe(3);
|
||||||
|
});
|
||||||
|
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
||||||
|
const onAddNewItem = jest.fn();
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={onAddNewItem}
|
||||||
|
onRemoveItem={jest.fn()}
|
||||||
|
associatedItems={associatedItems}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const event = {
|
||||||
|
preventDefault: () => {},
|
||||||
|
key: 'Tab',
|
||||||
|
};
|
||||||
|
const component = wrapper.find('MultiSelect');
|
||||||
|
|
||||||
|
component.setState({ input: 'newLabel' });
|
||||||
|
component.update();
|
||||||
|
component.instance().handleAddItem(event);
|
||||||
|
expect(component.state().chipItems.length).toBe(3);
|
||||||
|
expect(component.state().input.length).toBe(0);
|
||||||
|
expect(component.state().isExpanded).toBe(false);
|
||||||
|
expect(onAddNewItem).toBeCalled();
|
||||||
|
});
|
||||||
|
test('removeChip removes chip properly', () => {
|
||||||
|
const onRemoveItem = jest.fn();
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={jest.fn()}
|
||||||
|
onRemoveItem={onRemoveItem}
|
||||||
|
associatedItems={associatedItems}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const event = {
|
||||||
|
preventDefault: () => {},
|
||||||
|
};
|
||||||
|
const component = wrapper.find('MultiSelect');
|
||||||
|
component
|
||||||
|
.instance()
|
||||||
|
.removeChip(event, { name: 'Foo', id: 1, organization: 1 });
|
||||||
|
expect(component.state().chipItems.length).toBe(1);
|
||||||
|
expect(onRemoveItem).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,19 +75,22 @@ class JobTemplateForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadLabels(QueryConfig) {
|
async loadLabels(QueryConfig) {
|
||||||
const { loadedLabels } = this.state;
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
|
let loadedLabels;
|
||||||
try {
|
try {
|
||||||
const { data } = await LabelsAPI.read(QueryConfig);
|
const { data } = await LabelsAPI.read(QueryConfig);
|
||||||
const labels = [...data.results];
|
loadedLabels = [...data.results];
|
||||||
this.setState({ loadedLabels: loadedLabels.concat(labels) });
|
|
||||||
if (data.next && data.next.includes('page=2')) {
|
if (data.next && data.next.includes('page=2')) {
|
||||||
this.loadLabels({
|
const {
|
||||||
|
data: { results },
|
||||||
|
} = await LabelsAPI.read({
|
||||||
page: 2,
|
page: 2,
|
||||||
page_size: 200,
|
page_size: 200,
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
loadedLabels = loadedLabels.concat(results);
|
||||||
}
|
}
|
||||||
|
this.setState({ loadedLabels });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ contentError: err });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -116,19 +119,22 @@ class JobTemplateForm extends Component {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
newLabels: [...newLabels, { associate: true, id: label.id }],
|
newLabels: [
|
||||||
|
...newLabels,
|
||||||
|
{ name: label.name, associate: true, id: label.id },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disassociateLabel(label) {
|
disassociateLabel(label) {
|
||||||
const { removedLabels, newLabels } = this.state;
|
const { removedLabels, loadedLabels, newLabels } = this.state;
|
||||||
const isNewCreatedLabel = newLabels.some(
|
const isNewCreatedLabel = loadedLabels.some(
|
||||||
newLabel => newLabel === label.name
|
loadedLabel => loadedLabel.name !== label.name
|
||||||
);
|
);
|
||||||
if (isNewCreatedLabel) {
|
if (isNewCreatedLabel) {
|
||||||
const filteredLabels = newLabels.filter(
|
const filteredLabels = newLabels.filter(
|
||||||
newLabel => newLabel !== label.name
|
newLabel => newLabel.name !== label.name
|
||||||
);
|
);
|
||||||
this.setState({ newLabels: filteredLabels });
|
this.setState({ newLabels: filteredLabels });
|
||||||
} else {
|
} else {
|
||||||
@@ -277,21 +283,17 @@ class JobTemplateForm extends Component {
|
|||||||
>
|
>
|
||||||
<QuestionCircleIcon />
|
<QuestionCircleIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Field
|
<MultiSelect
|
||||||
render={() => (
|
onAddNewItem={this.handleNewLabel}
|
||||||
<MultiSelect
|
onRemoveItem={this.disassociateLabel}
|
||||||
onAddNewItem={this.handleNewLabel}
|
associatedItems={template.summary_fields.labels.results}
|
||||||
onRemoveItem={this.disassociateLabel}
|
options={loadedLabels}
|
||||||
associatedItems={template.summary_fields.labels.results}
|
|
||||||
options={loadedLabels}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormActionGroup
|
<FormActionGroup
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={values => handleSubmit(values)}
|
onSubmit={formik.handleSubmit}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
import JobTemplateForm from './JobTemplateForm';
|
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
|
||||||
|
import { LabelsAPI } from '@api';
|
||||||
|
|
||||||
jest.mock('@api');
|
jest.mock('@api');
|
||||||
|
|
||||||
@@ -19,10 +20,16 @@ describe('<JobTemplateForm />', () => {
|
|||||||
inventory: {
|
inventory: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
|
organization_id: 1,
|
||||||
},
|
},
|
||||||
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
|
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
LabelsAPI.read.mockReturnValue({
|
||||||
|
data: mockData.summary_fields.labels,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -36,6 +43,7 @@ describe('<JobTemplateForm />', () => {
|
|||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
expect(LabelsAPI.read).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update form values on input changes', async () => {
|
test('should update form values on input changes', async () => {
|
||||||
@@ -104,4 +112,59 @@ describe('<JobTemplateForm />', () => {
|
|||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
expect(handleCancel).toBeCalled();
|
expect(handleCancel).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handleNewLabel should arrange new labels properly', async () => {
|
||||||
|
const handleNewLabel = jest.spyOn(
|
||||||
|
_JobTemplateForm.prototype,
|
||||||
|
'handleNewLabel'
|
||||||
|
);
|
||||||
|
const event = { key: 'Tab' };
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<JobTemplateForm
|
||||||
|
template={mockData}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const multiSelect = wrapper.find('MultiSelect');
|
||||||
|
const component = wrapper.find('JobTemplateForm');
|
||||||
|
|
||||||
|
wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] });
|
||||||
|
multiSelect.setState({ input: 'Foo' });
|
||||||
|
|
||||||
|
wrapper.find('input[aria-label="labels"]').prop('onKeyDown')(event);
|
||||||
|
expect(handleNewLabel).toHaveBeenCalledWith('Foo');
|
||||||
|
|
||||||
|
component.instance().handleNewLabel({ name: 'Bar', id: 2 });
|
||||||
|
expect(component.state().newLabels).toEqual([
|
||||||
|
{ name: 'Foo', organization: 1 },
|
||||||
|
{ associate: true, id: 2, name: 'Bar' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test('disassociateLabel should arrange new labels properly', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<JobTemplateForm
|
||||||
|
template={mockData}
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const multiSelect = wrapper.find('MultiSelect');
|
||||||
|
const component = wrapper.find('JobTemplateForm');
|
||||||
|
|
||||||
|
component.setState({
|
||||||
|
newLabels: [{ name: 'Foo', id: 1 }],
|
||||||
|
loadedLabels: [{ name: 'Bar', id: 3 }],
|
||||||
|
removedLabels: [],
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
multiSelect.setState({ input: 'Wowza' });
|
||||||
|
component.instance().disassociateLabel({ name: 'Foo', id: 1 });
|
||||||
|
expect(component.state().newLabels.length).toBe(0);
|
||||||
|
expect(component.state().removedLabels.length).toBe(0);
|
||||||
|
|
||||||
|
component.instance().disassociateLabel({ name: 'Bar', id: 3 });
|
||||||
|
expect(component.state().newLabels.length).toBe(0);
|
||||||
|
expect(component.state().removedLabels.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user