Merge pull request #4257 from marshmalien/skeleton-template-add-form

Skeleton template add form

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-07-09 17:06:46 +00:00 committed by GitHub
commit f22314caaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 16 deletions

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
CardBody,
CardHeader,
PageSection,
Tooltip,
} from '@patternfly/react-core';
import CardCloseButton from '@components/CardCloseButton';
import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI } from '@api';
function JobTemplateAdd({ history, i18n }) {
const [error, setError] = useState(null);
const handleSubmit = async values => {
setError(null);
try {
const { data } = await JobTemplatesAPI.create(values);
history.push(`/templates/${data.type}/${data.id}/details`);
} catch (err) {
setError(err);
}
};
const handleCancel = () => {
history.push(`/templates`);
};
return (
<PageSection>
<Card>
<CardHeader className="at-u-textRight">
<Tooltip content={i18n._(t`Close`)} position="top">
<CardCloseButton onClick={handleCancel} />
</Tooltip>
</CardHeader>
<CardBody>
<JobTemplateForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
/>
</CardBody>
{error ? <div>error</div> : ''}
</Card>
</PageSection>
);
}
export default withI18n()(withRouter(JobTemplateAdd));

View File

@ -0,0 +1,118 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import JobTemplateAdd from './JobTemplateAdd';
import { JobTemplatesAPI } from '../../../api';
jest.mock('@api');
describe('<JobTemplateAdd />', () => {
const defaultProps = {
description: '',
inventory: '',
job_type: 'run',
name: '',
playbook: '',
project: '',
};
afterEach(() => {
jest.clearAllMocks();
});
test('should render Job Template Form', () => {
const wrapper = mountWithContexts(<JobTemplateAdd />);
expect(wrapper.find('JobTemplateForm').length).toBe(1);
});
test('should render Job Template Form with default values', () => {
const wrapper = mountWithContexts(<JobTemplateAdd />);
expect(wrapper.find('input#template-description').text()).toBe(
defaultProps.description
);
expect(wrapper.find('input#template-inventory').text()).toBe(
defaultProps.inventory
);
expect(wrapper.find('AnsibleSelect[name="job_type"]').props().value).toBe(
defaultProps.job_type
);
expect(
wrapper
.find('AnsibleSelect[name="job_type"]')
.containsAllMatchingElements([
<option>Choose a job type</option>,
<option>Run</option>,
<option>Check</option>,
])
).toEqual(true);
expect(wrapper.find('input#template-name').text()).toBe(defaultProps.name);
expect(wrapper.find('input#template-playbook').text()).toBe(
defaultProps.playbook
);
expect(wrapper.find('input#template-project').text()).toBe(
defaultProps.project
);
});
test('handleSubmit should post to api', async done => {
const jobTemplateData = {
description: 'Baz',
inventory: 1,
job_type: 'run',
name: 'Foo',
playbook: 'Bar',
project: 2,
};
JobTemplatesAPI.create.mockResolvedValueOnce({
data: {
id: 1,
...jobTemplateData,
},
});
const wrapper = mountWithContexts(<JobTemplateAdd />);
await wrapper.find('JobTemplateForm').prop('handleSubmit')(jobTemplateData);
expect(JobTemplatesAPI.create).toHaveBeenCalledWith(jobTemplateData);
done();
});
test('should navigate to job template detail after form submission', async done => {
const history = {
push: jest.fn(),
};
const jobTemplateData = {
description: 'Baz',
inventory: 1,
job_type: 'run',
name: 'Foo',
playbook: 'Bar',
project: 2,
};
JobTemplatesAPI.create.mockResolvedValueOnce({
data: {
id: 1,
type: 'job_template',
...jobTemplateData,
},
});
const wrapper = mountWithContexts(<JobTemplateAdd />, {
context: { router: { history } },
});
await wrapper.find('JobTemplateForm').prop('handleSubmit')(jobTemplateData);
expect(history.push).toHaveBeenCalledWith(
'/templates/job_template/1/details'
);
done();
});
test('should navigate to templates list when cancel is clicked', () => {
const history = {
push: jest.fn(),
};
const wrapper = mountWithContexts(<JobTemplateAdd />, {
context: { router: { history } },
});
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/templates');
});
});

View File

@ -0,0 +1 @@
export { default } from './JobTemplateAdd';

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { withRouter, Redirect } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core';
import TemplateForm from '../shared/TemplateForm';
import JobTemplateForm from '../shared/JobTemplateForm';
import { JobTemplatesAPI } from '@api';
import { JobTemplate } from '@types';
@ -57,7 +57,7 @@ class JobTemplateEdit extends Component {
return (
<CardBody>
<TemplateForm
<JobTemplateForm
template={template}
handleCancel={this.handleCancel}
handleSubmit={this.handleSubmit}

View File

@ -34,7 +34,7 @@ describe('<JobTemplateEdit />', () => {
job_type: 'check',
};
wrapper.find('TemplateForm').prop('handleSubmit')(updatedTemplateData);
wrapper.find('JobTemplateForm').prop('handleSubmit')(updatedTemplateData);
expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, updatedTemplateData);
});

View File

@ -8,6 +8,7 @@ import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
import { TemplateList } from './TemplateList';
import Template from './Template';
import JobTemplateAdd from './JobTemplateAdd';
class Templates extends Component {
constructor(props) {
@ -17,6 +18,7 @@ class Templates extends Component {
this.state = {
breadcrumbConfig: {
'/templates': i18n._(t`Templates`),
'/templates/job_template/add': i18n._(t`Create New Job Template`),
},
};
}
@ -28,6 +30,7 @@ class Templates extends Component {
}
const breadcrumbConfig = {
'/templates': i18n._(t`Templates`),
'/templates/job_template/add': i18n._(t`Create New Job Template`),
[`/templates/${template.type}/${template.id}`]: `${template.name}`,
[`/templates/${template.type}/${template.id}/details`]: i18n._(
t`Details`
@ -46,6 +49,10 @@ class Templates extends Component {
<Fragment>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route
path={`${match.path}/:templateType/add`}
render={() => <JobTemplateAdd />}
/>
<Route
path={`${match.path}/:templateType/:id`}
render={({ match: newRouteMatch }) => (

View File

@ -18,13 +18,24 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;
`;
class TemplateForm extends Component {
class JobTemplateForm extends Component {
static propTypes = {
template: JobTemplate.isRequired,
template: JobTemplate,
handleCancel: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
static defaultProps = {
template: {
name: '',
description: '',
inventory: '',
job_type: 'run',
project: '',
playbook: '',
},
};
render() {
const { handleCancel, handleSubmit, i18n, template } = this.props;
@ -137,4 +148,4 @@ class TemplateForm extends Component {
}
}
export default withI18n()(withRouter(TemplateForm));
export default withI18n()(withRouter(JobTemplateForm));

View File

@ -1,11 +1,11 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import TemplateForm from './TemplateForm';
import JobTemplateForm from './JobTemplateForm';
jest.mock('@api');
describe('<TemplateForm />', () => {
describe('<JobTemplateForm />', () => {
const mockData = {
id: 1,
name: 'Foo',
@ -23,7 +23,7 @@ describe('<TemplateForm />', () => {
test('initially renders successfully', () => {
mountWithContexts(
<TemplateForm
<JobTemplateForm
template={mockData}
handleSubmit={jest.fn()}
handleCancel={jest.fn()}
@ -33,7 +33,7 @@ describe('<TemplateForm />', () => {
test('should update form values on input changes', () => {
const wrapper = mountWithContexts(
<TemplateForm
<JobTemplateForm
template={mockData}
handleSubmit={jest.fn()}
handleCancel={jest.fn()}
@ -70,7 +70,7 @@ describe('<TemplateForm />', () => {
test('should call handleSubmit when Submit button is clicked', async () => {
const handleSubmit = jest.fn();
const wrapper = mountWithContexts(
<TemplateForm
<JobTemplateForm
template={mockData}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
@ -86,7 +86,7 @@ describe('<TemplateForm />', () => {
test('should call handleCancel when Cancel button is clicked', () => {
const handleCancel = jest.fn();
const wrapper = mountWithContexts(
<TemplateForm
<JobTemplateForm
template={mockData}
handleSubmit={jest.fn()}
handleCancel={handleCancel}

View File

@ -1 +1 @@
export { default } from './TemplateForm';
export { default } from './JobTemplateForm';

View File

@ -1,4 +1,12 @@
import { shape, arrayOf, number, string, bool, oneOf } from 'prop-types';
import {
shape,
arrayOf,
number,
string,
bool,
oneOf,
oneOfType,
} from 'prop-types';
export const Role = shape({
descendent_roles: arrayOf(string),
@ -57,8 +65,8 @@ export const QSConfig = shape({
export const JobTemplate = shape({
name: string.isRequired,
description: string,
inventory: number.isRequired,
inventory: oneOfType([number, string]).isRequired,
job_type: oneOf(['run', 'check']),
playbook: string.isRequired,
project: number.isRequired,
project: oneOfType([number, string]).isRequired,
});