[Ingest Pipelines] Add test coverage for ingest pipelines editor component (#69283)

* first iteration of CIT tests

* address pr feedback

- use dot notation where we can
- use string literals instead of + concatentation
This commit is contained in:
Jean-Louis Leysens 2020-06-18 19:24:41 +02:00 committed by GitHub
parent 2544daf21b
commit e0460290b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 334 additions and 65 deletions

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed, TestBed } from '../../../../../../../test_utils';
import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container';
const testBedSetup = registerTestBed<TestSubject>(PipelineProcessorsEditor, {
doMountAsync: false,
});
export interface SetupResult extends TestBed<TestSubject> {
actions: {
toggleOnFailure: () => void;
};
}
export const setup = async (props: Props): Promise<SetupResult> => {
const testBed = await testBedSetup(props);
const toggleOnFailure = () => {
const { find } = testBed;
find('pipelineEditorOnFailureToggle').simulate('click');
};
return {
...testBed,
actions: { toggleOnFailure },
};
};
type TestSubject =
| 'pipelineEditorDoneButton'
| 'pipelineEditorOnFailureToggle'
| 'pipelineEditorOnFailureTree';

View file

@ -0,0 +1,158 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act } from 'react-dom/test-utils';
import React from 'react';
import { registerTestBed, TestBed } from '../../../../../../../test_utils';
import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
// which does not produce a valid component wrapper
EuiComboBox: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockComboBox'}
data-currentvalue={props.selectedOptions}
onChange={async (syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
}}
/>
),
// Mocking EuiCodeEditor, which uses React Ace under the hood
EuiCodeEditor: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-currentvalue={props.value}
onChange={(e: any) => {
props.onChange(e.jsonContent);
}}
/>
),
}));
jest.mock('react-virtualized', () => ({
...jest.requireActual('react-virtualized'),
AutoSizer: ({ children }: { children: any }) => (
<div>{children({ height: 500, width: 500 })}</div>
),
}));
const testBedSetup = registerTestBed<TestSubject>(PipelineProcessorsEditor, {
doMountAsync: false,
});
export interface SetupResult extends TestBed<TestSubject> {
actions: ReturnType<typeof createActions>;
}
/**
* We make heavy use of "processorSelector" in these actions. They are a way to uniquely identify
* a processor and are a stringified version of {@link ProcessorSelector}.
*
* @remark
* See also {@link selectorToDataTestSubject}.
*/
const createActions = (testBed: TestBed<TestSubject>) => {
const { find, component } = testBed;
return {
async addProcessor(processorsSelector: string, type: string, options: Record<string, any>) {
find(`${processorsSelector}.addProcessorButton`).simulate('click');
await act(async () => {
find('processorTypeSelector').simulate('change', [{ value: type, label: type }]);
});
component.update();
await act(async () => {
find('processorOptionsEditor').simulate('change', {
jsonContent: JSON.stringify(options),
});
});
await act(async () => {
find('processorSettingsForm.submitButton').simulate('click');
});
},
removeProcessor(processorSelector: string) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
find(`${processorSelector}.moreMenu.deleteButton`).simulate('click');
act(() => {
find('removeProcessorConfirmationModal.confirmModalConfirmButton').simulate('click');
});
},
moveProcessor(processorSelector: string, dropZoneSelector: string) {
act(() => {
find(`${processorSelector}.moveItemButton`).simulate('click');
});
act(() => {
find(dropZoneSelector).last().simulate('click');
});
component.update();
},
async addOnFailureProcessor(
processorSelector: string,
type: string,
options: Record<string, any>
) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
find(`${processorSelector}.moreMenu.addOnFailureButton`).simulate('click');
await act(async () => {
find('processorTypeSelector').simulate('change', [{ value: type, label: type }]);
});
component.update();
await act(async () => {
find('processorOptionsEditor').simulate('change', {
jsonContent: JSON.stringify(options),
});
});
await act(async () => {
find('processorSettingsForm.submitButton').simulate('click');
});
},
duplicateProcessor(processorSelector: string) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
act(() => {
find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click');
});
},
startAndCancelMove(processorSelector: string) {
act(() => {
find(`${processorSelector}.moveItemButton`).simulate('click');
});
component.update();
act(() => {
find(`${processorSelector}.cancelMoveItemButton`).simulate('click');
});
},
toggleOnFailure() {
find('pipelineEditorOnFailureToggle').simulate('click');
},
};
};
export const setup = async (props: Props): Promise<SetupResult> => {
const testBed = await testBedSetup(props);
return {
...testBed,
actions: createActions(testBed),
};
};
type TestSubject =
| 'pipelineEditorDoneButton'
| 'pipelineEditorOnFailureToggle'
| 'addProcessorsButtonLevel1'
| 'processorSettingsForm'
| 'processorSettingsForm.submitButton'
| 'processorOptionsEditor'
| 'processorSettingsFormFlyout'
| 'processorTypeSelector'
| 'pipelineEditorOnFailureTree'
| string;

View file

@ -3,8 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { setup } from './pipeline_processors_editor.helpers';
import { setup, SetupResult } from './pipeline_processors_editor.helpers';
import { Pipeline } from '../../../../../common/types';
const testProcessors: Pick<Pipeline, 'processors'> = {
@ -25,10 +24,20 @@ const testProcessors: Pick<Pipeline, 'processors'> = {
};
describe('Pipeline Editor', () => {
it('provides the same data out it got in if nothing changes', async () => {
const onUpdate = jest.fn();
let onUpdate: jest.Mock;
let testBed: SetupResult;
await setup({
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(async () => {
onUpdate = jest.fn();
testBed = await setup({
value: {
...testProcessors,
},
@ -38,7 +47,9 @@ describe('Pipeline Editor', () => {
onTestPipelineClick: jest.fn(),
esDocsBasePath: 'test',
});
});
it('provides the same data out it got in if nothing changes', () => {
const {
calls: [[arg]],
} = onUpdate.mock;
@ -46,20 +57,134 @@ describe('Pipeline Editor', () => {
expect(arg.getData()).toEqual(testProcessors);
});
it('toggles the on-failure processors', async () => {
const { actions, exists } = await setup({
value: {
...testProcessors,
},
onFlyoutOpen: jest.fn(),
onUpdate: jest.fn(),
isTestButtonDisabled: false,
onTestPipelineClick: jest.fn(),
esDocsBasePath: 'test',
});
it('toggles the on-failure processors tree', () => {
const { actions, exists } = testBed;
expect(exists('pipelineEditorOnFailureTree')).toBe(false);
actions.toggleOnFailure();
expect(exists('pipelineEditorOnFailureTree')).toBe(true);
});
describe('processors', () => {
it('adds a new processor', async () => {
const { actions } = testBed;
await actions.addProcessor('processors', 'test', { if: '1 == 1' });
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors.length).toBe(3);
const [a, b, c] = processors;
expect(a).toEqual(testProcessors.processors[0]);
expect(b).toEqual(testProcessors.processors[1]);
expect(c).toEqual({ test: { if: '1 == 1' } });
});
it('removes a processor', () => {
const { actions } = testBed;
// processor>0 denotes the first processor in the top-level processors array.
actions.removeProcessor('processors>0');
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors.length).toBe(1);
expect(processors[0]).toEqual({
gsub: {
field: '_index',
pattern: '(.monitoring-\\w+-)6(-.+)',
replacement: '$17$2',
},
});
});
it('reorders processors', () => {
const { actions } = testBed;
actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1');
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors).toEqual(testProcessors.processors.slice(0).reverse());
});
it('adds an on-failure processor to a processor', async () => {
const { actions, find, exists } = testBed;
const processorSelector = 'processors>1';
await actions.addOnFailureProcessor(processorSelector, 'test', { if: '1 == 2' });
// Assert that the add on failure button has been removed
find(`${processorSelector}.moreMenu.button`).simulate('click');
expect(!exists(`${processorSelector}.moreMenu.addOnFailureButton`));
// Assert that the add processor button is now visible
expect(exists(`${processorSelector}.addProcessor`));
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors.length).toBe(2);
expect(processors[0]).toEqual(testProcessors.processors[0]); // should be unchanged
expect(processors[1].gsub).toEqual({
...testProcessors.processors[1].gsub,
on_failure: [{ test: { if: '1 == 2' } }],
});
});
it('moves a processor to a nested dropzone', async () => {
const { actions } = testBed;
await actions.addOnFailureProcessor('processors>1', 'test', { if: '1 == 3' });
actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1>onFailure>0');
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors.length).toBe(1);
expect(processors[0].gsub.on_failure).toEqual([
{
test: { if: '1 == 3' },
},
testProcessors.processors[0],
]);
});
it('duplicates a processor', async () => {
const { actions } = testBed;
await actions.addOnFailureProcessor('processors>1', 'test', { if: '1 == 4' });
actions.duplicateProcessor('processors>1');
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
expect(processors.length).toBe(3);
const duplicatedProcessor = {
gsub: {
...testProcessors.processors[1].gsub,
on_failure: [{ test: { if: '1 == 4' } }],
},
};
expect(processors).toEqual([
testProcessors.processors[0],
duplicatedProcessor,
duplicatedProcessor,
]);
});
it('can cancel a move', () => {
const { actions, exists } = testBed;
const processorSelector = 'processors>0';
actions.startAndCancelMove(processorSelector);
// Assert that we have exited move mode for this processor
expect(exists(`moveItemButton-${processorSelector}`));
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const { processors } = onUpdateResult.getData();
// Assert that nothing has changed
expect(processors).toEqual(testProcessors.processors);
});
it('moves to and from the global on-failure tree', async () => {
const { actions } = testBed;
actions.toggleOnFailure();
await actions.addProcessor('onFailure', 'test', { if: '1 == 5' });
actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0');
const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const data1 = onUpdateResult1.getData();
expect(data1.processors.length).toBe(1);
expect(data1.on_failure.length).toBe(2);
expect(data1.processors).toEqual([testProcessors.processors[1]]);
expect(data1.on_failure).toEqual([{ test: { if: '1 == 5' } }, testProcessors.processors[0]]);
actions.moveProcessor('onFailure>1', 'dropButtonAbove-processors>0');
const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
const data2 = onUpdateResult2.getData();
expect(data2.processors.length).toBe(2);
expect(data2.on_failure.length).toBe(1);
expect(data2.processors).toEqual(testProcessors.processors);
expect(data2.on_failure).toEqual([{ test: { if: '1 == 5' } }]);
});
});
});

View file

@ -13,12 +13,14 @@ export interface Props {
onClick: () => void;
}
export const AddProcessorButton: FunctionComponent<Props> = ({ onClick }) => {
export const AddProcessorButton: FunctionComponent<Props> = (props) => {
const { onClick } = props;
const {
state: { editor },
} = usePipelineProcessorsContext();
return (
<EuiButtonEmpty
data-test-subj="addProcessorButton"
disabled={editor.mode.id !== 'idle'}
iconSide="left"
iconType="plusInCircle"

View file

@ -16,19 +16,16 @@ interface Props {
onDuplicate: () => void;
onDelete: () => void;
onAddOnFailure: () => void;
'data-test-subj'?: string;
}
export const ContextMenu: FunctionComponent<Props> = ({
showAddOnFailure,
onDuplicate,
onAddOnFailure,
onDelete,
disabled,
}) => {
export const ContextMenu: FunctionComponent<Props> = (props) => {
const { showAddOnFailure, onDuplicate, onAddOnFailure, onDelete, disabled } = props;
const [isOpen, setIsOpen] = useState<boolean>(false);
const contextMenuItems = [
<EuiContextMenuItem
data-test-subj="duplicateButton"
key="duplicate"
icon="copy"
onClick={() => {
@ -40,6 +37,7 @@ export const ContextMenu: FunctionComponent<Props> = ({
</EuiContextMenuItem>,
showAddOnFailure ? (
<EuiContextMenuItem
data-test-subj="addOnFailureButton"
key="addOnFailure"
icon="indexClose"
onClick={() => {
@ -51,6 +49,7 @@ export const ContextMenu: FunctionComponent<Props> = ({
</EuiContextMenuItem>
) : undefined,
<EuiContextMenuItem
data-test-subj="deleteButton"
key="delete"
icon="trash"
color="danger"
@ -65,12 +64,14 @@ export const ContextMenu: FunctionComponent<Props> = ({
return (
<EuiPopover
data-test-subj={props['data-test-subj']}
anchorPosition="leftCenter"
panelPaddingSize="none"
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
button={
<EuiButtonIcon
data-test-subj="button"
disabled={disabled}
onClick={() => setIsOpen((v) => !v)}
iconType="boxesHorizontal"

View file

@ -8,6 +8,7 @@ import React, { FunctionComponent, memo } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import { ProcessorInternal, ProcessorSelector } from '../../types';
import { selectorToDataTestSubject } from '../../utils';
import { usePipelineProcessorsContext } from '../../context';
@ -46,6 +47,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
responsive={false}
alignItems="center"
justifyContent="spaceBetween"
data-test-subj={selectorToDataTestSubject(selector)}
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
@ -85,6 +87,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="editItemButton"
disabled={disabled}
aria-label={editorItemMessages.editorButtonLabel}
iconType="pencil"
@ -100,6 +103,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
<EuiFlexItem grow={false}>
{selected ? (
<EuiButtonIcon
data-test-subj="cancelMoveItemButton"
aria-label={editorItemMessages.cancelMoveButtonLabel}
size="s"
onClick={onCancelMove}
@ -108,6 +112,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
) : (
<EuiToolTip content={editorItemMessages.moveButtonLabel}>
<EuiButtonIcon
data-test-subj="moveItemButton"
disabled={disabled}
aria-label={editorItemMessages.moveButtonLabel}
size="s"
@ -121,6 +126,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent<Props> = memo(
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ContextMenu
data-test-subj="moreMenu"
disabled={disabled}
showAddOnFailure={!processor.onFailure?.length}
onAddOnFailure={() => {

View file

@ -64,7 +64,7 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
);
return (
<Form form={form}>
<Form data-test-subj="processorSettingsForm" form={form}>
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader>
<EuiFlexGroup gutterSize="xs">
@ -122,7 +122,7 @@ export const ProcessorSettingsForm: FunctionComponent<Props> = memo(
return (
<>
{formContent}
<EuiButton onClick={form.submit}>
<EuiButton data-test-subj="submitButton" onClick={form.submit}>
{i18n.translate(
'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel',
{ defaultMessage: 'Submit' }

View file

@ -54,6 +54,7 @@ export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) =>
component={ComboBoxField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'processorTypeSelector',
fullWidth: true,
options: types.map((type) => ({ label: type, value: type })),
noSuggestions: false,

View file

@ -76,6 +76,7 @@ export const Custom: FunctionComponent<Props> = ({ defaultOptions }) => {
defaultValue={defaultOptions}
componentProps={{
euiCodeEditorProps: {
'data-test-subj': 'processorOptionsEditor',
height: '300px',
'aria-label': i18n.translate(
'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel',

View file

@ -12,13 +12,15 @@ import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
export interface Props {
isDisabled: boolean;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
'data-test-subj'?: string;
}
const MOVE_HERE_LABEL = i18n.translate('xpack.ingestPipelines.pipelineEditor.moveTargetLabel', {
defaultMessage: 'Move here',
});
export const DropZoneButton: FunctionComponent<Props> = ({ onClick, isDisabled }) => {
export const DropZoneButton: FunctionComponent<Props> = (props) => {
const { onClick, isDisabled } = props;
const containerClasses = classNames({
'pipelineProcessorsEditor__tree__dropZoneContainer--active': !isDisabled,
});
@ -31,6 +33,7 @@ export const DropZoneButton: FunctionComponent<Props> = ({ onClick, isDisabled }
className={`pipelineProcessorsEditor__tree__dropZoneContainer ${containerClasses}`}
>
<EuiButtonIcon
data-test-subj={props['data-test-subj']}
className={`pipelineProcessorsEditor__tree__dropZoneButton ${buttonClasses}`}
aria-label={MOVE_HERE_LABEL}
disabled={isDisabled}

View file

@ -11,6 +11,7 @@ import { AutoSizer, List, WindowScroller } from 'react-virtualized';
import { DropSpecialLocations } from '../../../constants';
import { ProcessorInternal, ProcessorSelector } from '../../../types';
import { isChildPath } from '../../../processors_reducer';
import { selectorToDataTestSubject } from '../../../utils';
import { DropZoneButton } from '.';
import { TreeNode } from '.';
@ -73,10 +74,12 @@ export const PrivateTree: FunctionComponent<PrivateProps> = ({
info: ProcessorInfo;
processor: ProcessorInternal;
}) => {
const stringifiedSelector = selectorToDataTestSubject(info.selector);
return (
<>
{idx === 0 ? (
<DropZoneButton
data-test-subj={`dropButtonAbove-${stringifiedSelector}`}
onClick={(event) => {
event.preventDefault();
onAction({
@ -102,6 +105,7 @@ export const PrivateTree: FunctionComponent<PrivateProps> = ({
/>
</EuiFlexItem>
<DropZoneButton
data-test-subj={`dropButtonBelow-${stringifiedSelector}`}
isDisabled={Boolean(!movingProcessor || isDropZoneBelowDisabled(info, movingProcessor!))}
onClick={(event) => {
event.preventDefault();

View file

@ -89,6 +89,7 @@ export const TreeNode: FunctionComponent<Props> = ({
processors={processor.onFailure}
/>
<AddProcessorButton
data-test-subj={stringSelector}
onClick={() =>
onAction({
type: 'addProcessor',

View file

@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, keyCodes } from '@elastic/eui';
import { List, WindowScroller } from 'react-virtualized';
import { ProcessorInternal, ProcessorSelector } from '../../types';
import { selectorToDataTestSubject } from '../../utils';
import './processors_tree.scss';
import { AddProcessorButton } from '../add_processor_button';
@ -96,7 +97,7 @@ export const ProcessorsTree: FunctionComponent<Props> = memo((props) => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} justifyContent="flexStart" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiFlexItem data-test-subj={selectorToDataTestSubject(baseSelector)} grow={false}>
<AddProcessorButton
onClick={() => {
onAction({ type: 'addProcessor', payload: { target: baseSelector } });

View file

@ -6,6 +6,8 @@
import { ProcessorSelector } from './types';
export const selectorToDataTestSubject = (selector: ProcessorSelector) => selector.join('>');
type Path = string[];
/**