From 74a1ebff32a06b8698dbe8101a4cf33c040ffe3a Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 1 Aug 2019 10:39:27 -0400 Subject: [PATCH] Adds tests and refines chip interaction in MultiSelect component --- .../components/MultiSelect/MultiSelect.jsx | 7 +- .../MultiSelect/MultiSelect.test.jsx | 86 +++++++++++++++++++ .../Template/shared/JobTemplateForm.jsx | 40 +++++---- .../Template/shared/JobTemplateForm.test.jsx | 65 +++++++++++++- 4 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx index 0448f569e4..9bc8ad2a86 100644 --- a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx +++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx @@ -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)); diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx new file mode 100644 index 0000000000..368f5ca27f --- /dev/null +++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import MultiSelect, { _MultiSelect } from './MultiSelect'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +describe('', () => { + 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( + + ); + 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( + + ); + 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( + + ); + 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( + + ); + 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(); + }); +}); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 98eaa6735e..c1a041d4eb 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -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 { > - ( - - )} + handleSubmit(values)} + onSubmit={formik.handleSubmit} /> )} diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx index 2350298dbf..b0833f8168 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -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('', () => { 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('', () => { handleCancel={jest.fn()} /> ); + expect(LabelsAPI.read).toHaveBeenCalled(); }); test('should update form values on input changes', async () => { @@ -104,4 +112,59 @@ describe('', () => { 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( + + ); + 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( + + ); + 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); + }); });