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 {