diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts index 464b4fd32aa14e..92ac66544a4bff 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts @@ -29,9 +29,21 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setCreatePipelineResponse = (response?: object, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', API_BASE_PATH, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadPipelinesResponse, setDeletePipelineResponse, + setCreatePipelineResponse, }; }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts index a2a5d4152cbae4..415c4c30706e82 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts @@ -5,6 +5,7 @@ */ import { setup as ingestPipelinesListSetup } from './ingest_pipelines_list.helpers'; +import { setup as pipelinesCreateSetup } from './pipelines_create.helpers'; export { nextTick, getRandomString, findTestSubject } from '../../../../../test_utils'; @@ -12,4 +13,5 @@ export { setupEnvironment } from './setup_environment'; export const pageHelpers = { ingestPipelinesList: { setup: ingestPipelinesListSetup }, + pipelinesCreate: { setup: pipelinesCreateSetup }, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts new file mode 100644 index 00000000000000..9675a6be1c0c9b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -0,0 +1,75 @@ +/* + * 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 { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; +import { nextTick } from './index'; + +export type TemplateFormTestBed = TestBed & + UnwrapPromise>; + +export const formSetup = async (initTestBed: SetupFunc) => { + const testBed = await initTestBed(); + + // User actions + const clickSubmitButton = () => { + testBed.find('submitButton').simulate('click'); + }; + + const clickTestPipelineButton = () => { + testBed.find('testPipelineButton').simulate('click'); + }; + + const clickShowRequestLink = () => { + testBed.find('showRequestLink').simulate('click'); + }; + + const toggleVersionSwitch = () => { + testBed.form.toggleEuiSwitch('versionToggle'); + }; + + const toggleOnFailureSwitch = () => { + testBed.form.toggleEuiSwitch('onFailureToggle'); + }; + + const setEditorInputValue = async (testSubject: PipelineFormTestSubjects, value: string) => { + testBed.find(testSubject).simulate('change', { + jsonString: value, + }); + await nextTick(); + testBed.component.update(); + }; + + return { + ...testBed, + actions: { + clickSubmitButton, + setEditorInputValue, + clickShowRequestLink, + toggleVersionSwitch, + toggleOnFailureSwitch, + clickTestPipelineButton, + }, + }; +}; + +export type PipelineFormTestSubjects = + | 'submitButton' + | 'pageTitle' + | 'savePipelineError' + | 'pipelineForm' + | 'versionToggle' + | 'versionField' + | 'nameField.input' + | 'descriptionField.input' + | 'processorsField' + | 'onFailureToggle' + | 'onFailureEditor' + | 'testPipelineButton' + | 'showRequestLink' + | 'requestFlyout' + | 'requestFlyout.title' + | 'testPipelineFlyout' + | 'testPipelineFlyout.title' + | 'documentationLink'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts new file mode 100644 index 00000000000000..263f915bbf32aa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -0,0 +1,26 @@ +/* + * 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, TestBedConfig } from '../../../../../test_utils'; +import { BASE_PATH } from '../../../common/constants'; +import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { formSetup, PipelineFormTestSubjects } from './pipeline_form.helpers'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/create`], + componentRoutePath: `${BASE_PATH}/create`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed( + WithAppDependencies(PipelinesCreate), + testBedConfig +); + +export const setup = formSetup.bind(null, initTestBed); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx new file mode 100644 index 00000000000000..ec0e86227563d2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -0,0 +1,210 @@ +/* + * 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 React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { TemplateFormTestBed } from './helpers/pipeline_form.helpers'; + +const { setup } = pageHelpers.pipelinesCreate; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), +})); + +describe('', () => { + let testBed: TemplateFormTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('on component mount', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + await testBed.waitFor('pipelineForm'); + }); + }); + + test('should render the correct page header', () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create pipeline'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Create pipeline docs'); + }); + + test('should toggle the version field', async () => { + const { actions, component, exists } = testBed; + + // Version field should be hidden by default + expect(exists('versionField')).toBe(false); + + await act(async () => { + actions.toggleVersionSwitch(); + await nextTick(); + component.update(); + }); + + expect(exists('versionField')).toBe(true); + }); + + test('should toggle the on-failure processors editor', async () => { + const { actions, component, exists } = testBed; + + // On-failure editor should be hidden by default + expect(exists('onFailureEditor')).toBe(false); + + await act(async () => { + actions.toggleOnFailureSwitch(); + await nextTick(); + component.update(); + }); + + expect(exists('onFailureEditor')).toBe(true); + }); + + test('should show the request flyout', async () => { + const { actions, component, find, exists } = testBed; + + await act(async () => { + actions.clickShowRequestLink(); + await nextTick(); + component.update(); + }); + + // Verify request flyout opens + expect(exists('requestFlyout')).toBe(true); + expect(find('requestFlyout.title').text()).toBe('Request'); + }); + + describe('form validation', () => { + test('should prevent form submission if required fields are missing', async () => { + const { form, actions, component, find } = testBed; + + await act(async () => { + actions.clickSubmitButton(); + await nextTick(); + component.update(); + }); + + expect(form.getErrorsMessages()).toEqual([ + 'A pipeline name is required.', + 'A pipeline description is required.', + ]); + expect(find('submitButton').props().disabled).toEqual(true); + + // Add required fields and verify button is enabled again + form.setInputValue('nameField.input', 'my_pipeline'); + form.setInputValue('descriptionField.input', 'pipeline description'); + + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('submitButton').props().disabled).toEqual(false); + }); + }); + + describe('form submission', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + + const { waitFor, form } = testBed; + + await waitFor('pipelineForm'); + + form.setInputValue('nameField.input', 'my_pipeline'); + form.setInputValue('descriptionField.input', 'pipeline description'); + }); + }); + + test('should send the correct payload', async () => { + const { actions } = testBed; + + await act(async () => { + actions.clickSubmitButton(); + await nextTick(100); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: 'my_pipeline', + description: 'pipeline description', + processors: [], + }; + + expect(JSON.parse(latestRequest.requestBody)).toEqual(expected); + }); + + test('should surface API errors from the request', async () => { + const { component, actions, find, exists } = testBed; + + const error = { + status: 409, + error: 'Conflict', + message: `There is already a pipeline with name 'my_pipeline'.`, + }; + + httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { body: error }); + + await act(async () => { + actions.clickSubmitButton(); + await nextTick(100); + component.update(); + }); + + expect(exists('savePipelineError')).toBe(true); + expect(find('savePipelineError').text()).toContain(error.message); + }); + }); + + describe('test pipeline', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + + const { waitFor } = testBed; + + await waitFor('pipelineForm'); + }); + }); + + test('should open the test pipeline flyout', async () => { + const { actions, component, exists, find } = testBed; + + await act(async () => { + actions.clickTestPipelineButton(); + await nextTick(); + component.update(); + }); + + // Verify test pipeline flyout opens + expect(exists('testPipelineFlyout')).toBe(true); + expect(find('testPipelineFlyout.title').text()).toBe('Test pipeline'); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 1d080dfc330bab..ec4f4c0cbcb679 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -128,6 +128,7 @@ export const PipelineForm: React.FunctionComponent = ({ setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} > {isRequestVisible ? ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 045afd52204faf..f9648a7aa39488 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -136,7 +136,12 @@ export const PipelineFormFields: React.FunctionComponent = ({ - + = ({ path="processors" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'processorsField', euiCodeEditorProps: { + ['data-test-subj']: 'processorsField', height: '300px', 'aria-label': i18n.translate('xpack.ingestPipelines.form.processorsFieldAriaLabel', { defaultMessage: 'Processors JSON editor', @@ -203,8 +208,8 @@ export const PipelineFormFields: React.FunctionComponent = ({ path="on_failure" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'onFailureEditor', euiCodeEditorProps: { + ['data-test-subj']: 'onFailureEditor', height: '300px', 'aria-label': i18n.translate('xpack.ingestPipelines.form.onFailureFieldAriaLabel', { defaultMessage: 'On-failure processors JSON editor', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx index c0f4b4a7a0aedc..19f6c180c39a52 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -128,10 +128,10 @@ export const PipelineTestFlyout: React.FunctionComponent + -

+

{pipeline.name ? ( = ({ uuid.current++; return ( - + -

+

{name ? ( - -

+ +

- -

+ +