Adds error handling test to add and edit form. Updates Form component

This commit is contained in:
Alex Corey 2020-03-02 14:48:44 -05:00
parent acfa6d056f
commit b055aad641
12 changed files with 216 additions and 130 deletions

View File

@ -48,7 +48,7 @@ class WorkflowJobTemplates extends Base {
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, {
params
params,
});
}
}

View File

@ -100,6 +100,7 @@ function CredentialLookup({
},
]}
readOnly={!canDelete}
name="credential"
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
/>

View File

@ -221,36 +221,34 @@ class Template extends Component {
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template && (
{template && (
<Route
path="/templates/:templateType/:id/schedules"
render={() => (
<ScheduleList loadSchedules={this.loadSchedules} />
)}
/>
)}
<Route
path="/templates/:templateType/:id/schedules"
render={() => <ScheduleList loadSchedules={this.loadSchedules} />}
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</Card>
</Switch>
</Card>
</PageSection>
);
}
}

View File

@ -21,7 +21,7 @@ class Templates extends Component {
'/templates': i18n._(t`Templates`),
'/templates/job_template/add': i18n._(t`Create New Job Template`),
'/templates/workflow_job_template/add': i18n._(
t`Create New Workflow Job Template`
t`Create New Workflow Template`
),
},
};
@ -35,6 +35,9 @@ class Templates extends Component {
const breadcrumbConfig = {
'/templates': i18n._(t`Templates`),
'/templates/job_template/add': i18n._(t`Create New Job Template`),
'/templates/workflow_job_template/add': i18n._(
t`Create New Workflow Template`
),
[`/templates/${template.type}/${template.id}`]: `${template.name}`,
[`/templates/${template.type}/${template.id}/details`]: i18n._(
t`Details`

View File

@ -186,16 +186,6 @@ class WorkflowJobTemplate extends Component {
)}
/>
)}
{template?.id && (
<Route path="/templates/workflow_job_template/:id/completed_jobs">
<JobList
defaultParams={{
workflow_job__workflow_job_template: template.id,
}}
/>
</Route>
)}
{template?.id && (
<Route path="/templates/workflow_job_template/:id/completed_jobs">
<JobList

View File

@ -17,7 +17,7 @@ function WorkflowJobTemplateAdd() {
const {
data: { id },
} = await WorkflowJobTemplatesAPI.create(remainingValues);
await submitLabels(id, labels, organizationId);
await Promise.all(await submitLabels(id, labels, organizationId));
history.push(`/templates/workflow_job_template/${id}/details`);
} catch (err) {
setFormSubmitError(err);

View File

@ -1,43 +1,60 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI } from '@api';
import { WorkflowJobTemplatesAPI, OrganizationsAPI, LabelsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history';
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
jest.mock('@api');
jest.mock('@api/models/WorkflowJobTemplates');
jest.mock('@api/models/Organizations');
jest.mock('@api/models/Labels');
jest.mock('@api/models/Inventories');
describe('<WorkflowJobTemplateAdd/>', () => {
let wrapper;
let history;
beforeEach(async () => {
WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } });
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
LabelsAPI.read.mockResolvedValue({
data: {
results: [
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
{ name: 'Label 3', id: 3 },
],
},
});
await act(async () => {
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/add'],
});
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/add"
component={() => <WorkflowJobTemplateAdd />}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
await act(async () => {
wrapper = await mountWithContexts(
<Route
path="/templates/workflow_job_template/add"
component={() => <WorkflowJobTemplateAdd />}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
},
},
},
},
}
);
}
);
});
});
});
afterEach(async () => {
wrapper.unmount();
jest.clearAllMocks();
});
test('initially renders successfully', async () => {
@ -67,28 +84,39 @@ describe('<WorkflowJobTemplateAdd/>', () => {
test('throwing error renders FormSubmitError component', async () => {
const error = new Error('oops');
WorkflowJobTemplatesAPI.create.mockImplementation(() =>
Promise.reject(error)
);
WorkflowJobTemplatesAPI.create.mockRejectedValue(error);
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').prop('handleSubmit')({
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' },
organization: 2,
scm_branch: 'master',
limit: '5000',
variables: '---',
wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
name: 'Foo',
});
});
expect(wrapper.find('ContentError').length).toBe(0);
expect(wrapper.length).toBe(1);
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalled();
wrapper.update();
expect(WorkflowJobTemplatesAPI.create).toBeCalled();
expect(wrapper.find('ContentError').length).toBe(1);
expect(wrapper.find('WorkflowJobTemplateForm').prop('submitError')).toEqual(
error
);
});
test('throwing error prevents navigation away from form', async () => {
OrganizationsAPI.read.mockRejectedValue(
new Error({
response: {
config: {
method: 'get',
},
data: 'An error occurred',
},
})
);
WorkflowJobTemplatesAPI.update.mockResolvedValue();
await act(async () => {
await wrapper.find('Button[aria-label="Save"]').simulate('click');
});
expect(wrapper.find('WorkflowJobTemplateForm').length).toBe(1);
expect(OrganizationsAPI.read).toBeCalled();
expect(history.location.pathname).toBe(
'/templates/workflow_job_template/add'
);
});
});

View File

@ -58,7 +58,7 @@ function WorkflowJobTemplateDetail({ template, i18n, webhook_key }) {
)}
{template.webhook_service && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`- Webhooks`)}
{i18n._(t`- Enable Webhook`)}
</TextListItem>
)}
</TextList>

View File

@ -13,7 +13,9 @@ function WorkflowJobTemplateEdit({ template, webhook_key }) {
const handleSubmit = async values => {
const { labels, ...remainingValues } = values;
try {
await submitLabels(labels, values.organization, template.organization);
await Promise.all(
await submitLabels(labels, values.organization, template.organization)
);
await WorkflowJobTemplatesAPI.update(template.id, remainingValues);
history.push(`/templates/workflow_job_template/${template.id}/details`);
} catch (err) {

View File

@ -1,13 +1,15 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
import { WorkflowJobTemplatesAPI, OrganizationsAPI, LabelsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history';
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
jest.mock('@api/models/WorkflowJobTemplates');
jest.mock('@api/models/Labels');
jest.mock('@api/models/Organizations');
jest.mock('@api/models/Inventories');
const mockTemplate = {
id: 6,
@ -27,8 +29,14 @@ const mockTemplate = {
describe('<WorkflowJobTemplateEdit/>', () => {
let wrapper;
let history;
beforeEach(async () => {
LabelsAPI.read.mockResolvedValue({
data: {
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
},
});
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
await act(async () => {
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],
@ -93,7 +101,6 @@ describe('<WorkflowJobTemplateEdit/>', () => {
id: 1,
});
wrapper.update();
await expect(WorkflowJobTemplatesAPI.associateLabel).toBeCalledTimes(1);
});
@ -108,7 +115,6 @@ describe('<WorkflowJobTemplateEdit/>', () => {
test('throwing error renders FormSubmitError component', async () => {
const error = new Error('oops');
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
WorkflowJobTemplatesAPI.update.mockRejectedValue(error);
await act(async () => {
wrapper.find('Button[aria-label="Save"]').simulate('click');
@ -120,13 +126,26 @@ describe('<WorkflowJobTemplateEdit/>', () => {
);
});
test('throwing error prevents form submission', () => {
OrganizationsAPI.read.mockRejectedValue(new Error('An error occurred'));
WorkflowJobTemplatesAPI.update.mockResolvedValue();
test('throwing error prevents form submission', async () => {
const templateWithoutOrg = {
id: 6,
name: 'Foo',
description: 'Foo description',
summary_fields: {
inventory: { id: 1, name: 'Inventory 1' },
labels: {
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
},
},
scm_branch: 'devel',
limit: '5000',
variables: '---',
};
act(() => {
wrapper = mountWithContexts(
<WorkflowJobTemplateEdit template={mockTemplate} />,
let newWrapper;
await act(async () => {
newWrapper = await mountWithContexts(
<WorkflowJobTemplateEdit template={templateWithoutOrg} />,
{
context: {
router: {
@ -136,9 +155,22 @@ describe('<WorkflowJobTemplateEdit/>', () => {
}
);
});
wrapper.find('Button[aria-label="Save"]').simulate('click');
OrganizationsAPI.read.mockRejectedValue(
new Error({
response: {
config: {
method: 'get',
},
data: 'An error occurred',
},
})
);
WorkflowJobTemplatesAPI.update.mockResolvedValue();
expect(wrapper.find('WorkflowJobTemplateForm').length).toBe(1);
await act(async () => {
await newWrapper.find('Button[aria-label="Save"]').simulate('click');
});
expect(newWrapper.find('WorkflowJobTemplateForm').length).toBe(1);
expect(OrganizationsAPI.read).toBeCalled();
expect(WorkflowJobTemplatesAPI.update).not.toBeCalled();
expect(history.location.pathname).toBe(

View File

@ -49,12 +49,15 @@ function WorkflowJobTemplateForm({
submitError,
}) {
const urlOrigin = window.location.origin;
const { id } = useParams();
const wfjtAddMatch = useRouteMatch('/templates/workflow_job_template/add');
const [hasContentError, setContentError] = useState(null);
const [webhook_url, setWebhookUrl] = useState(
template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: ''
);
const [inventory, setInventory] = useState(
template?.summary_fields?.inventory || null
);
@ -141,8 +144,11 @@ function WorkflowJobTemplateForm({
);
setWebhookCredential(initialWebhookCredential);
form.setFieldValue('webhook_url', form.initialValues.webhook_url);
setWebhookUrl(
template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: ''
);
form.setFieldValue('webhook_service', form.initialValues.webhook_service);
setWebHookService(form.initialValues.webhook_service);
@ -151,8 +157,7 @@ function WorkflowJobTemplateForm({
form.setFieldValue('webhook_credential', null);
setWebhookCredential(null);
form.setFieldValue(
'webhook_url',
setWebhookUrl(
`${urlOrigin}/api/v2/workflow_job_templates/${template.id}/${webhookServiceValue}/`
);
@ -171,7 +176,7 @@ function WorkflowJobTemplateForm({
initialWebhookKey = webhookKey;
form.setFieldValue('webhook_credential', null);
form.setFieldValue('webhook_service', '');
form.setFieldValue('webhook_url', '');
setWebhookUrl('');
setWebHookService('');
setWebHookKey('');
} else {
@ -203,11 +208,8 @@ function WorkflowJobTemplateForm({
labels: template.summary_fields?.labels?.results || [],
extra_vars: template.variables || '---',
limit: template.limit || '',
scmBranch: template.scm_branch || '',
scm_branch: template.scm_branch || '',
allow_simultaneous: template.allow_simultaneous || false,
webhook_url:
template?.related?.webhook_receiver &&
`${urlOrigin}${template?.related?.webhook_receiver}`,
webhook_credential:
template?.summary_fields?.webhook_credential?.id || null,
webhook_service: template.webhook_service || '',
@ -290,8 +292,8 @@ function WorkflowJobTemplateForm({
tooltip={i18n._(
t`Select a branch for the workflow. This branch is applied to all job template nodes that prompt for a branch.`
)}
id="wfjt-scmBranch"
name="scmBranch"
id="wfjt-scm_branch"
name="scm_branch"
/>
</FormColumnLayout>
<FormFullWidthLayout>
@ -333,17 +335,13 @@ function WorkflowJobTemplateForm({
isInline
label={i18n._(t`Options`)}
>
<Field
id="wfjt-webhooks"
name="hasWebhooks"
label={i18n._(t`Webhooks`)}
>
<Field id="wfjt-webhooks" name="hasWebhooks">
{({ form }) => (
<Checkbox
aria-label={i18n._(t`Webhooks`)}
aria-label={i18n._(t`Enable Webhook`)}
label={
<span>
{i18n._(t`Webhooks`)}
{i18n._(t`Enable Webhook`)}
&nbsp;
<FieldTooltip
content={i18n._(
@ -408,16 +406,24 @@ function WorkflowJobTemplateForm({
</Field>
{!wfjtAddMatch && (
<>
<FormField
<FormGroup
type="text"
fieldId="wfjt-webhookURL"
label={i18n._(t`Webhook URL`)}
id="wfjt-webhook-url"
name="webhook_url"
tooltip={i18n._(
t`Webhook services can launch jobs with this job template by making a POST request to this URL.`
)}
isReadOnly
/>
>
<FieldTooltip
content={i18n._(
t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.`
)}
/>
<TextInput
aria-label={i18n._(t`Webhook URL`)}
value={webhook_url}
isReadOnly
/>
</FormGroup>
<Field>
{({ form }) => (
<FormGroup

View File

@ -6,13 +6,17 @@ import { sleep } from '@testUtils/testUtils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import WorkflowJobTemplateForm from './WorkflowJobTemplateForm';
import { WorkflowJobTemplatesAPI } from '@api';
import {
WorkflowJobTemplatesAPI,
LabelsAPI,
OrganizationsAPI,
InventoriesAPI,
} from '@api';
jest.mock('@api/models/WorkflowJobTemplates');
WorkflowJobTemplatesAPI.updateWebhookKey.mockResolvedValue({
data: { webhook_key: 'sdafdghjkl2345678ionbvcxz' },
});
jest.mock('@api/models/Labels');
jest.mock('@api/models/Organizations');
jest.mock('@api/models/Inventories');
describe('<WorkflowJobTemplateForm/>', () => {
let wrapper;
@ -38,12 +42,31 @@ describe('<WorkflowJobTemplateForm/>', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.updateWebhookKey.mockResolvedValue({
data: { webhook_key: 'sdafdghjkl2345678ionbvcxz' },
});
LabelsAPI.read.mockResolvedValue({
data: {
results: [
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
{ name: 'Label 3', id: 3 },
],
},
});
OrganizationsAPI.read.mockResolvedValue({
results: [{ id: 1 }, { id: 2 }],
});
InventoriesAPI.read.mockResolvedValue({
results: [{ id: 1, name: 'Foo' }, { id: 2, name: 'Bar' }],
});
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],
});
act(() => {
wrapper = mountWithContexts(
await act(async () => {
wrapper = await mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/edit"
component={() => (
@ -86,7 +109,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
'Field[name="organization"]',
'Field[name="inventory"]',
'FormField[name="limit"]',
'FormField[name="scmBranch"]',
'FormField[name="scm_branch"]',
'Field[name="labels"]',
'VariablesField',
];
@ -108,8 +131,8 @@ describe('<WorkflowJobTemplateForm/>', () => {
},
{ element: 'wfjt-limit', value: { value: 1234567890, name: 'limit' } },
{
element: 'wfjt-scmBranch',
value: { value: 'new branch', name: 'scmBranch' },
element: 'wfjt-scm_branch',
value: { value: 'new branch', name: 'scm_branch' },
},
];
const changeInputs = async ({ element, value }) => {
@ -122,7 +145,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
inputsToChange.map(input => changeInputs(input));
wrapper.find('LabelSelect').invoke('onChange')([
{ name: 'new label', id: 5 },
{ name: 'Label 3', id: 3 },
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
]);
@ -148,13 +171,16 @@ describe('<WorkflowJobTemplateForm/>', () => {
test('webhooks and enable concurrent jobs functions properly', async () => {
act(() => {
wrapper.find('Checkbox[aria-label="Webhooks"]').invoke('onChange')(true, {
currentTarget: { value: true, type: 'change', checked: true },
});
wrapper.find('Checkbox[aria-label="Enable Webhook"]').invoke('onChange')(
true,
{
currentTarget: { value: true, type: 'change', checked: true },
}
);
});
wrapper.update();
expect(
wrapper.find('Checkbox[aria-label="Webhooks"]').prop('isChecked')
wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked')
).toBe(true);
expect(
@ -171,7 +197,7 @@ describe('<WorkflowJobTemplateForm/>', () => {
);
expect(WorkflowJobTemplatesAPI.updateWebhookKey).toBeCalledWith('6');
expect(
wrapper.find('TextInputBase[name="webhook_url"]').prop('value')
wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value')
).toContain('/api/v2/workflow_job_templates/57/gitlab/');
wrapper.update();