diff --git a/public/components/__tests__/edit_conversation_name_modal.test.tsx b/public/components/__tests__/edit_conversation_name_modal.test.tsx new file mode 100644 index 00000000..5f1838ab --- /dev/null +++ b/public/components/__tests__/edit_conversation_name_modal.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; + +import { coreMock } from '../../../../../src/core/public/mocks'; +import * as coreContextExports from '../../contexts/core_context'; + +import { + EditConversationNameModal, + EditConversationNameModalProps, +} from '../edit_conversation_name_modal'; +import { HttpHandler } from '../../../../../src/core/public'; + +const setup = ({ onClose, defaultTitle, sessionId }: EditConversationNameModalProps) => { + const useCoreMock = { + services: coreMock.createStart(), + }; + jest.spyOn(coreContextExports, 'useCore').mockReturnValue(useCoreMock); + + const renderResult = render( + + + + ); + + return { + useCoreMock, + renderResult, + }; +}; + +describe('', () => { + it('should render default title in name input', async () => { + const { renderResult } = setup({ + sessionId: '1', + defaultTitle: 'foo', + }); + + await waitFor(async () => { + expect(renderResult.getByLabelText('Conversation name input').getAttribute('value')).toBe( + 'foo' + ); + }); + }); + + it('should call onClose with "canceled" after cancel button click', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + defaultTitle: 'foo', + onClose: onCloseMock, + }); + + act(() => { + fireEvent.change(renderResult.getByLabelText('Conversation name input'), { + target: { + value: 'bar', + }, + }); + }); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalCancelButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('cancelled'); + }); + }); + + it('should show success toast and call onClose with "updated" after patch session succeed', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + defaultTitle: 'foo', + onClose: onCloseMock, + }); + useCoreMock.services.http.put.mockImplementation(() => Promise.resolve()); + + act(() => { + fireEvent.change(renderResult.getByLabelText('Conversation name input'), { + target: { + value: 'bar', + }, + }); + }); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('updated', 'bar'); + expect(useCoreMock.services.notifications.toasts.addSuccess).toHaveBeenLastCalledWith( + 'This conversation was successfully updated.' + ); + }); + }); + + it('should show error toasts and call onClose with "errored" after failed patch session', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + defaultTitle: 'foo', + onClose: onCloseMock, + }); + useCoreMock.services.http.put.mockImplementation(() => Promise.reject(new Error())); + + act(() => { + fireEvent.change(renderResult.getByLabelText('Conversation name input'), { + target: { + value: 'bar', + }, + }); + }); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('errored'); + expect(useCoreMock.services.notifications.toasts.addDanger).toHaveBeenLastCalledWith( + 'There was an error. The name failed to update.' + ); + }); + }); + + it('should call onClose with cancelled after patch session aborted', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + defaultTitle: 'foo', + onClose: onCloseMock, + }); + useCoreMock.services.http.put.mockImplementation(((_path, options) => { + return new Promise((_resolve, reject) => { + if (options?.signal) { + options.signal.onabort = () => { + reject(new Error('Aborted')); + }; + } + }); + }) as HttpHandler); + + act(() => { + fireEvent.change(renderResult.getByLabelText('Conversation name input'), { + target: { + value: 'bar', + }, + }); + }); + + expect(onCloseMock).not.toHaveBeenCalled(); + expect(useCoreMock.services.http.put).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + expect(useCoreMock.services.http.put).toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalCancelButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('cancelled'); + expect(useCoreMock.services.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + expect(useCoreMock.services.notifications.toasts.addDanger).not.toHaveBeenCalled(); + expect(useCoreMock.services.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/public/components/edit_conversation_name_modal.tsx b/public/components/edit_conversation_name_modal.tsx index e408be9b..9ef804b6 100644 --- a/public/components/edit_conversation_name_modal.tsx +++ b/public/components/edit_conversation_name_modal.tsx @@ -6,10 +6,10 @@ import React, { useCallback, useRef } from 'react'; import { EuiConfirmModal, EuiFieldText, EuiSpacer, EuiText } from '@elastic/eui'; -import { usePatchSession } from '../hooks/use_sessions'; import { useCore } from '../contexts/core_context'; +import { usePatchSession } from '../hooks'; -interface EditConversationNameModalProps { +export interface EditConversationNameModalProps { onClose?: (status: 'updated' | 'cancelled' | 'errored', newTitle?: string) => void; sessionId: string; defaultTitle: string; diff --git a/public/hooks/index.ts b/public/hooks/index.ts index 05e7214c..346ced32 100644 --- a/public/hooks/index.ts +++ b/public/hooks/index.ts @@ -6,3 +6,4 @@ export { useSaveChat } from './use_save_chat'; export { useChatState, ChatStateProvider } from './use_chat_state'; export { useChatActions } from './use_chat_actions'; +export { usePatchSession, useDeleteSession } from './use_sessions'; diff --git a/public/tabs/history/__tests__/delete_conversation_confirm_modal.test.tsx b/public/tabs/history/__tests__/delete_conversation_confirm_modal.test.tsx new file mode 100644 index 00000000..7060ecdb --- /dev/null +++ b/public/tabs/history/__tests__/delete_conversation_confirm_modal.test.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import * as coreContextExports from '../../../contexts/core_context'; + +import { + DeleteConversationConfirmModal, + DeleteConversationConfirmModalProps, +} from '../delete_conversation_confirm_modal'; +import { HttpHandler } from '../../../../../../src/core/public'; + +const setup = ({ onClose, sessionId }: DeleteConversationConfirmModalProps) => { + const useCoreMock = { + services: coreMock.createStart(), + }; + jest.spyOn(coreContextExports, 'useCore').mockReturnValue(useCoreMock); + + const renderResult = render( + + + + ); + + return { + useCoreMock, + renderResult, + }; +}; + +describe('', () => { + it('should render confirm text and button', async () => { + const { renderResult } = setup({ + sessionId: '1', + }); + + await waitFor(async () => { + expect( + renderResult.getByText( + 'Are you sure you want to delete the conversation? After it’s deleted, the conversation details will not be accessible.' + ) + ).toBeTruthy(); + expect(renderResult.getByRole('button', { name: 'Delete conversation' })).toBeTruthy(); + expect(renderResult.getByRole('button', { name: 'Cancel' })).toBeTruthy(); + }); + }); + + it('should call onClose with "canceled" after cancel button click', async () => { + const onCloseMock = jest.fn(); + const { renderResult } = setup({ + sessionId: '1', + onClose: onCloseMock, + }); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalCancelButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('cancelled'); + }); + }); + + it('should show success toast and call onClose with "deleted" after delete session succeed', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + onClose: onCloseMock, + }); + useCoreMock.services.http.delete.mockImplementation(() => Promise.resolve()); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('deleted'); + expect(useCoreMock.services.notifications.toasts.addSuccess).toHaveBeenLastCalledWith( + 'The conversation was successfully deleted.' + ); + }); + }); + + it('should show error toasts and call onClose with "errored" after delete session failed', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + onClose: onCloseMock, + }); + useCoreMock.services.http.delete.mockImplementation(() => Promise.reject(new Error())); + + expect(onCloseMock).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('errored'); + }); + }); + + it('should call onClose with cancelled after delete session aborted', async () => { + const onCloseMock = jest.fn(); + const { renderResult, useCoreMock } = setup({ + sessionId: '1', + onClose: onCloseMock, + }); + useCoreMock.services.http.delete.mockImplementation(((_path, options) => { + return new Promise((_resolve, reject) => { + if (options?.signal) { + options.signal.onabort = () => { + reject(new Error('Aborted')); + }; + } + }); + }) as HttpHandler); + + expect(onCloseMock).not.toHaveBeenCalled(); + expect(useCoreMock.services.http.delete).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + expect(useCoreMock.services.http.delete).toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalCancelButton')); + }); + + await waitFor(() => { + expect(onCloseMock).toHaveBeenLastCalledWith('cancelled'); + expect(useCoreMock.services.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + expect(useCoreMock.services.notifications.toasts.addDanger).not.toHaveBeenCalled(); + expect(useCoreMock.services.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/public/tabs/history/delete_conversation_confirm_modal.tsx b/public/tabs/history/delete_conversation_confirm_modal.tsx index e3b6952c..05b205de 100644 --- a/public/tabs/history/delete_conversation_confirm_modal.tsx +++ b/public/tabs/history/delete_conversation_confirm_modal.tsx @@ -7,11 +7,11 @@ import React, { useCallback } from 'react'; import { EuiConfirmModal, EuiText } from '@elastic/eui'; -import { useDeleteSession } from '../../hooks/use_sessions'; +import { useDeleteSession } from '../../hooks'; import { useCore } from '../../contexts/core_context'; -interface DeleteConversationConfirmModalProps { - onClose?: (status: 'canceled' | 'errored' | 'deleted') => void; +export interface DeleteConversationConfirmModalProps { + onClose?: (status: 'cancelled' | 'errored' | 'deleted') => void; sessionId: string; } @@ -28,7 +28,7 @@ export const DeleteConversationConfirmModal = ({ const handleCancel = useCallback(() => { abort(); - onClose?.('canceled'); + onClose?.('cancelled'); }, [onClose, abort]); const handleConfirm = useCallback(async () => { try {