Adds tests and refines chip interaction in MultiSelect component

This commit is contained in:
Alex Corey 2019-08-01 10:39:27 -04:00
parent a577be906e
commit 74a1ebff32
4 changed files with 176 additions and 22 deletions

View File

@ -1,5 +1,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { withRouter } from 'react-router-dom';
import { Chip, ChipGroup } from '@components/Chip';
import {
Dropdown as PFDropdown,
@ -123,7 +125,7 @@ class MultiSelect extends Component {
removeChip(e, item) {
const { onRemoveItem } = this.props;
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 });
onRemoveItem(item);
@ -199,4 +201,5 @@ class MultiSelect extends Component {
);
}
}
export default MultiSelect;
export { MultiSelect as _MultiSelect };
export default withI18n()(withRouter(MultiSelect));

View 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();
});
});

View File

@ -75,19 +75,22 @@ class JobTemplateForm extends Component {
}
async loadLabels(QueryConfig) {
const { loadedLabels } = this.state;
this.setState({ contentError: null, hasContentLoading: true });
let loadedLabels;
try {
const { data } = await LabelsAPI.read(QueryConfig);
const labels = [...data.results];
this.setState({ loadedLabels: loadedLabels.concat(labels) });
loadedLabels = [...data.results];
if (data.next && data.next.includes('page=2')) {
this.loadLabels({
const {
data: { results },
} = await LabelsAPI.read({
page: 2,
page_size: 200,
order_by: 'name',
});
loadedLabels = loadedLabels.concat(results);
}
this.setState({ loadedLabels });
} catch (err) {
this.setState({ contentError: err });
} finally {
@ -116,19 +119,22 @@ class JobTemplateForm extends Component {
});
} else {
this.setState({
newLabels: [...newLabels, { associate: true, id: label.id }],
newLabels: [
...newLabels,
{ name: label.name, associate: true, id: label.id },
],
});
}
}
disassociateLabel(label) {
const { removedLabels, newLabels } = this.state;
const isNewCreatedLabel = newLabels.some(
newLabel => newLabel === label.name
const { removedLabels, loadedLabels, newLabels } = this.state;
const isNewCreatedLabel = loadedLabels.some(
loadedLabel => loadedLabel.name !== label.name
);
if (isNewCreatedLabel) {
const filteredLabels = newLabels.filter(
newLabel => newLabel !== label.name
newLabel => newLabel.name !== label.name
);
this.setState({ newLabels: filteredLabels });
} else {
@ -277,21 +283,17 @@ class JobTemplateForm extends Component {
>
<QuestionCircleIcon />
</Tooltip>
<Field
render={() => (
<MultiSelect
onAddNewItem={this.handleNewLabel}
onRemoveItem={this.disassociateLabel}
associatedItems={template.summary_fields.labels.results}
options={loadedLabels}
/>
)}
<MultiSelect
onAddNewItem={this.handleNewLabel}
onRemoveItem={this.disassociateLabel}
associatedItems={template.summary_fields.labels.results}
options={loadedLabels}
/>
</FormGroup>
</FormRow>
<FormActionGroup
onCancel={handleCancel}
onSubmit={values => handleSubmit(values)}
onSubmit={formik.handleSubmit}
/>
</Form>
)}

View File

@ -1,7 +1,8 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import JobTemplateForm from './JobTemplateForm';
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
import { LabelsAPI } from '@api';
jest.mock('@api');
@ -19,10 +20,16 @@ describe('<JobTemplateForm />', () => {
inventory: {
id: 2,
name: 'foo',
organization_id: 1,
},
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
},
};
beforeEach(() => {
LabelsAPI.read.mockReturnValue({
data: mockData.summary_fields.labels,
});
});
afterEach(() => {
jest.clearAllMocks();
@ -36,6 +43,7 @@ describe('<JobTemplateForm />', () => {
handleCancel={jest.fn()}
/>
);
expect(LabelsAPI.read).toHaveBeenCalled();
});
test('should update form values on input changes', async () => {
@ -104,4 +112,59 @@ describe('<JobTemplateForm />', () => {
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
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);
});
});