Adds tests

This commit is contained in:
Alex Corey 2022-08-04 14:03:42 -04:00 committed by Jeff Bradberry
parent 5d3a19e542
commit d2c63a9b36
7 changed files with 280 additions and 53 deletions

View File

@ -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);

View File

@ -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('<InstanceAdd />', () => {
let wrapper;
let history;
beforeEach(async () => {
history = createMemoryHistory({ initialEntries: ['/instances'] });
InstancesAPI.create.mockResolvedValue({ data: { id: 13 } });
await act(async () => {
wrapper = mountWithContexts(<InstanceAdd />, {
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');
});
});

View File

@ -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 (
<>
<PageSection>
@ -135,6 +142,15 @@ function InstanceList() {
onExpandAll={expandAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(isK8 && me.is_superuser
? [
<ToolbarAddButton
ouiaId="instances-add-button"
key="add"
linkTo="/instances/add"
/>,
]
: []),
<HealthCheckButton
onClick={handleHealthCheck}
selectedItems={selected}

View File

@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { InstancesAPI } from 'api';
import { InstancesAPI, SettingsAPI } from 'api';
import {
mountWithContexts,
waitForElement,
@ -111,6 +111,7 @@ describe('<InstanceList/>', () => {
},
});
InstancesAPI.readOptions.mockResolvedValue(options);
SettingsAPI.readCategory.mockResolvedValue({ data: { IS_K8S: false } });
const history = createMemoryHistory({
initialEntries: ['/instances/1'],
});
@ -190,4 +191,52 @@ describe('<InstanceList/>', () => {
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(
<Route path="/instances/:id">
<InstanceList />
</Route>,
{
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
);
});
});

View File

@ -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() {
<>
<ScreenHeader streamType="instance" breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path="/instances/add">
<InstanceAdd />
</Route>
<Route path="/instances/:id">
<Instance setBreadcrumb={buildBreadcrumbConfig} />
</Route>

View File

@ -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 (
<>
<FormField
id="instance-name"
id="name"
label={t`Name`}
name="name"
name="hostname"
type="text"
validate={required(null)}
isRequired
@ -45,6 +41,20 @@ function InstanceFormFields() {
name="description"
type="text"
/>
<FormField
id="instance-state"
label={t`Instance State`}
name="node_state"
type="text"
isDisabled
/>
<FormField
id="instance-port"
label={t`Listener Port`}
name="listener_port"
type="number"
isRequired
/>
<FormGroup
fieldId="instanceType"
label={t`Instance Type`}
@ -57,9 +67,8 @@ function InstanceFormFields() {
id="instanceType-select"
data={INSTANCE_TYPES.map((type) => ({
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() {
}}
/>
</FormGroup>
<InstanceGroupsLookup
value={instanceGroupsField.value}
onChange={(value) => {
instanceGroupsHelpers.setValue(value);
}}
fieldName="instanceGroups"
/>
<ExecutionEnvironmentLookup
helperTextInvalid={executionEnvironmentMeta.error}
isValid={
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
<FormGroup
label={t`Enable Instance`}
aria-label={t`Enable Instance`}
labelIcon={
<Popover
content={t`If enabled, the instance will be ready to accept work.`}
/>
}
fieldName={executionEnvironmentField.name}
onBlur={() => executionEnvironmentHelpers.setTouched()}
value={executionEnvironmentField.value}
onChange={(value) => {
executionEnvironmentHelpers.setValue(value);
}}
/>
>
<Switch
css="display: inline-flex;"
id="enabled"
label={t`Enabled`}
labelOff={t`Disabled`}
isChecked={enabled.value}
onChange={() => {
enabledHelpers.setValue(!enabled.value);
}}
ouiaId="enable-instance-switch"
/>
</FormGroup>
</>
);
}
@ -100,11 +111,12 @@ function InstanceForm({
<CardBody>
<Formik
initialValues={{
name: instance.name || '',
description: instance.description || '',
type: instance.type || 'execution',
instanceGroups: instance.instance_groups || [],
executionEnvironment: instance.execution_environment || null,
hostname: '',
description: '',
node_type: 'execution',
node_state: 'Installed',
listener_port: 27199,
enabled: true,
}}
onSubmit={(values) => {
handleSubmit(values);

View File

@ -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('<InstanceForm />', () => {
let wrapper;
let handleCancel;
let handleSubmit;
beforeAll(async () => {
handleCancel = jest.fn();
handleSubmit = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<InstanceForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={null}
/>
);
});
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',
});
});
});