Adds WorkflowJobTemplate Add and Edit form and functionality.

This commit is contained in:
Alex Corey 2020-02-04 16:30:06 -05:00
parent c1bfcd73fb
commit 52a8935b20
13 changed files with 767 additions and 116 deletions

View File

@ -6,6 +6,24 @@ class WorkflowJobTemplates extends Base {
this.baseUrl = '/api/v2/workflow_job_templates/';
}
associateLabel(id, label, orgId) {
return this.http.post(`${this.baseUrl}${id}/labels/`, {
name: label.name,
organization: orgId,
});
}
createNode(id, data) {
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
}
disassociateLabel(id, label) {
return this.http.post(`${this.baseUrl}${id}/labels/`, {
id: label.id,
disassociate: true,
});
}
launch(id, data) {
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
}
@ -20,17 +38,16 @@ class WorkflowJobTemplates extends Base {
});
}
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, {
params
});
}
readWebhookKey(id) {
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
}
createNode(id, data) {
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
}
readScheduleList(id, params) {
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
}
}
export default WorkflowJobTemplates;

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
import { useParams, useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Card } from '@patternfly/react-core';
import { Card, PageSection } from '@patternfly/react-core';
import {
JobTemplatesAPI,
@ -141,7 +141,7 @@ function TemplateList({ i18n }) {
);
return (
<>
<PageSection>
<Card>
<PaginatedDataList
contentError={contentError}
@ -245,7 +245,7 @@ function TemplateList({ i18n }) {
{i18n._(t`Failed to delete one or more templates.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
</>
</PageSection>
);
}

View File

@ -2,7 +2,6 @@ import React, { Component } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Route, withRouter, Switch } from 'react-router-dom';
import { PageSection } from '@patternfly/react-core';
import { Config } from '@contexts/Config';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
@ -10,6 +9,7 @@ import { TemplateList } from './TemplateList';
import Template from './Template';
import WorkflowJobTemplate from './WorkflowJobTemplate';
import JobTemplateAdd from './JobTemplateAdd';
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
class Templates extends Component {
constructor(props) {
@ -20,6 +20,9 @@ class Templates extends Component {
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 Job Template`
),
},
};
}
@ -56,47 +59,49 @@ class Templates extends Component {
return (
<>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<PageSection>
<Switch>
<Route
path={`${match.path}/job_template/add`}
render={() => <JobTemplateAdd />}
/>
<Route
path={`${match.path}/job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<Template
history={history}
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route
path={`${match.path}/workflow_job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<WorkflowJobTemplate
history={history}
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route path={`${match.path}`} render={() => <TemplateList />} />
</Switch>
</PageSection>
<Switch>
<Route
path={`${match.path}/job_template/add`}
render={() => <JobTemplateAdd />}
/>
<Route
path={`${match.path}/workflow_job_template/add`}
render={() => <WorkflowJobTemplateAdd />}
/>
<Route
path={`${match.path}/job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<Template
history={history}
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route
path={`${match.path}/workflow_job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<WorkflowJobTemplate
history={history}
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route path={`${match.path}`} render={() => <TemplateList />} />
</Switch>
</>
);
}

View File

@ -13,6 +13,7 @@ import RoutedTabs from '@components/RoutedTabs';
import ScheduleList from '@components/ScheduleList';
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
import { Visualizer } from './WorkflowJobTemplateVisualizer';
class WorkflowJobTemplate extends Component {
@ -134,73 +135,99 @@ class WorkflowJobTemplate extends Component {
}
return (
<Card>
{cardHeader}
<Switch>
<Redirect
from="/templates/workflow_job_template/:id"
to="/templates/workflow_job_template/:id/details"
exact
/>
{template && (
<Route
key="wfjt-details"
path="/templates/workflow_job_template/:id/details"
render={() => (
<WorkflowJobTemplateDetail
template={template}
webHookKey={webHookKey}
/>
)}
<PageSection>
<Card className="awx-c-card">
{cardHeader}
<Switch>
<Redirect
from="/templates/workflow_job_template/:id"
to="/templates/workflow_job_template/:id/details"
exact
/>
)}
{template && (
<Route
key="wfjt-visualizer"
path="/templates/workflow_job_template/:id/visualizer"
render={() => (
<AppendBody>
<FullPage>
<Visualizer template={template} />
</FullPage>
</AppendBody>
)}
/>
)}
{template?.id && (
<Route path="/templates/workflow_job_template/:id/completed_jobs">
<JobList
defaultParams={{
workflow_job__workflow_job_template: template.id,
}}
{template && (
<Route
key="wfjt-details"
path="/templates/workflow_job_template/:id/details"
render={() => (
<WorkflowJobTemplateDetail
template={template}
webHookKey={webHookKey}
/>
)}
/>
</Route>
)}
{template && (
)}
{template && (
<Route
key="wfjt-edit"
path="/templates/workflow_job_template/:id/edit"
render={() => (
<WorkflowJobTemplateEdit
template={template}
webHookKey={webHookKey}
/>
)}
/>
)}
{template && (
<Route
key="wfjt-visualizer"
path="/templates/workflow_job_template/:id/visualizer"
render={() => (
<AppendBody>
<FullPage>
<Visualizer template={template} />
</FullPage>
</AppendBody>
)}
/>
)}
{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
defaultParams={{
workflow_job__workflow_job_template: template.id,
}}
/>
</Route>
)}
{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/workflow_job_template/${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/workflow_job_template/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</Card>
</Switch>
</Card>
</PageSection>
);
}
}

View File

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { WorkflowJobTemplatesAPI } from '@api';
import WorkflowJobTemplateForm from '../shared/WorkflowJobTemplateForm';
function WorkflowJobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState();
const history = useHistory();
const handleSubmit = async values => {
const { labels, organizationId, ...remainingValues } = values;
try {
const {
data: { id },
} = await WorkflowJobTemplatesAPI.create(remainingValues);
await Promise.all([submitLabels(id, labels, organizationId)]);
history.push(`/templates/workflow_job_template/${id}/details`);
} catch (err) {
setFormSubmitError(err);
}
};
const submitLabels = (templateId, labels = [], organizationId) => {
const associatePromises = labels.map(label =>
WorkflowJobTemplatesAPI.associateLabel(templateId, label, organizationId)
);
return Promise.all([...associatePromises]);
};
const handleCancel = () => {
history.push(`/templates`);
};
return (
<PageSection>
<Card>
<CardBody>
<WorkflowJobTemplateForm
handleCancel={handleCancel}
handleSubmit={handleSubmit}
/>
</CardBody>
{formSubmitError ? <div>formSubmitError</div> : ''}
</Card>
</PageSection>
);
}
export default WorkflowJobTemplateAdd;

View File

@ -0,0 +1,64 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history';
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
jest.mock('@api');
describe('<WorkflowJobTemplateAdd/>', () => {
let wrapper;
let history;
beforeEach(async () => {
WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } });
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,
},
},
},
}
);
});
});
afterEach(async () => {
wrapper.unmount();
});
test('initially renders successfully', async () => {
expect(wrapper.length).toBe(1);
});
test('calls workflowJobTemplatesAPI with correct information on submit', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
name: 'Alex',
labels: [{ name: 'Foo', id: 1 }, { name: 'bar', id: 2 }],
organizationId: 1,
});
});
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalledWith({
name: 'Alex',
});
expect(WorkflowJobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2);
});
test('handleCancel navigates the user to the /templates', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')();
});
expect(history.location.pathname).toBe('/templates');
});
});

View File

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

View File

@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { CardBody } from '@components/Card';
import { getAddedAndRemoved } from '@util/lists';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
import ContentLoading from '@components/ContentLoading';
import { WorkflowJobTemplateForm } from '../shared';
function WorkflowJobTemplateEdit({ template, hasContentLoading }) {
const [formSubmitError, setFormSubmitError] = useState();
const history = useHistory();
const handleSubmit = async values => {
const { labels, ...remainingValues } = values;
try {
await WorkflowJobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([submitLabels(labels, values.organization)]);
history.push(`/templates/workflow_job_template/${template.id}/details`);
} catch (err) {
setFormSubmitError(err);
}
};
const submitLabels = async (labels = [], orgId) => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results,
labels
);
if (!orgId && !template.organization) {
try {
const {
data: { results },
} = await OrganizationsAPI.read();
orgId = results[0].id;
} catch (err) {
setFormSubmitError(err);
}
}
const disassociationPromises = removed.map(label =>
WorkflowJobTemplatesAPI.disassociateLabel(template.id, label)
);
const associationPromises = added.map(label => {
return WorkflowJobTemplatesAPI.associateLabel(
template.id,
label,
orgId || template.organization
);
});
const results = await Promise.all([
...disassociationPromises,
...associationPromises,
]);
return results;
};
const handleCancel = () => {
history.push(`/templates`);
};
if (hasContentLoading) {
return <ContentLoading />;
}
return (
<>
<CardBody>
<WorkflowJobTemplateForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
template={template}
/>
</CardBody>
{formSubmitError ? <div>formSubmitError</div> : ''}
</>
);
}
export default WorkflowJobTemplateEdit;

View File

@ -0,0 +1,97 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { createMemoryHistory } from 'history';
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
jest.mock('@api');
const mockTemplate = {
id: 6,
name: 'Foo',
description: 'Foo description',
summary_fields: {
inventory: { id: 1, name: 'Inventory 1' },
organization: { id: 1, name: 'Organization 1' },
labels: {
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
},
},
scm_branch: 'devel',
limit: '5000',
variables: '---',
};
describe('<WorkflowJobTemplateEdit/>', () => {
let wrapper;
let history;
beforeEach(async () => {
await act(async () => {
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/6/edit'],
});
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/edit"
component={() => <WorkflowJobTemplateEdit template={mockTemplate} />}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 6 } },
},
},
},
}
);
});
});
afterEach(async () => {
wrapper.unmount();
});
test('renders successfully', () => {
expect(wrapper.find('WorkflowJobTemplateEdit').length).toBe(1);
});
test('api is called to properly to save the updated template.', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' },
organization: 2,
labels: [{ name: 'Label 2', id: 2 }, { name: 'Generated Label' }],
scm_branch: 'master',
limit: '5000',
variables: '---',
});
});
expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalledWith(6, {
id: 6,
name: 'Alex',
description: 'Apollo and Athena',
inventory: { id: 1, name: 'Inventory 1' },
organization: 2,
scm_branch: 'master',
limit: '5000',
variables: '---',
});
wrapper.update();
await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, {
name: 'Label 1',
id: 1,
});
wrapper.update();
await expect(WorkflowJobTemplatesAPI.associateLabel).toBeCalledTimes(1);
});
test('handleCancel navigates the user to the /templates', async () => {
await act(async () => {
await wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')();
});
expect(history.location.pathname).toBe('/templates');
});
});

View File

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

View File

@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import { Formik, Field } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
import { required } from '@util/validators';
import PropTypes from 'prop-types';
import FormRow from '@components/FormRow';
import FormField, { FieldTooltip } from '@components/FormField';
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { InventoryLookup } from '@components/Lookup';
import { VariablesField } from '@components/CodeMirrorInput';
import FormActionGroup from '@components/FormActionGroup';
import ContentError from '@components/ContentError';
import LabelSelect from './LabelSelect';
function WorkflowJobTemplateForm({
handleSubmit,
handleCancel,
i18n,
template = {},
}) {
const [contentError, setContentError] = useState(null);
const [inventory, setInventory] = useState(
template?.summary_fields?.inventory || null
);
const [organization, setOrganization] = useState(
template?.summary_fields?.organization || null
);
if (contentError) {
return <ContentError error={contentError} />;
}
return (
<Formik
onSubmit={handleSubmit}
initialValues={{
name: template.name || '',
description: template.description || '',
inventory: template.summary_fields?.inventory?.id || '',
organization: template.summary_fields?.organization?.id || '',
labels: template.summary_fields?.labels?.results || [],
variables: template.variables || '---',
limit: template.limit || '',
scmBranch: template.scm_branch || '',
ask_limit_on_launch: template.ask_limit_on_launch || false,
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
ask_variables_on_launch: template.ask_variables_on_launch || false,
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false,
}}
>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormRow>
<FormField
id="wfjt-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required(null, i18n)}
isRequired
/>
<FormField
id="wfjt-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<Field
id="wfjt-organization"
label={i18n._(t`Organization`)}
name="organization"
>
{({ form }) => (
<OrganizationLookup
helperTextInvalid={form.errors.organization}
isValid={
!form.touched.organization || !form.errors.organization
}
onBlur={() => form.setFieldTouched('organization')}
onChange={value => {
form.setFieldValue('organization', value?.id || null);
setOrganization(value);
}}
value={organization}
touched={form.touched.organization}
error={form.errors.organization}
/>
)}
</Field>
</FormRow>
<FormRow>
<Field name="inventory">
{({ form }) => (
<InventoryLookup
value={inventory}
onBlur={() => form.setFieldTouched('inventory')}
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
isValid={!form.touched.inventory || !form.errors.inventory}
helperTextInvalid={form.errors.inventory}
onChange={value => {
form.setFieldValue('inventory', value?.id || null);
form.setFieldValue(
'organizationId',
value?.organization || null
);
setInventory(value);
}}
touched={form.touched.inventory}
error={form.errors.inventory}
/>
)}
</Field>
<FormGroup
fieldId="wfjt-limit"
name="limit"
label={i18n._(t`Limit`)}
>
<FieldTooltip
content={i18n._(
t`Provide a host pattern to further constrain the list of hosts that will be managed or affected by the workflow. This limit is applied to all job template nodes that prompt for a limit. Refer to Ansible documentation for more information and examples on patterns.`
)}
/>
<FormField type="text" name="limit" id="wfjt-limit" label="" />
</FormGroup>
<FormField
type="text"
id="wfjt-scmBranch"
label={i18n._(t`SCM Branch`)}
name="scmBranch"
/>
</FormRow>
<FormRow>
<Field name="labels">
{({ field, form }) => (
<FormGroup label={i18n._(t`Labels`)} fieldId="wfjt-labels">
<FieldTooltip
content={i18n._(t`Optional labels that describe this job template,
such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`)}
/>
<LabelSelect
value={field.value}
onChange={labels => form.setFieldValue('labels', labels)}
onError={() => setContentError()}
/>
</FormGroup>
)}
</Field>
</FormRow>
<FormRow>
<VariablesField
id="host-variables"
name="variables"
label={i18n._(t`Variables`)}
/>
</FormRow>
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
/>
</Form>
)}
</Formik>
);
}
WorkflowJobTemplateForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired,
};
export default withI18n()(WorkflowJobTemplateForm);

View File

@ -0,0 +1,137 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import WorkflowJobTemplateForm from './WorkflowJobTemplateForm';
describe('<WorkflowJobTemplateForm/>', () => {
let wrapper;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
const mockTemplate = {
id: 6,
name: 'Foo',
description: 'Foo description',
summary_fields: {
inventory: { id: 1, name: 'Inventory 1' },
organization: { id: 1, name: 'Organization 1' },
labels: {
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
},
},
scm_branch: 'devel',
limit: '5000',
variables: '---',
};
beforeEach(() => {
act(() => {
wrapper = mountWithContexts(
<WorkflowJobTemplateForm
template={mockTemplate}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
/>
);
});
});
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('renders successfully', () => {
expect(wrapper.length).toBe(1);
});
test('all the fields render successfully', () => {
const fields = [
'name',
'description',
'organization',
'inventory',
'limit',
'scmBranch',
'labels',
'variables',
];
const assertField = (field, index) => {
expect(
wrapper
.find('Field')
.at(index)
.prop('name')
).toBe(`${field}`);
};
fields.map((field, index) => assertField(field, index));
});
test('changing inputs should update values', async () => {
const inputsToChange = [
{ element: 'wfjt-name', value: { value: 'new foo', name: 'name' } },
{
element: 'wfjt-description',
value: { value: 'new bar', name: 'description' },
},
{ element: 'wfjt-limit', value: { value: 1234567890, name: 'limit' } },
{
element: 'wfjt-scmBranch',
value: { value: 'new branch', name: 'scmBranch' },
},
];
const changeInputs = async ({ element, value }) => {
wrapper.find(`input#${element}`).simulate('change', {
target: value,
});
};
await act(async () => {
inputsToChange.map(input => changeInputs(input));
wrapper.find('LabelSelect').invoke('onChange')([
{ name: 'new label', id: 5 },
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
]);
wrapper.find('InventoryLookup').invoke('onChange')({
id: 3,
name: 'inventory',
});
wrapper.find('OrganizationLookup').invoke('onChange')({
id: 3,
name: 'organization',
});
});
wrapper.update();
const assertChanges = ({ element, value }) => {
expect(wrapper.find(`input#${element}`).prop('value')).toEqual(
typeof value.value === 'string' ? `${value.value}` : value.value
);
};
inputsToChange.map(input => assertChanges(input));
expect(wrapper.find('InventoryLookup').prop('value')).toEqual({
id: 3,
name: 'inventory',
});
expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
id: 3,
name: 'organization',
});
expect(wrapper.find('LabelSelect').prop('value')).toEqual([
{ name: 'new label', id: 5 },
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
]);
});
test('handleSubmit is called on submit button click', async () => {
await act(async () => {
await wrapper.find('button[aria-label="Save"]').simulate('click');
});
act(() => {
expect(handleSubmit).toBeCalled();
});
});
test('handleCancel is called on cancel button click', () => {
act(() => {
wrapper.find('button[aria-label="Cancel"]').simulate('click');
});
expect(handleCancel).toBeCalled();
});
});

View File

@ -1 +1,2 @@
export { default } from './JobTemplateForm';
export { default as JobTemplateForm } from './JobTemplateForm';
export { default as WorkflowJobTemplateForm } from './WorkflowJobTemplateForm';