[Cases] Fix connector information disappearing (#110914)

* Move intialization to use effect

* Fixing fields can't get test working

* Fix tests

Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
This commit is contained in:
Jonathan Buttner 2021-09-03 12:00:51 -04:00 committed by GitHub
parent b6ab15e9f4
commit a2c848e1d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 110 additions and 52 deletions

View file

@ -57,8 +57,8 @@ export const useGetFieldsByIssueType = ({
});
if (!didCancel.current) {
setIsLoading(false);
setFields(res.data ?? {});
setIsLoading(false);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.FIELDS_API_ERROR,

View file

@ -56,13 +56,14 @@ export const useGetIssueTypes = ({
});
if (!didCancel.current) {
setIsLoading(false);
const asOptions = (res.data ?? []).map((type) => ({
text: type.name ?? '',
value: type.id ?? '',
}));
setIssueTypes(res.data ?? []);
handleIssueType(asOptions);
setIsLoading(false);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.ISSUE_TYPES_API_ERROR,

View file

@ -7,16 +7,14 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { render, waitFor, screen } from '@testing-library/react';
import { EditConnector, EditConnectorProps } from './index';
import { getFormMock, useFormMock } from '../__mock__/form';
import { TestProviders } from '../../common/mock';
import { connectorsMock } from '../../containers/configure/mock';
import { basicCase, basicPush, caseUserActions } from '../../containers/mock';
import { useKibana } from '../../common/lib/kibana';
jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form');
jest.mock('../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
@ -50,17 +48,32 @@ const defaultProps: EditConnectorProps = {
};
describe('EditConnector ', () => {
const sampleConnector = '123';
const formHookMock = getFormMock({ connectorId: sampleConnector });
beforeEach(() => {
jest.clearAllMocks();
useFormMock.mockImplementation(() => ({ form: formHookMock }));
useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({
actionTypeTitle: '.servicenow',
iconClass: 'logoSecurity',
});
});
it('Renders servicenow connector from case initially', async () => {
const serviceNowProps = {
...defaultProps,
caseData: {
...defaultProps.caseData,
connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' },
},
};
render(
<TestProviders>
<EditConnector {...serviceNowProps} />
</TestProviders>
);
expect(await screen.findByText('My Connector')).toBeInTheDocument();
});
it('Renders no connector, and then edit', async () => {
const wrapper = mount(
<TestProviders>
@ -98,58 +111,81 @@ describe('EditConnector ', () => {
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector));
await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe('resilient-2'));
});
it('Revert to initial external service on error', async () => {
onSubmit.mockImplementation((connector, onSuccess, onError) => {
onError(new Error('An error has occurred'));
});
const wrapper = mount(
<TestProviders>
<EditConnector {...defaultProps} />
</TestProviders>
);
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
wrapper.update();
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
await waitFor(() => {
wrapper.update();
expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connectorId', 'none');
});
});
it('Resets selector on cancel', async () => {
const props = {
...defaultProps,
caseData: {
...defaultProps.caseData,
connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' },
},
};
const wrapper = mount(
<TestProviders>
<EditConnector {...props} />
</TestProviders>
);
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
await waitFor(() => {
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
wrapper.update();
expect(
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()
).toBeTruthy();
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
});
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy();
});
/**
* If an error is being throw on submit the selected connector should
* be reverted to the initial one. In our test the initial one is the .servicenow-1
* connector. The title of the .servicenow-1 connector is My Connector.
*/
expect(wrapper.text().includes('My Connector')).toBeTruthy();
});
it('Resets selector on cancel', async () => {
const props = {
...defaultProps,
caseData: {
...defaultProps.caseData,
connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' },
},
};
const wrapper = mount(
<TestProviders>
<EditConnector {...props} />
</TestProviders>
);
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
wrapper.update();
wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click');
await waitFor(() => {
wrapper.update();
expect(formHookMock.setFieldValue).toBeCalledWith(
'connectorId',
defaultProps.caseData.connector.id
);
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy();
});
expect(wrapper.text().includes('My Connector')).toBeTruthy();
});
it('Renders loading spinner', async () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useReducer } from 'react';
import React, { useCallback, useEffect, useReducer } from 'react';
import deepEqual from 'fast-deep-equal';
import {
EuiText,
@ -144,33 +144,54 @@ export const EditConnector = React.memo(
{ ...initialState, fields: caseFields }
);
useEffect(() => {
// Initialize the current connector with the connector information attached to the case if we can find that
// connector in the retrieved connectors from the API call
if (!isLoading) {
dispatch({
type: 'SET_CURRENT_CONNECTOR',
payload: getConnectorById(caseData.connector.id, connectors),
});
// Set the fields initially to whatever is present in the case, this should match with
// the latest user action for an update connector as well
dispatch({
type: 'SET_FIELDS',
payload: caseFields,
});
}
}, [caseData.connector.id, connectors, isLoading, caseFields]);
/**
* There is a race condition with this callback. At some point during the initial mounting of this component, this
* callback will be called. There are a couple problems with this:
*
* 1. If the call occurs before the above useEffect does its dispatches (aka while the connectors are still loading) this will
* result in setting the current connector to null when in fact we might have a valid connector. It could also
* cause issues when setting the fields because if there are no user actions then the getConnectorFieldsFromUserActions
* will return null even when the caseData.connector.fields is valid and populated.
*
* 2. If the call occurs after the above useEffect then the currentConnector should === newConnectorId
*
* As far as I know dispatch is synchronous so if the useEffect runs first it should successfully set currentConnector. If
* onChangeConnector runs first and sets stuff to null, then when useEffect runs it'll switch everything back to what we need it to be
* initially.
*/
const onChangeConnector = useCallback(
(newConnectorId) => {
// Init
if (currentConnector == null) {
// change connector on dropdown action
if (currentConnector?.id !== newConnectorId) {
dispatch({
type: 'SET_CURRENT_CONNECTOR',
payload: getConnectorById(newConnectorId, connectors),
});
}
// change connect on dropdown action
else if (currentConnector.id !== newConnectorId) {
dispatch({
type: 'SET_CURRENT_CONNECTOR',
payload: getConnectorById(newConnectorId, connectors),
});
dispatch({
type: 'SET_FIELDS',
payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []),
});
} else if (fields === null) {
dispatch({
type: 'SET_FIELDS',
payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []),
});
}
},
[currentConnector, fields, userActions, connectors]
[currentConnector, userActions, connectors]
);
const onFieldsChange = useCallback(