From d2c63a9b36e973ba0b79636ebcb80346a1ff3584 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 4 Aug 2022 14:03:42 -0400 Subject: [PATCH] Adds tests --- .../Instances/InstanceAdd/InstanceAdd.js | 8 +- .../Instances/InstanceAdd/InstanceAdd.test.js | 53 ++++++++++ .../Instances/InstanceList/InstanceList.js | 22 ++++- .../InstanceList/InstanceList.test.js | 51 +++++++++- awx/ui/src/screens/Instances/Instances.js | 5 + .../screens/Instances/Shared/InstanceForm.js | 96 ++++++++++-------- .../Instances/Shared/InstanceForm.test.js | 98 +++++++++++++++++++ 7 files changed, 280 insertions(+), 53 deletions(-) create mode 100644 awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.test.js create mode 100644 awx/ui/src/screens/Instances/Shared/InstanceForm.test.js diff --git a/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.js b/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.js index 0fa6f1c630..1c0e86400d 100644 --- a/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.js +++ b/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.js @@ -8,17 +8,11 @@ function InstanceAdd() { const history = useHistory(); const [formError, setFormError] = useState(); const handleSubmit = async (values) => { - const { instanceGroups, executionEnvironment } = values; - values.execution_environment = executionEnvironment?.id; - try { const { data: { id }, - } = await InstancesAPI.create(); + } = await InstancesAPI.create(values); - for (const group of instanceGroups) { - await InstancesAPI.associateInstanceGroup(id, group.id); - } history.push(`/instances/${id}/details`); } catch (err) { setFormError(err); diff --git a/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.test.js b/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.test.js new file mode 100644 index 0000000000..e79b0471c8 --- /dev/null +++ b/awx/ui/src/screens/Instances/InstanceAdd/InstanceAdd.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { InstancesAPI } from 'api'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; + +import InstanceAdd from './InstanceAdd'; + +jest.mock('../../../api'); + +describe('', () => { + let wrapper; + let history; + + beforeEach(async () => { + history = createMemoryHistory({ initialEntries: ['/instances'] }); + InstancesAPI.create.mockResolvedValue({ data: { id: 13 } }); + await act(async () => { + wrapper = mountWithContexts(, { + context: { router: { history } }, + }); + }); + }); + + test('Initially renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + test('handleSubmit should call the api and redirect to details page', async () => { + await waitForElement(wrapper, 'isLoading', (el) => el.length === 0); + await act(async () => { + wrapper.find('InstanceForm').prop('handleSubmit')({ + name: 'new Foo', + node_type: 'hop', + }); + }); + expect(InstancesAPI.create).toHaveBeenCalledWith({ + name: 'new Foo', + node_type: 'hop', + }); + expect(history.location.pathname).toBe('/instances/13/details'); + }); + + test('handleCancel should return the user back to the instances list', async () => { + await waitForElement(wrapper, 'isLoading', (el) => el.length === 0); + await act(async () => { + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + }); + expect(history.location.pathname).toEqual('/instances'); + }); +}); diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceList.js b/awx/ui/src/screens/Instances/InstanceList/InstanceList.js index 782fcdd187..a50891bc68 100644 --- a/awx/ui/src/screens/Instances/InstanceList/InstanceList.js +++ b/awx/ui/src/screens/Instances/InstanceList/InstanceList.js @@ -10,12 +10,14 @@ import PaginatedTable, { HeaderRow, HeaderCell, getSearchableKeys, + ToolbarAddButton, } from 'components/PaginatedTable'; import AlertModal from 'components/AlertModal'; import ErrorDetail from 'components/ErrorDetail'; +import { useConfig } from 'contexts/Config'; import useRequest, { useDismissableError } from 'hooks/useRequest'; import useSelected from 'hooks/useSelected'; -import { InstancesAPI } from 'api'; +import { InstancesAPI, SettingsAPI } from 'api'; import { getQSConfig, parseQueryString } from 'util/qs'; import HealthCheckButton from 'components/HealthCheckButton'; import InstanceListItem from './InstanceListItem'; @@ -28,21 +30,24 @@ const QS_CONFIG = getQSConfig('instance', { function InstanceList() { const location = useLocation(); + const { me } = useConfig(); const { - result: { instances, count, relatedSearchableKeys, searchableKeys }, + result: { instances, count, relatedSearchableKeys, searchableKeys, isK8 }, error: contentError, isLoading, request: fetchInstances, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [response, responseActions] = await Promise.all([ + const [response, responseActions, sysSettings] = await Promise.all([ InstancesAPI.read(params), InstancesAPI.readOptions(), + SettingsAPI.readCategory('system'), ]); return { instances: response.data.results, + isK8: sysSettings.data.IS_K8S, count: response.data.count, actions: responseActions.data.actions, relatedSearchableKeys: ( @@ -57,6 +62,7 @@ function InstanceList() { actions: {}, relatedSearchableKeys: [], searchableKeys: [], + isK8: false, } ); @@ -89,6 +95,7 @@ function InstanceList() { const { expanded, isAllExpanded, handleExpand, expandAll } = useExpanded(instances); + return ( <> @@ -135,6 +142,15 @@ function InstanceList() { onExpandAll={expandAll} qsConfig={QS_CONFIG} additionalControls={[ + ...(isK8 && me.is_superuser + ? [ + , + ] + : []), ', () => { }, }); InstancesAPI.readOptions.mockResolvedValue(options); + SettingsAPI.readCategory.mockResolvedValue({ data: { IS_K8S: false } }); const history = createMemoryHistory({ initialEntries: ['/instances/1'], }); @@ -190,4 +191,52 @@ describe('', () => { wrapper.update(); expect(wrapper.find('AlertModal')).toHaveLength(1); }); + test('Should not show Add button', () => { + expect(wrapper.find('Button[ouiaId="instances-add-button"]')).toHaveLength( + 0 + ); + }); +}); + +describe('InstanceList should show Add button', () => { + let wrapper; + + const options = { data: { actions: { POST: true } } }; + + beforeEach(async () => { + InstancesAPI.read.mockResolvedValue({ + data: { + count: instances.length, + results: instances, + }, + }); + InstancesAPI.readOptions.mockResolvedValue(options); + SettingsAPI.readCategory.mockResolvedValue({ data: { IS_K8S: true } }); + const history = createMemoryHistory({ + initialEntries: ['/instances/1'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { history, route: { location: history.location } }, + }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Should show Add button', () => { + expect(wrapper.find('Button[ouiaId="instances-add-button"]')).toHaveLength( + 1 + ); + }); }); diff --git a/awx/ui/src/screens/Instances/Instances.js b/awx/ui/src/screens/Instances/Instances.js index a230fb9a67..ca42498e41 100644 --- a/awx/ui/src/screens/Instances/Instances.js +++ b/awx/ui/src/screens/Instances/Instances.js @@ -6,10 +6,12 @@ import ScreenHeader from 'components/ScreenHeader'; import PersistentFilters from 'components/PersistentFilters'; import { InstanceList } from './InstanceList'; import Instance from './Instance'; +import InstanceAdd from './InstanceAdd'; function Instances() { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ '/instances': t`Instances`, + '/instances/add': t`Create new Instance`, }); const buildBreadcrumbConfig = useCallback((instance) => { @@ -27,6 +29,9 @@ function Instances() { <> + + + diff --git a/awx/ui/src/screens/Instances/Shared/InstanceForm.js b/awx/ui/src/screens/Instances/Shared/InstanceForm.js index 6d706b39cf..4dff50df77 100644 --- a/awx/ui/src/screens/Instances/Shared/InstanceForm.js +++ b/awx/ui/src/screens/Instances/Shared/InstanceForm.js @@ -1,40 +1,36 @@ import React from 'react'; import { t } from '@lingui/macro'; import { Formik, useField } from 'formik'; -import { Form, FormGroup, CardBody } from '@patternfly/react-core'; +import { + Form, + FormGroup, + CardBody, + Switch, + Popover, +} from '@patternfly/react-core'; import { FormColumnLayout } from 'components/FormLayout'; import FormField, { FormSubmitError } from 'components/FormField'; import FormActionGroup from 'components/FormActionGroup'; import { required } from 'util/validators'; import AnsibleSelect from 'components/AnsibleSelect'; -import { - ExecutionEnvironmentLookup, - InstanceGroupsLookup, -} from 'components/Lookup'; // This is hard coded because the API does not have the ability to send us a list that contains // only the types of instances that can be added. Control and Hybrid instances cannot be added. const INSTANCE_TYPES = [ - { id: 2, name: t`Execution`, value: 'execution' }, - { id: 3, name: t`Hop`, value: 'hop' }, + { id: 'execution', name: t`Execution` }, + { id: 'hop', name: t`Hop` }, ]; function InstanceFormFields() { - const [instanceType, , instanceTypeHelpers] = useField('type'); - const [instanceGroupsField, , instanceGroupsHelpers] = - useField('instanceGroups'); - const [ - executionEnvironmentField, - executionEnvironmentMeta, - executionEnvironmentHelpers, - ] = useField('executionEnvironment'); + const [instanceType, , instanceTypeHelpers] = useField('node_type'); + const [enabled, , enabledHelpers] = useField('enabled'); return ( <> + + ({ key: type.id, - value: type.value, + value: type.id, label: type.name, - isDisabled: false, }))} value={instanceType.value} onChange={(e, opt) => { @@ -67,25 +76,27 @@ function InstanceFormFields() { }} /> - { - instanceGroupsHelpers.setValue(value); - }} - fieldName="instanceGroups" - /> - } - fieldName={executionEnvironmentField.name} - onBlur={() => executionEnvironmentHelpers.setTouched()} - value={executionEnvironmentField.value} - onChange={(value) => { - executionEnvironmentHelpers.setValue(value); - }} - /> + > + { + enabledHelpers.setValue(!enabled.value); + }} + ouiaId="enable-instance-switch" + /> + ); } @@ -100,11 +111,12 @@ function InstanceForm({ { handleSubmit(values); diff --git a/awx/ui/src/screens/Instances/Shared/InstanceForm.test.js b/awx/ui/src/screens/Instances/Shared/InstanceForm.test.js new file mode 100644 index 0000000000..8a3b9898bf --- /dev/null +++ b/awx/ui/src/screens/Instances/Shared/InstanceForm.test.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; + +import InstanceForm from './InstanceForm'; + +jest.mock('../../../api'); + +describe('', () => { + let wrapper; + let handleCancel; + let handleSubmit; + + beforeAll(async () => { + handleCancel = jest.fn(); + handleSubmit = jest.fn(); + + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('Initially renders successfully', () => { + expect(wrapper.length).toBe(1); + }); + + test('should display form fields properly', async () => { + await waitForElement(wrapper, 'InstanceForm', (el) => el.length > 0); + expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Instance State"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Listener Port"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Instance Type"]').length).toBe(1); + }); + + test('should update form values', async () => { + await act(async () => { + wrapper.find('input#name').simulate('change', { + target: { value: 'new Foo', name: 'hostname' }, + }); + }); + + wrapper.update(); + expect(wrapper.find('input#name').prop('value')).toEqual('new Foo'); + }); + + test('should call handleCancel when Cancel button is clicked', async () => { + expect(handleCancel).not.toHaveBeenCalled(); + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + wrapper.update(); + expect(handleCancel).toBeCalled(); + }); + + test('should call handleSubmit when Cancel button is clicked', async () => { + expect(handleSubmit).not.toHaveBeenCalled(); + await act(async () => { + wrapper.find('input#name').simulate('change', { + target: { value: 'new Foo', name: 'hostname' }, + }); + wrapper.find('input#instance-description').simulate('change', { + target: { value: 'This is a repeat song', name: 'description' }, + }); + wrapper.find('input#instance-port').simulate('change', { + target: { value: 'This is a repeat song', name: 'listener_port' }, + }); + }); + wrapper.update(); + expect( + wrapper.find('FormField[label="Instance State"]').prop('isDisabled') + ).toBe(true); + await act(async () => { + wrapper.find('button[aria-label="Save"]').invoke('onClick')(); + }); + + expect(handleSubmit).toBeCalledWith({ + description: 'This is a repeat song', + enabled: true, + hostname: 'new Foo', + listener_port: 'This is a repeat song', + node_state: 'Installed', + node_type: 'execution', + }); + }); +});