Adds test coverage for node modal components

This commit is contained in:
mabashian 2020-02-06 10:29:23 -05:00
parent 887469d73e
commit 50c74a2ec8
21 changed files with 862 additions and 58 deletions

View File

@ -1,7 +1,5 @@
import React from 'react';
import {
WorkflowDispatchContext,
} from '@contexts/Workflow';
import { WorkflowDispatchContext } from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import DeleteAllNodesModal from './DeleteAllNodesModal';

View File

@ -1,12 +1,15 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { WorkflowDispatchContext, WorkflowStateContext } from '@contexts/Workflow';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import LinkAddModal from './LinkAddModal';
const dispatch = jest.fn();
const workflowContext = {
linkToEdit: null
linkToEdit: null,
};
describe('LinkAddModal', () => {
@ -17,8 +20,11 @@ describe('LinkAddModal', () => {
<LinkAddModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
);
wrapper.find('button#link-confirm').simulate('click');
expect(dispatch).toHaveBeenCalledWith({ type: 'CREATE_LINK', linkType: 'success' });
expect(dispatch).toHaveBeenCalledWith({
type: 'CREATE_LINK',
linkType: 'success',
});
});
});

View File

@ -28,7 +28,7 @@ function LinkDeleteModal({ i18n }) {
{i18n._(t`Remove`)}
</Button>,
<Button
id="cancel-link-removal"
id="cancel-link-removal"
aria-label={i18n._(t`Cancel link removal`)}
key="cancel"
onClick={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}

View File

@ -1,7 +1,7 @@
import React from 'react';
import {
WorkflowDispatchContext,
WorkflowStateContext
WorkflowStateContext,
} from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import LinkDeleteModal from './LinkDeleteModal';
@ -12,13 +12,13 @@ const dispatch = jest.fn();
const workflowContext = {
linkToDelete: {
source: {
id: 2
id: 2,
},
target: {
id: 3
id: 3,
},
linkType: 'always'
}
linkType: 'always',
},
};
describe('LinkDeleteModal', () => {
@ -54,7 +54,8 @@ describe('LinkDeleteModal', () => {
test('Close button dispatches as expected', () => {
wrapper.find('TimesIcon').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'SET_LINK_TO_DELETE', value: null
type: 'SET_LINK_TO_DELETE',
value: null,
});
});
});

View File

@ -1,6 +1,9 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { WorkflowDispatchContext, WorkflowStateContext } from '@contexts/Workflow';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import LinkEditModal from './LinkEditModal';
const dispatch = jest.fn();
@ -8,13 +11,13 @@ const dispatch = jest.fn();
const workflowContext = {
linkToEdit: {
source: {
id: 2
id: 2,
},
target: {
id: 3
id: 3,
},
linkType: 'always'
}
linkType: 'always',
},
};
describe('LinkEditModal', () => {
@ -25,8 +28,11 @@ describe('LinkEditModal', () => {
<LinkEditModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
);
wrapper.find('button#link-confirm').simulate('click');
expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_LINK', linkType: 'always' });
expect(dispatch).toHaveBeenCalledWith({
type: 'UPDATE_LINK',
linkType: 'always',
});
});
});

View File

@ -0,0 +1,85 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import LinkModal from './LinkModal';
const dispatch = jest.fn();
const onConfirm = jest.fn();
let wrapper;
describe('LinkModal', () => {
describe('Adding new link', () => {
beforeAll(() => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
linkToEdit: null,
}}
>
<LinkModal header="TEST" onConfirm={onConfirm} />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
afterAll(() => {
wrapper.unmount();
});
test('Dropdown defaults to success when adding new link', () => {
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('success');
});
test('Cancel button dispatches as expected', () => {
wrapper.find('button#link-cancel').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'CANCEL_LINK_MODAL',
});
});
test('Close button dispatches as expected', () => {
wrapper.find('TimesIcon').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'CANCEL_LINK_MODAL',
});
});
test('Confirm button passes callback correct link type after changing dropdown', () => {
act(() => {
wrapper.find('AnsibleSelect').prop('onChange')(null, 'always');
});
wrapper.find('button#link-confirm').simulate('click');
expect(onConfirm).toHaveBeenCalledWith('always');
});
});
describe('Editing existing link', () => {
test('Dropdown defaults to existing link type when editing link', () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
linkToEdit: {
source: {
id: 2,
},
target: {
id: 3,
},
linkType: 'failure',
},
}}
>
<LinkModal header="TEST" onConfirm={onConfirm} />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('failure');
wrapper.unmount();
});
});
});

View File

@ -11,13 +11,12 @@ function NodeAddModal({ i18n }) {
const dispatch = useContext(WorkflowDispatchContext);
const { addNodeSource } = useContext(WorkflowStateContext);
const addNode = (linkType, resource, nodeType) => {
const addNode = (resource, linkType) => {
dispatch({
type: 'CREATE_NODE',
node: {
linkType,
nodeResource: resource,
nodeType,
},
});
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import NodeAddModal from './NodeAddModal';
const dispatch = jest.fn();
const nodeResource = {
id: 448,
type: 'job_template',
name: 'Test JT',
};
const workflowContext = {
addNodeSource: 2,
};
describe('NodeAddModal', () => {
test('Node modal confirmation dispatches as expected', () => {
const wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={workflowContext}>
<NodeAddModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
act(() => {
wrapper.find('NodeModal').prop('onSave')(nodeResource, 'success');
});
expect(dispatch).toHaveBeenCalledWith({
type: 'CREATE_NODE',
node: {
linkType: 'success',
nodeResource,
},
});
});
});

View File

@ -19,6 +19,7 @@ function NodeDeleteModal({ i18n }) {
onClose={() => dispatch({ type: 'SET_NODE_TO_DELETE', value: null })}
actions={[
<Button
id="confirm-node-removal"
key="remove"
variant="danger"
aria-label={i18n._(t`Confirm node removal`)}
@ -27,6 +28,7 @@ function NodeDeleteModal({ i18n }) {
{i18n._(t`Remove`)}
</Button>,
<Button
id="cancel-node-removal"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Cancel node removal`)}

View File

@ -0,0 +1,85 @@
import React from 'react';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import NodeDeleteModal from './NodeDeleteModal';
let wrapper;
const dispatch = jest.fn();
describe('NodeDeleteModal', () => {
describe('Node with unified job template', () => {
beforeAll(() => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
nodeToDelete: {
id: 2,
unifiedJobTemplate: {
id: 4000,
name: 'Test JT',
type: 'job_template',
},
},
}}
>
<NodeDeleteModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
afterAll(() => {
wrapper.unmount();
});
test('Mounts successfully', () => {
expect(wrapper.length).toBe(1);
});
test('Confirm button dispatches as expected', () => {
wrapper.find('button#confirm-node-removal').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'DELETE_NODE',
});
});
test('Cancel button dispatches as expected', () => {
wrapper.find('button#cancel-node-removal').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'SET_NODE_TO_DELETE',
value: null,
});
});
test('Close button dispatches as expected', () => {
wrapper.find('TimesIcon').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'SET_NODE_TO_DELETE',
value: null,
});
});
});
describe('Node without unified job template', () => {
test('Mounts successfully', () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
nodeToDelete: {
id: 2,
},
}}
>
<NodeDeleteModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
expect(wrapper.length).toBe(1);
wrapper.unmount();
});
});
});

View File

@ -7,13 +7,11 @@ import NodeModal from './NodeModal';
function NodeEditModal({ i18n }) {
const dispatch = useContext(WorkflowDispatchContext);
const updateNode = (linkType, resource, nodeType) => {
const updateNode = resource => {
dispatch({
type: 'UPDATE_NODE',
node: {
linkType,
nodeResource: resource,
nodeType,
},
});
};

View File

@ -0,0 +1,48 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import NodeEditModal from './NodeEditModal';
const dispatch = jest.fn();
const nodeResource = {
id: 448,
type: 'job_template',
name: 'Test JT',
};
const workflowContext = {
nodeToEdit: {
id: 4,
unifiedJobTemplate: {
id: 30,
name: 'Foo JT',
type: 'job_template',
},
},
};
describe('NodeEditModal', () => {
test('Node modal confirmation dispatches as expected', () => {
const wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={workflowContext}>
<NodeEditModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
act(() => {
wrapper.find('NodeModal').prop('onSave')(nodeResource);
});
expect(dispatch).toHaveBeenCalledWith({
type: 'UPDATE_NODE',
node: {
nodeResource,
},
});
});
});

View File

@ -104,7 +104,7 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
}
: nodeResource;
onSave(linkType, resource, nodeType);
onSave(resource, askLinkType ? linkType : null);
};
const handleCancel = () => {
@ -177,11 +177,15 @@ function NodeModal({ askLinkType, i18n, onSave, title }) {
}
/>
{activeStep && activeStep.id !== 1 && (
<Button variant="secondary" onClick={onBack}>
<Button id="back-node-modal" variant="secondary" onClick={onBack}>
{i18n._(t`Back`)}
</Button>
)}
<Button variant="link" onClick={handleCancel}>
<Button
id="cancel-node-modal"
variant="link"
onClick={handleCancel}
>
{i18n._(t`Cancel`)}
</Button>
</>

View File

@ -0,0 +1,414 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import {
WorkflowDispatchContext,
WorkflowStateContext,
} from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import {
InventorySourcesAPI,
JobTemplatesAPI,
ProjectsAPI,
WorkflowJobTemplatesAPI,
} from '@api';
import NodeModal from './NodeModal';
jest.mock('@api/models/InventorySources');
jest.mock('@api/models/JobTemplates');
jest.mock('@api/models/Projects');
jest.mock('@api/models/WorkflowJobTemplates');
let wrapper;
const dispatch = jest.fn();
const onSave = jest.fn();
describe('NodeModal', () => {
beforeAll(() => {
JobTemplatesAPI.read.mockResolvedValue({
data: {
count: 1,
results: [
{
id: 1,
name: 'Test Job Template',
type: 'job_template',
url: '/api/v2/job_templates/1',
},
],
},
});
ProjectsAPI.read.mockResolvedValue({
data: {
count: 1,
results: [
{
id: 1,
name: 'Test Project',
type: 'project',
url: '/api/v2/projects/1',
},
],
},
});
InventorySourcesAPI.read.mockResolvedValue({
data: {
count: 1,
results: [
{
id: 1,
name: 'Test Inventory Source',
type: 'inventory_source',
url: '/api/v2/inventory_sources/1',
},
],
},
});
WorkflowJobTemplatesAPI.read.mockResolvedValue({
data: {
count: 1,
results: [
{
id: 1,
name: 'Test Workflow Job Template',
type: 'workflow_job_template',
url: '/api/v2/workflow_job_templates/1',
},
],
},
});
});
afterAll(() => {
jest.clearAllMocks();
});
describe('Add new node', () => {
beforeEach(async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
nodeToEdit: null,
}}
>
<NodeModal askLinkType onSave={onSave} title="Add Node" />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
});
afterAll(() => {
wrapper.unmount();
});
test('Can successfully create a new job template node', async () => {
act(() => {
wrapper.find('#link-type-always').simulate('click');
});
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
wrapper.update();
wrapper.find('DataListRadio').simulate('click');
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
id: 1,
name: 'Test Job Template',
type: 'job_template',
url: '/api/v2/job_templates/1',
},
'always'
);
});
test('Can successfully create a new project sync node', async () => {
act(() => {
wrapper.find('#link-type-failure').simulate('click');
});
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
wrapper.update();
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(null, 'project_sync');
});
wrapper.update();
wrapper.find('DataListRadio').simulate('click');
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
id: 1,
name: 'Test Project',
type: 'project',
url: '/api/v2/projects/1',
},
'failure'
);
});
test('Can successfully create a new inventory source sync node', async () => {
act(() => {
wrapper.find('#link-type-failure').simulate('click');
});
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
wrapper.update();
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(
null,
'inventory_source_sync'
);
});
wrapper.update();
wrapper.find('DataListRadio').simulate('click');
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
id: 1,
name: 'Test Inventory Source',
type: 'inventory_source',
url: '/api/v2/inventory_sources/1',
},
'failure'
);
});
test('Can successfully create a new workflow job template node', async () => {
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
wrapper.update();
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(
null,
'workflow_job_template'
);
});
wrapper.update();
wrapper.find('DataListRadio').simulate('click');
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
id: 1,
name: 'Test Workflow Job Template',
type: 'workflow_job_template',
url: '/api/v2/workflow_job_templates/1',
},
'success'
);
});
test('Can successfully create a new approval template node', async () => {
act(() => {
wrapper.find('#link-type-always').simulate('click');
});
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
wrapper.update();
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
});
wrapper.update();
await act(async () => {
wrapper.find('input#approval-name').simulate('change', {
target: { value: 'Test Approval', name: 'name' },
});
wrapper.find('input#approval-description').simulate('change', {
target: { value: 'Test Approval Description', name: 'description' },
});
wrapper.find('input#approval-timeout-minutes').simulate('change', {
target: { value: 5, name: 'timeoutMinutes' },
});
});
// Updating the minutes and seconds is split to avoid a race condition.
// They both update the same state variable in the parent so triggering
// them syncronously creates flakey test results.
await act(async () => {
wrapper.find('input#approval-timeout-seconds').simulate('change', {
target: { value: 30, name: 'timeoutSeconds' },
});
});
wrapper.update();
expect(wrapper.find('input#approval-name').prop('value')).toBe(
'Test Approval'
);
expect(wrapper.find('input#approval-description').prop('value')).toBe(
'Test Approval Description'
);
expect(wrapper.find('input#approval-timeout-minutes').prop('value')).toBe(
5
);
expect(wrapper.find('input#approval-timeout-seconds').prop('value')).toBe(
30
);
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
description: 'Test Approval Description',
name: 'Test Approval',
timeout: 330,
type: 'workflow_approval_template',
},
'always'
);
});
test('Cancel button dispatches as expected', () => {
wrapper.find('button#cancel-node-modal').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'CANCEL_NODE_MODAL',
});
});
});
describe('Edit existing node', () => {
afterEach(() => {
wrapper.unmount();
});
test('Can successfully change project sync node to workflow approval node', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
nodeToEdit: {
id: 2,
unifiedJobTemplate: {
id: 1,
name: 'Test Project',
unified_job_type: 'project_update',
},
},
}}
>
<NodeModal
askLinkType={false}
onSave={onSave}
title="Edit Node"
/>
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('project_sync');
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(null, 'approval');
});
wrapper.update();
await act(async () => {
wrapper.find('input#approval-name').simulate('change', {
target: { value: 'Test Approval', name: 'name' },
});
wrapper.find('input#approval-description').simulate('change', {
target: { value: 'Test Approval Description', name: 'description' },
});
wrapper.find('input#approval-timeout-minutes').simulate('change', {
target: { value: 5, name: 'timeoutMinutes' },
});
});
// Updating the minutes and seconds is split to avoid a race condition.
// They both update the same state variable in the parent so triggering
// them syncronously creates flakey test results.
await act(async () => {
wrapper.find('input#approval-timeout-seconds').simulate('change', {
target: { value: 30, name: 'timeoutSeconds' },
});
});
wrapper.update();
expect(wrapper.find('input#approval-name').prop('value')).toBe(
'Test Approval'
);
expect(wrapper.find('input#approval-description').prop('value')).toBe(
'Test Approval Description'
);
expect(wrapper.find('input#approval-timeout-minutes').prop('value')).toBe(
5
);
expect(wrapper.find('input#approval-timeout-seconds').prop('value')).toBe(
30
);
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
description: 'Test Approval Description',
name: 'Test Approval',
timeout: 330,
type: 'workflow_approval_template',
},
null
);
});
test('Can successfully change approval node to workflow job template node', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider
value={{
nodeToEdit: {
id: 2,
unifiedJobTemplate: {
id: 1,
name: 'Test Approval',
description: 'Test Approval Description',
unified_job_type: 'workflow_approval',
timeout: 0,
},
},
}}
>
<NodeModal
askLinkType={false}
onSave={onSave}
title="Edit Node"
/>
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
expect(wrapper.find('AnsibleSelect').prop('value')).toBe('approval');
await act(async () => {
wrapper.find('AnsibleSelect').prop('onChange')(
null,
'workflow_job_template'
);
});
wrapper.update();
wrapper.find('DataListRadio').simulate('click');
await act(async () => {
wrapper.find('button#next-node-modal').simulate('click');
});
expect(onSave).toBeCalledWith(
{
id: 1,
name: 'Test Workflow Job Template',
type: 'workflow_job_template',
url: '/api/v2/workflow_job_templates/1',
},
null
);
});
});
});

View File

@ -18,6 +18,7 @@ function NodeNextButton({
return (
<Button
id="next-node-modal"
variant="primary"
type="submit"
onClick={() => onClick(activeStep)}

View File

@ -0,0 +1,48 @@
import React from 'react';
import { mount } from 'enzyme';
import NodeNextButton from './NodeNextButton';
const activeStep = {
name: 'Node Type',
key: 'node_resource',
enableNext: true,
component: {},
id: 1,
};
const buttonText = 'Next';
const onClick = jest.fn();
const onNext = jest.fn();
const triggerNext = 0;
let wrapper;
describe('NodeNextButton', () => {
beforeAll(() => {
wrapper = mount(
<NodeNextButton
activeStep={activeStep}
buttonText={buttonText}
onClick={onClick}
onNext={onNext}
triggerNext={triggerNext}
/>
);
});
afterAll(() => {
wrapper.unmount();
});
test('Button text matches', () => {
expect(wrapper.find('button').text()).toBe(buttonText);
});
test('Clicking button makes expected callback', () => {
wrapper.find('button').simulate('click');
expect(onClick).toBeCalledWith(activeStep);
});
test('onNext triggered when triggerNext counter incrimented', () => {
wrapper.setProps({ triggerNext: 1 });
expect(onNext).toBeCalled();
});
});

View File

@ -26,6 +26,7 @@ const TimeoutInput = styled(TextInput)`
margin-left: 20px;
}
`;
TimeoutInput.displayName = 'TimeoutInput';
const TimeoutLabel = styled.p`
margin-left: 10px;
@ -125,12 +126,12 @@ function NodeTypeStep({
timeoutMinutes: Math.floor(timeout / 60),
timeoutSeconds: timeout - Math.floor(timeout / 60) * 60,
}}
render={() => (
>
{() => (
<Form css="margin-top: 20px;">
<FormRow>
<Field
name="name"
render={({ field, form }) => {
<Field name="name">
{({ field, form }) => {
const isValid =
form &&
(!form.touched[field.name] || !form.errors[field.name]);
@ -150,19 +151,18 @@ function NodeTypeStep({
type="text"
{...field}
onChange={(value, evt) => {
onUpdateName(value);
onUpdateName(evt.target.value);
field.onChange(evt);
}}
/>
</FormGroup>
);
}}
/>
</Field>
</FormRow>
<FormRow>
<Field
name="description"
render={({ field }) => (
<Field name="description">
{({ field }) => (
<FormGroup
fieldId="approval-description"
label={i18n._(t`Description`)}
@ -172,13 +172,13 @@ function NodeTypeStep({
type="text"
{...field}
onChange={(value, evt) => {
onUpdateDescription(value);
onUpdateDescription(evt.target.value);
field.onChange(evt);
}}
/>
</FormGroup>
)}
/>
</Field>
</FormRow>
<FormRow>
<FormGroup
@ -186,9 +186,8 @@ function NodeTypeStep({
fieldId="approval-timeout"
>
<div css="display: flex;align-items: center;">
<Field
name="timeoutMinutes"
render={({ field, form }) => (
<Field name="timeoutMinutes">
{({ field, form }) => (
<>
<TimeoutInput
id="approval-timeout-minutes"
@ -197,11 +196,14 @@ function NodeTypeStep({
step="1"
{...field}
onChange={(value, evt) => {
if (!value || value === '') {
value = 0;
if (
!evt.target.value ||
evt.target.value === ''
) {
evt.target.value = 0;
}
onUpdateTimeout(
Number(value) * 60 +
Number(evt.target.value) * 60 +
Number(form.values.timeoutSeconds)
);
field.onChange(evt);
@ -212,10 +214,9 @@ function NodeTypeStep({
</TimeoutLabel>
</>
)}
/>
<Field
name="timeoutSeconds"
render={({ field, form }) => (
</Field>
<Field name="timeoutSeconds">
{({ field, form }) => (
<>
<TimeoutInput
id="approval-timeout-seconds"
@ -224,11 +225,14 @@ function NodeTypeStep({
step="1"
{...field}
onChange={(value, evt) => {
if (!value || value === '') {
value = 0;
if (
!evt.target.value ||
evt.target.value === ''
) {
evt.target.value = 0;
}
onUpdateTimeout(
Number(value) +
Number(evt.target.value) +
Number(form.values.timeoutMinutes) * 60
);
field.onChange(evt);
@ -239,13 +243,13 @@ function NodeTypeStep({
</TimeoutLabel>
</>
)}
/>
</Field>
</div>
</FormGroup>
</FormRow>
</Form>
)}
/>
</Formik>
)}
</>
);

View File

@ -0,0 +1,22 @@
import React from 'react';
import { WorkflowDispatchContext } from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import NodeViewModal from './NodeViewModal';
let wrapper;
const dispatch = jest.fn();
describe('NodeViewModal', () => {
test('Close button dispatches as expected', () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<NodeViewModal />
</WorkflowDispatchContext.Provider>
);
wrapper.find('TimesIcon').simulate('click');
expect(dispatch).toHaveBeenCalledWith({
type: 'SET_NODE_TO_VIEW',
value: null,
});
});
});

View File

@ -29,6 +29,7 @@ function RunStep({ i18n, linkType, onUpdateLinkType }) {
</p>
<Grid>
<SelectableCard
id="link-type-success"
isSelected={linkType === 'success'}
label={i18n._(t`On Success`)}
description={i18n._(
@ -37,6 +38,7 @@ function RunStep({ i18n, linkType, onUpdateLinkType }) {
onClick={() => onUpdateLinkType('success')}
/>
<SelectableCard
id="link-type-failure"
isSelected={linkType === 'failure'}
label={i18n._(t`On Failure`)}
description={i18n._(
@ -45,6 +47,7 @@ function RunStep({ i18n, linkType, onUpdateLinkType }) {
onClick={() => onUpdateLinkType('failure')}
/>
<SelectableCard
id="link-type-always"
isSelected={linkType === 'always'}
label={i18n._(t`Always`)}
description={i18n._(

View File

@ -0,0 +1,40 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import RunStep from './RunStep';
let wrapper;
const linkType = 'always';
const onUpdateLinkType = jest.fn();
describe('RunStep', () => {
beforeAll(() => {
wrapper = mountWithContexts(
<RunStep linkType={linkType} onUpdateLinkType={onUpdateLinkType} />
);
});
afterAll(() => {
wrapper.unmount();
});
test('Default selected card matches default link type when present', () => {
expect(wrapper.find('#link-type-success').props().isSelected).toBe(false);
expect(wrapper.find('#link-type-failure').props().isSelected).toBe(false);
expect(wrapper.find('#link-type-always').props().isSelected).toBe(true);
});
test('Clicking success card makes expected callback', () => {
wrapper.find('#link-type-success').simulate('click');
expect(onUpdateLinkType).toHaveBeenCalledWith('success');
});
test('Clicking failure card makes expected callback', () => {
wrapper.find('#link-type-failure').simulate('click');
expect(onUpdateLinkType).toHaveBeenCalledWith('failure');
});
test('Clicking always card makes expected callback', () => {
wrapper.find('#link-type-always').simulate('click');
expect(onUpdateLinkType).toHaveBeenCalledWith('always');
});
});

View File

@ -1,7 +1,5 @@
import React from 'react';
import {
WorkflowDispatchContext,
} from '@contexts/Workflow';
import { WorkflowDispatchContext } from '@contexts/Workflow';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import UnsavedChangesModal from './UnsavedChangesModal';