diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx index ca4d50958005d..133da383e4e99 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import { TestProvider } from '../../../mocks/test_provider'; import { CreateIntegrationAssistant } from './create_integration_assistant'; import type { State } from './state'; import { ExperimentalFeaturesService } from '../../../services'; +import { mockReportEvent } from '../../../services/telemetry/mocks/service'; +import { TelemetryEventType } from '../../../services/telemetry/types'; export const defaultInitialState: State = { step: 1, @@ -20,6 +22,7 @@ export const defaultInitialState: State = { hasCelInput: false, result: undefined, }; + const mockInitialState = jest.fn((): State => defaultInitialState); jest.mock('./state', () => ({ ...jest.requireActual('./state'), @@ -39,39 +42,45 @@ const mockCelInputStep = jest.fn(() =>
const mockReviewCelStep = jest.fn(() =>
); const mockDeployStep = jest.fn(() =>
); -const mockIsConnectorStepReady = jest.fn(); -const mockIsIntegrationStepReady = jest.fn(); -const mockIsDataStreamStepReady = jest.fn(); -const mockIsReviewStepReady = jest.fn(); -const mockIsCelInputStepReady = jest.fn(); -const mockIsCelReviewStepReady = jest.fn(); +const mockIsConnectorStepReadyToComplete = jest.fn(); +const mockIsIntegrationStepReadyToComplete = jest.fn(); +const mockIsDataStreamStepReadyToComplete = jest.fn(); +const mockIsReviewStepReadyToComplete = jest.fn(); +const mockIsCelInputStepReadyToComplete = jest.fn(); +const mockIsCelReviewStepReadyToComplete = jest.fn(); jest.mock('./steps/connector_step', () => ({ ConnectorStep: () => mockConnectorStep(), - isConnectorStepReady: () => mockIsConnectorStepReady(), + isConnectorStepReadyToComplete: () => mockIsConnectorStepReadyToComplete(), })); jest.mock('./steps/integration_step', () => ({ IntegrationStep: () => mockIntegrationStep(), - isIntegrationStepReady: () => mockIsIntegrationStepReady(), + isIntegrationStepReadyToComplete: () => mockIsIntegrationStepReadyToComplete(), })); jest.mock('./steps/data_stream_step', () => ({ DataStreamStep: () => mockDataStreamStep(), - isDataStreamStepReady: () => mockIsDataStreamStepReady(), + isDataStreamStepReadyToComplete: () => mockIsDataStreamStepReadyToComplete(), })); jest.mock('./steps/review_step', () => ({ ReviewStep: () => mockReviewStep(), - isReviewStepReady: () => mockIsReviewStepReady(), + isReviewStepReadyToComplete: () => mockIsReviewStepReadyToComplete(), })); jest.mock('./steps/cel_input_step', () => ({ CelInputStep: () => mockCelInputStep(), - isCelInputStepReady: () => mockIsCelInputStepReady(), + isCelInputStepReadyToComplete: () => mockIsCelInputStepReadyToComplete(), })); jest.mock('./steps/review_cel_step', () => ({ ReviewCelStep: () => mockReviewCelStep(), - isCelReviewStepReady: () => mockIsCelReviewStepReady(), + isCelReviewStepReadyToComplete: () => mockIsCelReviewStepReadyToComplete(), })); jest.mock('./steps/deploy_step', () => ({ DeployStep: () => mockDeployStep() })); +const mockNavigate = jest.fn(); +jest.mock('../../../common/hooks/use_navigate', () => ({ + ...jest.requireActual('../../../common/hooks/use_navigate'), + useNavigate: () => mockNavigate, +})); + const renderIntegrationAssistant = () => render(, { wrapper: TestProvider }); @@ -89,19 +98,116 @@ describe('CreateIntegration', () => { mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 1 }); }); - it('should render connector', () => { + it('shoud report telemetry for assistant open', () => { + renderIntegrationAssistant(); + expect(mockReportEvent).toHaveBeenCalledWith(TelemetryEventType.IntegrationAssistantOpen, { + sessionId: expect.any(String), + }); + }); + + it('should render connector step', () => { const result = renderIntegrationAssistant(); expect(result.queryByTestId('connectorStepMock')).toBeInTheDocument(); }); - it('should call isConnectorStepReady', () => { + it('should call isConnectorStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsConnectorStepReady).toHaveBeenCalled(); + expect(mockIsConnectorStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show "Next" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Next'); + }); + + describe('when connector step is not done', () => { + beforeEach(() => { + mockIsConnectorStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); + }); + + it('should still enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should still enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + }); + + describe('when connector step is done', () => { + beforeEach(() => { + mockIsConnectorStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for connector step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 1, + stepName: 'Connector Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + }); + }); + + describe('when back button is clicked', () => { + let result: ReturnType; + beforeEach(() => { + result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-backButton').click(); + }); + }); + + it('should not report telemetry', () => { + expect(mockReportEvent).not.toHaveBeenCalled(); + }); + + it('should navigate to the landing page', () => { + expect(mockNavigate).toHaveBeenCalledWith('landing'); + }); }); }); describe('when step is 2', () => { beforeEach(() => { + mockIsConnectorStepReadyToComplete.mockReturnValue(true); mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 2 }); }); @@ -110,14 +216,109 @@ describe('CreateIntegration', () => { expect(result.queryByTestId('integrationStepMock')).toBeInTheDocument(); }); - it('should call isIntegrationStepReady', () => { + it('should call isIntegrationStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsIntegrationStepReady).toHaveBeenCalled(); + expect(mockIsIntegrationStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show "Next" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Next'); + }); + + describe('when integration step is not done', () => { + beforeEach(() => { + mockIsIntegrationStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); + }); + + it('should still enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should still enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + }); + + describe('when integration step is done', () => { + beforeEach(() => { + mockIsIntegrationStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for integration step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 2, + stepName: 'Integration Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + }); + }); + + describe('when back button is clicked', () => { + let result: ReturnType; + beforeEach(() => { + result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-backButton').click(); + }); + }); + + it('should not report telemetry', () => { + expect(mockReportEvent).not.toHaveBeenCalled(); + }); + + it('should show connector step', () => { + expect(result.queryByTestId('connectorStepMock')).toBeInTheDocument(); + }); + + it('should enable the next button', () => { + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); }); }); describe('when step is 3', () => { beforeEach(() => { + mockIsConnectorStepReadyToComplete.mockReturnValue(true); + mockIsIntegrationStepReadyToComplete.mockReturnValue(true); mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 3 }); }); @@ -126,9 +327,116 @@ describe('CreateIntegration', () => { expect(result.queryByTestId('dataStreamStepMock')).toBeInTheDocument(); }); - it('should call isDataStreamStepReady', () => { + it('should call isDataStreamStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsDataStreamStepReady).toHaveBeenCalled(); + expect(mockIsDataStreamStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show "Analyze logs" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Analyze logs'); + }); + + describe('when data stream step is not done', () => { + beforeEach(() => { + mockIsDataStreamStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); + }); + + it('should still enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should still enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + }); + + describe('when data stream step is done', () => { + beforeEach(() => { + mockIsDataStreamStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for data stream step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 3, + stepName: 'DataStream Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + + it('should show loader on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('generatingLoader')).toBeInTheDocument(); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + // Not sure why there are two buttons when testing. + const nextButton = result + .getAllByTestId('buttonsFooter-nextButton') + .filter((button) => button.textContent !== 'Next')[0]; + expect(nextButton).toBeDisabled(); + }); + }); + }); + + describe('when back button is clicked', () => { + let result: ReturnType; + beforeEach(() => { + result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-backButton').click(); + }); + }); + + it('should not report telemetry', () => { + expect(mockReportEvent).not.toHaveBeenCalled(); + }); + + it('should show integration step', () => { + expect(result.queryByTestId('integrationStepMock')).toBeInTheDocument(); + }); + + it('should enable the next button', () => { + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); }); }); @@ -142,9 +450,89 @@ describe('CreateIntegration', () => { expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument(); }); - it('should call isReviewStepReady', () => { + it('should call isReviewStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsReviewStepReady).toHaveBeenCalled(); + expect(mockIsReviewStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show the "Add to Elastic" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Add to Elastic'); + }); + + describe('when review step is not done', () => { + beforeEach(() => { + mockIsReviewStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); + }); + + it('should still enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should still enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + }); + + describe('when review step is done', () => { + beforeEach(() => { + mockIsReviewStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for review step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 4, + stepName: 'Review Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + + it('should show deploy step', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + }); }); }); @@ -157,6 +545,26 @@ describe('CreateIntegration', () => { const result = renderIntegrationAssistant(); expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); }); + + it('should hide the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should hide the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + it('should show "Close" on the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close'); + }); }); }); @@ -179,9 +587,107 @@ describe('CreateIntegration with generateCel enabled', () => { expect(result.queryByTestId('celInputStepMock')).toBeInTheDocument(); }); - it('should call isCelInputStepReady', () => { + it('should call isCelInputStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsCelInputStepReady).toHaveBeenCalled(); + expect(mockIsCelInputStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show "Generate CEL input configuration" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent( + 'Generate CEL input configuration' + ); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + describe('when cel input step is not done', () => { + beforeEach(() => { + mockIsCelInputStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + // Not sure why there are two buttons when testing. + const nextButton = result + .getAllByTestId('buttonsFooter-nextButton') + .filter((button) => button.textContent !== 'Next')[0]; + expect(nextButton).toBeDisabled(); + }); + }); + + describe('when cel input step is done', () => { + beforeEach(() => { + mockIsCelInputStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for cel input step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 5, + stepName: 'CEL Input Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + + it('should show loader on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('generatingLoader')).toBeInTheDocument(); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + // Not sure why there are two buttons when testing. + const nextButton = result + .getAllByTestId('buttonsFooter-nextButton') + .filter((button) => button.textContent !== 'Next')[0]; + expect(nextButton).toBeDisabled(); + }); + }); + }); + + describe('when back button is clicked', () => { + let result: ReturnType; + beforeEach(() => { + result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-backButton').click(); + }); + }); + + it('should not report telemetry', () => { + expect(mockReportEvent).not.toHaveBeenCalled(); + }); + + it('should show review step', () => { + expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument(); + }); + + it('should enable the next button', () => { + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); }); }); @@ -194,6 +700,26 @@ describe('CreateIntegration with generateCel enabled', () => { const result = renderIntegrationAssistant(); expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); }); + + it('should hide the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should hide the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + it('should show "Close" on the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close'); + }); }); describe('when step is 6', () => { @@ -210,9 +736,89 @@ describe('CreateIntegration with generateCel enabled', () => { expect(result.queryByTestId('reviewCelStepMock')).toBeInTheDocument(); }); - it('should call isReviewCelStepReady', () => { + it('should call isReviewCelStepReadyToComplete', () => { renderIntegrationAssistant(); - expect(mockIsCelReviewStepReady).toHaveBeenCalled(); + expect(mockIsCelReviewStepReadyToComplete).toHaveBeenCalled(); + }); + + it('should show the "Add to Elastic" on the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Add to Elastic'); + }); + + describe('when cel review step is not done', () => { + beforeEach(() => { + mockIsCelReviewStepReadyToComplete.mockReturnValue(false); + }); + + it('should disable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); + }); + + it('should still enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should still enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + }); + + describe('when cel review step is done', () => { + beforeEach(() => { + mockIsCelReviewStepReadyToComplete.mockReturnValue(true); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + + it('should enable the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + describe('when next button is clicked', () => { + beforeEach(() => { + const result = renderIntegrationAssistant(); + mockReportEvent.mockClear(); + act(() => { + result.getByTestId('buttonsFooter-nextButton').click(); + }); + }); + + it('should report telemetry for review step completion', () => { + expect(mockReportEvent).toHaveBeenCalledWith( + TelemetryEventType.IntegrationAssistantStepComplete, + { + sessionId: expect.any(String), + step: 6, + stepName: 'CEL Review Step', + durationMs: expect.any(Number), + sessionElapsedTime: expect.any(Number), + } + ); + }); + + it('should show deploy step', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); + }); + + it('should enable the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); + }); + }); }); }); @@ -225,5 +831,25 @@ describe('CreateIntegration with generateCel enabled', () => { const result = renderIntegrationAssistant(); expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); }); + + it('should hide the back button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should hide the next button', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); + }); + + it('should enable the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); + }); + + it('should show "Close" on the cancel button', () => { + const result = renderIntegrationAssistant(); + expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close'); + }); }); }); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx index 72e085e19920a..81cb5a9b3b137 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx @@ -5,31 +5,96 @@ * 2.0. */ -import React, { useReducer, useMemo, useEffect } from 'react'; +import React, { useReducer, useMemo, useEffect, useCallback } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { Header } from './header'; import { Footer } from './footer'; -import { ConnectorStep, isConnectorStepReady } from './steps/connector_step'; -import { IntegrationStep, isIntegrationStepReady } from './steps/integration_step'; -import { DataStreamStep, isDataStreamStepReady } from './steps/data_stream_step'; -import { ReviewStep, isReviewStepReady } from './steps/review_step'; -import { CelInputStep, isCelInputStepReady } from './steps/cel_input_step'; -import { ReviewCelStep, isCelReviewStepReady } from './steps/review_cel_step'; +import { useNavigate, Page } from '../../../common/hooks/use_navigate'; +import { ConnectorStep, isConnectorStepReadyToComplete } from './steps/connector_step'; +import { IntegrationStep, isIntegrationStepReadyToComplete } from './steps/integration_step'; +import { DataStreamStep, isDataStreamStepReadyToComplete } from './steps/data_stream_step'; +import { ReviewStep, isReviewStepReadyToComplete } from './steps/review_step'; +import { CelInputStep, isCelInputStepReadyToComplete } from './steps/cel_input_step'; +import { ReviewCelStep, isCelReviewStepReadyToComplete } from './steps/review_cel_step'; import { DeployStep } from './steps/deploy_step'; import { reducer, initialState, ActionsProvider, type Actions } from './state'; import { useTelemetry } from '../telemetry'; import { ExperimentalFeaturesService } from '../../../services'; +const stepNames: Record = { + 1: 'Connector Step', + 2: 'Integration Step', + 3: 'DataStream Step', + 4: 'Review Step', + cel_input: 'CEL Input Step', + cel_review: 'CEL Review Step', + deploy: 'Deploy Step', +}; + export const CreateIntegrationAssistant = React.memo(() => { const [state, dispatch] = useReducer(reducer, initialState); - + const navigate = useNavigate(); const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get(); + const celInputStepIndex = isGenerateCelEnabled && state.hasCelInput ? 5 : null; + const celReviewStepIndex = isGenerateCelEnabled && state.celInputResult ? 6 : null; + const deployStepIndex = + celInputStepIndex !== null || celReviewStepIndex !== null || state.step === 7 ? 7 : 5; + + const stepName = + state.step === deployStepIndex + ? stepNames.deploy + : state.step === celReviewStepIndex + ? stepNames.cel_review + : state.step === celInputStepIndex + ? stepNames.cel_input + : state.step in stepNames + ? stepNames[state.step] + : 'Unknown Step'; + const telemetry = useTelemetry(); useEffect(() => { telemetry.reportAssistantOpen(); }, [telemetry]); + const isThisStepReadyToComplete = useMemo(() => { + if (state.step === 1) { + return isConnectorStepReadyToComplete(state); + } else if (state.step === 2) { + return isIntegrationStepReadyToComplete(state); + } else if (state.step === 3) { + return isDataStreamStepReadyToComplete(state); + } else if (state.step === 4) { + return isReviewStepReadyToComplete(state); + } else if (isGenerateCelEnabled && state.step === 5) { + return isCelInputStepReadyToComplete(state); + } else if (isGenerateCelEnabled && state.step === 6) { + return isCelReviewStepReadyToComplete(state); + } + return false; + }, [state, isGenerateCelEnabled]); + + const goBackStep = useCallback(() => { + if (state.step === 1) { + navigate(Page.landing); + } else { + dispatch({ type: 'SET_STEP', payload: state.step - 1 }); + } + }, [navigate, dispatch, state.step]); + + const completeStep = useCallback(() => { + if (!isThisStepReadyToComplete) { + // If the user tries to navigate to the next step without completing the current step. + return; + } + telemetry.reportAssistantStepComplete({ step: state.step, stepName }); + if (state.step === 3 || state.step === celInputStepIndex) { + dispatch({ type: 'SET_IS_GENERATING', payload: true }); + } else { + dispatch({ type: 'SET_STEP', payload: state.step + 1 }); + } + }, [telemetry, state.step, stepName, celInputStepIndex, isThisStepReadyToComplete]); + const actions = useMemo( () => ({ setStep: (payload) => { @@ -53,27 +118,11 @@ export const CreateIntegrationAssistant = React.memo(() => { setCelInputResult: (payload) => { dispatch({ type: 'SET_CEL_INPUT_RESULT', payload }); }, + completeStep, }), - [] + [completeStep] ); - const isNextStepEnabled = useMemo(() => { - if (state.step === 1) { - return isConnectorStepReady(state); - } else if (state.step === 2) { - return isIntegrationStepReady(state); - } else if (state.step === 3) { - return isDataStreamStepReady(state); - } else if (state.step === 4) { - return isReviewStepReady(state); - } else if (isGenerateCelEnabled && state.step === 5) { - return isCelInputStepReady(state); - } else if (isGenerateCelEnabled && state.step === 6) { - return isCelReviewStepReady(state); - } - return false; - }, [state, isGenerateCelEnabled]); - return ( @@ -95,28 +144,21 @@ export const CreateIntegrationAssistant = React.memo(() => { result={state.result} /> )} - {state.step === 5 && - (isGenerateCelEnabled && state.hasCelInput ? ( - - ) : ( - - ))} - - {isGenerateCelEnabled && state.celInputResult && state.step === 6 && ( + {state.step === celInputStepIndex && ( + + )} + {state.step === celReviewStepIndex && ( )} - {isGenerateCelEnabled && state.step === 7 && ( + + {state.step === deployStepIndex && ( { )}