Skip to content

Commit

Permalink
test: add unit tests for edit and delete modal (#50)
Browse files Browse the repository at this point in the history
* feat: add unit tests for edit and delete modal

Signed-off-by: Lin Wang <wonglam@amazon.com>

* fix: abort not happend in unit tests

Signed-off-by: Lin Wang <wonglam@amazon.com>

---------

Signed-off-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
wanglam authored Dec 11, 2023
1 parent 45ee7fd commit c70bcfc
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 6 deletions.
188 changes: 188 additions & 0 deletions public/components/__tests__/edit_conversation_name_modal.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<I18nProvider>
<EditConversationNameModal
onClose={onClose}
sessionId={sessionId}
defaultTitle={defaultTitle}
/>
</I18nProvider>
);

return {
useCoreMock,
renderResult,
};
};

describe('<EditConversationNameModal />', () => {
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();
});
});
});
4 changes: 2 additions & 2 deletions public/components/edit_conversation_name_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions public/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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(
<I18nProvider>
<DeleteConversationConfirmModal onClose={onClose} sessionId={sessionId} />
</I18nProvider>
);

return {
useCoreMock,
renderResult,
};
};

describe('<DeleteConversationConfirmModal />', () => {
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();
});
});
});
8 changes: 4 additions & 4 deletions public/tabs/history/delete_conversation_confirm_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -28,7 +28,7 @@ export const DeleteConversationConfirmModal = ({

const handleCancel = useCallback(() => {
abort();
onClose?.('canceled');
onClose?.('cancelled');
}, [onClose, abort]);
const handleConfirm = useCallback(async () => {
try {
Expand Down

0 comments on commit c70bcfc

Please sign in to comment.