Move organization form to functional component

This commit is contained in:
Jake McDermott
2019-12-18 08:40:47 -05:00
parent 2f9742e9de
commit 057320aed3
6 changed files with 311 additions and 360 deletions

View File

@@ -50,8 +50,8 @@ function OrganizationAdd({ i18n }) {
<Config> <Config>
{({ me }) => ( {({ me }) => (
<OrganizationForm <OrganizationForm
handleSubmit={handleSubmit} onSubmit={handleSubmit}
handleCancel={handleCancel} onCancel={handleCancel}
me={me || {}} me={me || {}}
/> />
)} )}

View File

@@ -8,7 +8,7 @@ import { OrganizationsAPI } from '@api';
jest.mock('@api'); jest.mock('@api');
describe('<OrganizationAdd />', () => { describe('<OrganizationAdd />', () => {
test('handleSubmit should post to api', async () => { test('onSubmit should post to api', async () => {
const updatedOrgData = { const updatedOrgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
@@ -16,11 +16,7 @@ describe('<OrganizationAdd />', () => {
}; };
await act(async () => { await act(async () => {
const wrapper = mountWithContexts(<OrganizationAdd />); const wrapper = mountWithContexts(<OrganizationAdd />);
wrapper.find('OrganizationForm').prop('handleSubmit')( wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
updatedOrgData,
[],
[]
);
}); });
expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData); expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData);
}); });
@@ -32,6 +28,9 @@ describe('<OrganizationAdd />', () => {
wrapper = mountWithContexts(<OrganizationAdd />, { wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history } }, context: { router: { history } },
}); });
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
}); });
expect(history.location.pathname).toEqual('/organizations'); expect(history.location.pathname).toEqual('/organizations');
@@ -71,16 +70,12 @@ describe('<OrganizationAdd />', () => {
context: { router: { history } }, context: { router: { history } },
}); });
await waitForElement(wrapper, 'button[aria-label="Save"]'); await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('OrganizationForm').prop('handleSubmit')( await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3], []);
orgData,
[3],
[]
);
}); });
expect(history.location.pathname).toEqual('/organizations/5'); expect(history.location.pathname).toEqual('/organizations/5');
}); });
test('handleSubmit should post instance groups', async () => { test('onSubmit should post instance groups', async () => {
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
@@ -100,15 +95,17 @@ describe('<OrganizationAdd />', () => {
wrapper = mountWithContexts(<OrganizationAdd />); wrapper = mountWithContexts(<OrganizationAdd />);
}); });
await waitForElement(wrapper, 'button[aria-label="Save"]'); await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('OrganizationForm').prop('handleSubmit')( await wrapper.find('OrganizationForm').prop('onSubmit')(orgData, [3], []);
orgData,
[3],
[]
);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3); expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3);
}); });
test('AnsibleSelect component renders if there are virtual environments', async () => { test('AnsibleSelect component renders if there are virtual environments', async () => {
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups,
},
});
const config = { const config = {
custom_virtualenvs: ['foo', 'bar'], custom_virtualenvs: ['foo', 'bar'],
}; };
@@ -116,8 +113,9 @@ describe('<OrganizationAdd />', () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<OrganizationAdd />, { wrapper = mountWithContexts(<OrganizationAdd />, {
context: { config }, context: { config },
}).find('AnsibleSelect'); });
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('FormSelect')).toHaveLength(1); expect(wrapper.find('FormSelect')).toHaveLength(1);
expect(wrapper.find('FormSelectOption')).toHaveLength(3); expect(wrapper.find('FormSelectOption')).toHaveLength(3);
expect( expect(
@@ -129,6 +127,12 @@ describe('<OrganizationAdd />', () => {
}); });
test('AnsibleSelect component does not render if there are 0 virtual environments', async () => { test('AnsibleSelect component does not render if there are 0 virtual environments', async () => {
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups,
},
});
const config = { const config = {
custom_virtualenvs: [], custom_virtualenvs: [],
}; };
@@ -136,8 +140,9 @@ describe('<OrganizationAdd />', () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<OrganizationAdd />, { wrapper = mountWithContexts(<OrganizationAdd />, {
context: { config }, context: { config },
}).find('AnsibleSelect'); });
}); });
expect(wrapper.find('FormSelect')).toHaveLength(0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('AnsibleSelect FormSelect')).toHaveLength(0);
}); });
}); });

View File

@@ -46,8 +46,8 @@ function OrganizationEdit({ organization }) {
{({ me }) => ( {({ me }) => (
<OrganizationForm <OrganizationForm
organization={organization} organization={organization}
handleSubmit={handleSubmit} onSubmit={handleSubmit}
handleCancel={handleCancel} onCancel={handleCancel}
me={me || {}} me={me || {}}
/> />
)} )}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import OrganizationEdit from './OrganizationEdit'; import OrganizationEdit from './OrganizationEdit';
jest.mock('@api'); jest.mock('@api');
@@ -18,7 +18,7 @@ describe('<OrganizationEdit />', () => {
}, },
}; };
test('handleSubmit should call api update', async () => { test('onSubmit should call api update', async () => {
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<OrganizationEdit organization={mockData} />); wrapper = mountWithContexts(<OrganizationEdit organization={mockData} />);
@@ -29,16 +29,12 @@ describe('<OrganizationEdit />', () => {
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz', custom_virtualenv: 'Buzz',
}; };
wrapper.find('OrganizationForm').prop('handleSubmit')( wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
updatedOrgData,
[],
[]
);
expect(OrganizationsAPI.update).toHaveBeenCalledWith(1, updatedOrgData); expect(OrganizationsAPI.update).toHaveBeenCalledWith(1, updatedOrgData);
}); });
test('handleSubmit associates and disassociates instance groups', async () => { test('onSubmit associates and disassociates instance groups', async () => {
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<OrganizationEdit organization={mockData} />); wrapper = mountWithContexts(<OrganizationEdit organization={mockData} />);
@@ -50,13 +46,12 @@ describe('<OrganizationEdit />', () => {
custom_virtualenv: 'Buzz', custom_virtualenv: 'Buzz',
}; };
await act(async () => { await act(async () => {
wrapper.find('OrganizationForm').invoke('handleSubmit')( wrapper.find('OrganizationForm').invoke('onSubmit')(
updatedOrgData, updatedOrgData,
[3, 4], [3, 4],
[2] [2]
); );
}); });
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 3); expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 3);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 4); expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 4);
expect(OrganizationsAPI.disassociateInstanceGroup).toHaveBeenCalledWith( expect(OrganizationsAPI.disassociateInstanceGroup).toHaveBeenCalledWith(
@@ -66,6 +61,12 @@ describe('<OrganizationEdit />', () => {
}); });
test('should navigate to organization detail when cancel is clicked', async () => { test('should navigate to organization detail when cancel is clicked', async () => {
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups,
},
});
const history = createMemoryHistory({}); const history = createMemoryHistory({});
let wrapper; let wrapper;
await act(async () => { await act(async () => {
@@ -74,9 +75,10 @@ describe('<OrganizationEdit />', () => {
{ context: { router: { history } } } { context: { router: { history } } }
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); await act(async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
});
expect(history.location.pathname).toEqual('/organizations/1/details'); expect(history.location.pathname).toEqual('/organizations/1/details');
}); });
}); });

View File

@@ -1,206 +1,174 @@
import React, { Component, Fragment } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { QuestionCircleIcon } from '@patternfly/react-icons';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { Formik, Field } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { QuestionCircleIcon } from '@patternfly/react-icons';
import { Tooltip, Form, FormGroup } from '@patternfly/react-core'; import { Tooltip, Form, FormGroup } from '@patternfly/react-core';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import { Config } from '@contexts/Config'; import { ConfigContext } from '@contexts/Config';
import AnsibleSelect from '@components/AnsibleSelect';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import FormField from '@components/FormField'; import FormField from '@components/FormField';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import AnsibleSelect from '@components/AnsibleSelect';
import { InstanceGroupsLookup } from '@components/Lookup/'; import { InstanceGroupsLookup } from '@components/Lookup/';
import { getAddedAndRemoved } from '@util/lists';
import { required, minMaxValue } from '@util/validators'; import { required, minMaxValue } from '@util/validators';
class OrganizationForm extends Component { function OrganizationForm({ organization, i18n, me, onCancel, onSubmit }) {
constructor(props) { const defaultVenv = {
super(props); label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/',
key: 'default',
};
const { custom_virtualenvs } = useContext(ConfigContext);
const [contentError, setContentError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true);
const [initialInstanceGroups, setInitialInstanceGroups] = useState([]);
const [instanceGroups, setInstanceGroups] = useState([]);
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this); const handleCancel = () => {
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind( onCancel();
this };
const handleSubmit = values => {
const { added, removed } = getAddedAndRemoved(
initialInstanceGroups,
instanceGroups
); );
this.handleSubmit = this.handleSubmit.bind(this); const addedIds = added.map(({ id }) => id);
const removedIds = removed.map(({ id }) => id);
this.state = {
instanceGroups: [],
initialInstanceGroups: [],
formIsValid: true,
};
}
async componentDidMount() {
let instanceGroups = [];
if (!this.isEditingNewOrganization()) {
try {
instanceGroups = await this.getRelatedInstanceGroups();
} catch (err) {
this.setState({ error: err });
}
}
this.setState({
instanceGroups,
initialInstanceGroups: [...instanceGroups],
});
}
async getRelatedInstanceGroups() {
const {
organization: { id },
} = this.props;
const { data } = await OrganizationsAPI.readInstanceGroups(id);
return data.results;
}
isEditingNewOrganization() {
const { organization } = this.props;
return !organization.id;
}
handleInstanceGroupsChange(instanceGroups) {
this.setState({ instanceGroups });
}
handleSubmit(values) {
const { handleSubmit } = this.props;
const { instanceGroups, initialInstanceGroups } = this.state;
const initialIds = initialInstanceGroups.map(ig => ig.id);
const updatedIds = instanceGroups.map(ig => ig.id);
const groupsToAssociate = [...updatedIds].filter(
x => !initialIds.includes(x)
);
const groupsToDisassociate = [...initialIds].filter(
x => !updatedIds.includes(x)
);
if ( if (
typeof values.max_hosts !== 'number' || typeof values.max_hosts !== 'number' ||
values.max_hosts === 'undefined' values.max_hosts === 'undefined'
) { ) {
values.max_hosts = 0; values.max_hosts = 0;
} }
onSubmit(values, addedIds, removedIds);
};
handleSubmit(values, groupsToAssociate, groupsToDisassociate); useEffect(() => {
(async () => {
const { id } = organization;
if (!id) {
setHasContentLoading(false);
return;
}
setContentError(null);
setHasContentLoading(true);
try {
const {
data: { results = [] },
} = await OrganizationsAPI.readInstanceGroups(id);
setInitialInstanceGroups(results);
setInstanceGroups(results);
} catch (error) {
setContentError(error);
} finally {
setHasContentLoading(false);
}
})();
}, [organization]);
if (contentError) {
return <ContentError error={contentError} />;
} }
render() { if (hasContentLoading) {
const { organization, handleCancel, i18n, me } = this.props; return <ContentLoading />;
const { instanceGroups, formIsValid, error } = this.state; }
const defaultVenv = {
label: i18n._(t`Use Default Ansible Environment`),
value: '/venv/ansible/',
key: 'default',
};
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
name: organization.name, name: organization.name,
description: organization.description, description: organization.description,
custom_virtualenv: organization.custom_virtualenv || '', custom_virtualenv: organization.custom_virtualenv || '',
max_hosts: organization.max_hosts || '0', max_hosts: organization.max_hosts || '0',
}} }}
onSubmit={this.handleSubmit} onSubmit={handleSubmit}
render={formik => ( render={formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}> <Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow> <FormRow>
<FormField <FormField
id="org-name" id="org-name"
name="name" name="name"
type="text" type="text"
label={i18n._(t`Name`)} label={i18n._(t`Name`)}
validate={required(null, i18n)} validate={required(null, i18n)}
isRequired isRequired
/> />
<FormField <FormField
id="org-description" id="org-description"
name="description" name="description"
type="text" type="text"
label={i18n._(t`Description`)} label={i18n._(t`Description`)}
/> />
<FormField <FormField
id="org-max_hosts" id="org-max_hosts"
name="max_hosts" name="max_hosts"
type="number" type="number"
label={ label={
<Fragment> <>
{i18n._(t`Max Hosts`)}{' '} {i18n._(t`Max Hosts`)}{' '}
{ <Tooltip
<Tooltip position="right"
position="right" content={i18n._(
content={i18n._(t`The maximum number of hosts allowed t`The maximum number of hosts allowed to be managed by this organization.
to be managed by this organization. Value defaults to Value defaults to 0 which means no limit. Refer to the Ansible
0 which means no limit. Refer to the Ansible documentation for more details.`
documentation for more details.`)} )}
> >
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
} </>
</Fragment> }
} validate={minMaxValue(0, Number.MAX_SAFE_INTEGER, i18n)}
validate={minMaxValue(0, 2147483647, i18n)} me={me || {}}
me={me || {}} isDisabled={!me.is_superuser}
isDisabled={!me.is_superuser} />
/> {custom_virtualenvs && custom_virtualenvs.length > 1 && (
<Config> <Field
{({ custom_virtualenvs }) => name="custom_virtualenv"
custom_virtualenvs && render={({ field }) => (
custom_virtualenvs.length > 1 && ( <FormGroup
<Field fieldId="org-custom-virtualenv"
name="custom_virtualenv" label={i18n._(t`Ansible Environment`)}
render={({ field }) => ( >
<FormGroup <AnsibleSelect
fieldId="org-custom-virtualenv" id="org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)} data={[
> defaultVenv,
<AnsibleSelect ...custom_virtualenvs
id="org-custom-virtualenv" .filter(value => value !== defaultVenv.value)
data={[ .map(value => ({ value, label: value, key: value })),
defaultVenv, ]}
...custom_virtualenvs {...field}
.filter(datum => datum !== defaultVenv.value)
.map(datum => ({
label: datum,
value: datum,
key: datum,
})),
]}
{...field}
/>
</FormGroup>
)}
/> />
) </FormGroup>
} )}
</Config> />
</FormRow> )}
<InstanceGroupsLookup </FormRow>
value={instanceGroups} <InstanceGroupsLookup
onChange={this.handleInstanceGroupsChange} value={instanceGroups}
tooltip={i18n._( onChange={setInstanceGroups}
t`Select the Instance Groups for this Organization to run on.` tooltip={i18n._(
)} t`Select the Instance Groups for this Organization to run on.`
/> )}
<FormActionGroup />
onCancel={handleCancel} <FormActionGroup
onSubmit={formik.handleSubmit} onCancel={handleCancel}
submitDisabled={!formIsValid} onSubmit={formik.handleSubmit}
/> />
{error ? <div>error</div> : null} </Form>
</Form> )}
)} />
/> );
);
}
} }
FormField.propTypes = { FormField.propTypes = {
@@ -209,8 +177,8 @@ FormField.propTypes = {
OrganizationForm.propTypes = { OrganizationForm.propTypes = {
organization: PropTypes.shape(), organization: PropTypes.shape(),
handleSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
OrganizationForm.defaultProps = { OrganizationForm.defaultProps = {

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import OrganizationForm from './OrganizationForm'; import OrganizationForm from './OrganizationForm';
@@ -25,18 +24,20 @@ describe('<OrganizationForm />', () => {
instance_groups: '/api/v2/organizations/1/instance_groups', instance_groups: '/api/v2/organizations/1/instance_groups',
}, },
}; };
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('should request related instance groups from api', async () => { test('should request related instance groups from api', async () => {
let wrapper;
await act(async () => { await act(async () => {
mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={jest.fn()}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/>, />,
{ {
@@ -44,12 +45,11 @@ describe('<OrganizationForm />', () => {
} }
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1); expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
}); });
test('componentDidMount should set instanceGroups to state', async () => { test('componentDidMount should set instanceGroups to state', async () => {
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
OrganizationsAPI.readInstanceGroups.mockReturnValue({ OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: { data: {
results: mockInstanceGroups, results: mockInstanceGroups,
@@ -60,8 +60,8 @@ describe('<OrganizationForm />', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={jest.fn()}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/>, />,
{ {
@@ -70,84 +70,109 @@ describe('<OrganizationForm />', () => {
); );
}); });
await waitForElement(
wrapper,
'InstanceGroupsLookup',
el => el.length === 1
);
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalled(); expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalled();
expect(wrapper.find('OrganizationForm').state().instanceGroups).toEqual( expect(wrapper.find('InstanceGroupsLookup Chip span')).toHaveLength(2);
mockInstanceGroups
);
}); });
test('changing instance group successfully sets instanceGroups state', async () => { test('Instance group is rendered when added', async () => {
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: { results: [] },
});
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={jest.fn()}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
const lookup = await waitForElement(
const lookup = wrapper.find('InstanceGroupsLookup'); wrapper,
'InstanceGroupsLookup',
el => el.length === 1
);
expect(lookup.length).toBe(1); expect(lookup.length).toBe(1);
expect(lookup.find('Chip span')).toHaveLength(0);
lookup.prop('onChange')( await act(async () => {
[ lookup.prop('onChange')(
{ [
id: 1, {
name: 'foo', id: 1,
}, name: 'foo',
], },
'instanceGroups' ],
'instanceGroups'
);
});
const group = await waitForElement(
wrapper,
'InstanceGroupsLookup Chip span',
el => el.length === 1
); );
expect(wrapper.find('OrganizationForm').state().instanceGroups).toEqual([ expect(group.text()).toEqual('foo');
{
id: 1,
name: 'foo',
},
]);
}); });
test('changing inputs should update form values', async () => { test('changing inputs and saving triggers expected callback', async () => {
let wrapper; let wrapper;
const onSubmit = jest.fn();
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={onSubmit}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
const form = wrapper.find('Formik'); await act(async () => {
wrapper.find('input#org-name').simulate('change', { wrapper.find('input#org-name').simulate('change', {
target: { value: 'new foo', name: 'name' }, target: { value: 'new foo', name: 'name' },
});
wrapper.find('input#org-description').simulate('change', {
target: { value: 'new bar', name: 'description' },
});
wrapper.find('input#org-max_hosts').simulate('change', {
target: { value: 134, name: 'max_hosts' },
});
}); });
expect(form.state('values').name).toEqual('new foo'); await act(async () => {
wrapper.find('input#org-description').simulate('change', { wrapper.find('button[aria-label="Save"]').simulate('click');
target: { value: 'new bar', name: 'description' },
}); });
expect(form.state('values').description).toEqual('new bar'); expect(onSubmit).toHaveBeenCalledTimes(1);
wrapper.find('input#org-max_hosts').simulate('change', { expect(onSubmit.mock.calls[0][0]).toEqual({
target: { value: '134', name: 'max_hosts' }, name: 'new foo',
description: 'new bar',
custom_virtualenv: 'Fizz',
max_hosts: 134,
}); });
expect(form.state('values').max_hosts).toEqual('134');
}); });
test('AnsibleSelect component renders if there are virtual environments', async () => { test('AnsibleSelect component renders if there are virtual environments', async () => {
const config = { const config = {
custom_virtualenvs: ['foo', 'bar'], custom_virtualenvs: ['foo', 'bar'],
}; };
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups,
},
});
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={jest.fn()}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/>, />,
{ {
@@ -155,6 +180,7 @@ describe('<OrganizationForm />', () => {
} }
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('FormSelect')).toHaveLength(1); expect(wrapper.find('FormSelect')).toHaveLength(1);
expect(wrapper.find('FormSelectOption')).toHaveLength(3); expect(wrapper.find('FormSelectOption')).toHaveLength(3);
expect( expect(
@@ -165,36 +191,7 @@ describe('<OrganizationForm />', () => {
).toEqual('/venv/ansible/'); ).toEqual('/venv/ansible/');
}); });
test('calls handleSubmit when form submitted', async () => { test('onSubmit associates and disassociates instance groups', async () => {
const handleSubmit = jest.fn();
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<OrganizationForm
organization={mockData}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
});
expect(handleSubmit).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Save"]').simulate('click');
await sleep(1);
expect(handleSubmit).toHaveBeenCalledWith(
{
name: 'Foo',
description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz',
},
[],
[]
);
});
test('handleSubmit associates and disassociates instance groups', async () => {
const mockInstanceGroups = [{ name: 'One', id: 1 }, { name: 'Two', id: 2 }];
OrganizationsAPI.readInstanceGroups.mockReturnValue({ OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: { data: {
results: mockInstanceGroups, results: mockInstanceGroups,
@@ -206,7 +203,7 @@ describe('<OrganizationForm />', () => {
max_hosts: 1, max_hosts: 1,
custom_virtualenv: 'Fizz', custom_virtualenv: 'Fizz',
}; };
const handleSubmit = jest.fn(); const onSubmit = jest.fn();
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm); OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
OrganizationsAPI.associateInstanceGroup.mockResolvedValue('done'); OrganizationsAPI.associateInstanceGroup.mockResolvedValue('done');
OrganizationsAPI.disassociateInstanceGroup.mockResolvedValue('done'); OrganizationsAPI.disassociateInstanceGroup.mockResolvedValue('done');
@@ -215,8 +212,8 @@ describe('<OrganizationForm />', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={handleSubmit} onSubmit={onSubmit}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/>, />,
{ {
@@ -224,47 +221,21 @@ describe('<OrganizationForm />', () => {
} }
); );
}); });
wrapper.find('InstanceGroupsLookup').prop('onChange')( await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
[{ name: 'One', id: 1 }, { name: 'Three', id: 3 }],
'instanceGroups'
);
wrapper.find('button[aria-label="Save"]').simulate('click');
await sleep(0);
expect(handleSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]);
});
test('handleSubmit is called with max_hosts value if it is in range', async () => {
const handleSubmit = jest.fn();
let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper.find('InstanceGroupsLookup').prop('onChange')(
<OrganizationForm [{ name: 'One', id: 1 }, { name: 'Three', id: 3 }],
organization={mockData} 'instanceGroups'
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
); );
}); });
wrapper.find('button[aria-label="Save"]').simulate('click'); await act(async () => {
await sleep(0); wrapper.find('button[aria-label="Save"]').simulate('click');
expect(handleSubmit).toHaveBeenCalledWith( });
{ expect(onSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]);
name: 'Foo',
description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz',
},
[],
[]
);
}); });
test('handleSubmit does not get called if max_hosts value is out of range', async () => { test('onSubmit does not get called if max_hosts value is out of range', async () => {
const handleSubmit = jest.fn(); const onSubmit = jest.fn();
// mount with negative value // mount with negative value
let wrapper1; let wrapper1;
const mockDataNegative = JSON.parse(JSON.stringify(mockData)); const mockDataNegative = JSON.parse(JSON.stringify(mockData));
@@ -273,38 +244,41 @@ describe('<OrganizationForm />', () => {
wrapper1 = mountWithContexts( wrapper1 = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockDataNegative} organization={mockDataNegative}
handleSubmit={handleSubmit} onSubmit={onSubmit}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
wrapper1.find('button[aria-label="Save"]').simulate('click'); await waitForElement(wrapper1, 'ContentLoading', el => el.length === 0);
await sleep(0); await act(async () => {
expect(handleSubmit).not.toHaveBeenCalled(); wrapper1.find('button[aria-label="Save"]').simulate('click');
});
expect(onSubmit).not.toHaveBeenCalled();
// mount with out of range value // mount with out of range value
let wrapper2; let wrapper2;
const mockDataOoR = JSON.parse(JSON.stringify(mockData)); const mockDataOutOfRange = JSON.parse(JSON.stringify(mockData));
mockDataOoR.max_hosts = 999999999999; mockDataOutOfRange.max_hosts = 999999999999999999999;
await act(async () => { await act(async () => {
wrapper2 = mountWithContexts( wrapper2 = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockDataOoR} organization={mockDataOutOfRange}
handleSubmit={handleSubmit} onSubmit={onSubmit}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
wrapper2.find('button[aria-label="Save"]').simulate('click'); await waitForElement(wrapper2, 'ContentLoading', el => el.length === 0);
await sleep(0); await act(async () => {
expect(handleSubmit).not.toHaveBeenCalled(); wrapper2.find('button[aria-label="Save"]').simulate('click');
});
expect(onSubmit).not.toHaveBeenCalled();
}); });
test('handleSubmit is called and max_hosts value defaults to 0 if input is not a number', async () => { test('onSubmit is called and max_hosts value defaults to 0 if input is not a number', async () => {
const handleSubmit = jest.fn(); const onSubmit = jest.fn();
// mount with String value (default to zero) // mount with String value (default to zero)
const mockDataString = JSON.parse(JSON.stringify(mockData)); const mockDataString = JSON.parse(JSON.stringify(mockData));
mockDataString.max_hosts = 'Bee'; mockDataString.max_hosts = 'Bee';
@@ -313,15 +287,17 @@ describe('<OrganizationForm />', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockDataString} organization={mockDataString}
handleSubmit={handleSubmit} onSubmit={onSubmit}
handleCancel={jest.fn()} onCancel={jest.fn()}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
wrapper.find('button[aria-label="Save"]').simulate('click'); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
await sleep(0); await act(async () => {
expect(handleSubmit).toHaveBeenCalledWith( wrapper.find('button[aria-label="Save"]').simulate('click');
});
expect(onSubmit).toHaveBeenCalledWith(
{ {
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
@@ -333,22 +309,22 @@ describe('<OrganizationForm />', () => {
); );
}); });
test('calls "handleCancel" when Cancel button is clicked', async () => { test('calls "onCancel" when Cancel button is clicked', async () => {
const handleCancel = jest.fn(); const onCancel = jest.fn();
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} onSubmit={jest.fn()}
handleCancel={handleCancel} onCancel={onCancel}
me={meConfig.me} me={meConfig.me}
/> />
); );
}); });
expect(handleCancel).not.toHaveBeenCalled(); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(onCancel).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(handleCancel).toBeCalled(); expect(onCancel).toBeCalled();
}); });
}); });