Skip to content

Commit

Permalink
add unit tests for chat window header (#63)
Browse files Browse the repository at this point in the history
* test: add unit tests for chat window header

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

* test: add ut for chat window header title

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

* test: add unit tests for chat experimental badge

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

* Address PR comments

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

---------

Signed-off-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
wanglam authored Dec 13, 2023
1 parent 1670183 commit 540bdb7
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 5 deletions.
53 changes: 53 additions & 0 deletions public/components/__tests__/chat_experimental_badge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { ChatExperimentalBadge } from '../chat_experimental_badge';

describe('<ChatWindowHeaderTitle />', () => {
it('should show experimental dropdown after icon clicked', () => {
const { getByRole, getByText, queryByText } = render(
<I18nProvider>
<ChatExperimentalBadge />
</I18nProvider>
);

expect(queryByText('Experimental')).not.toBeInTheDocument();
fireEvent.click(getByRole('button'));
expect(getByText('Experimental')).toBeInTheDocument();
});

it('should hide experimental dropdown after click other places', async () => {
const { getByRole, getByText, queryByText } = render(
<I18nProvider>
<ChatExperimentalBadge />
</I18nProvider>
);

act(() => {
fireEvent.click(getByRole('button'));
});

await waitFor(() => {
expect(getByText('Experimental')).toBeInTheDocument();

// Ensure focus trap enabled, then we can click outside.
expect(
getByText('Experimental').closest('div[data-focus-lock-disabled="false"]')
).toBeInTheDocument();
});

act(() => {
fireEvent.mouseDown(document.body);
});

await waitFor(() => {
expect(queryByText('Experimental')).not.toBeInTheDocument();
});
});
});
113 changes: 110 additions & 3 deletions public/components/__tests__/chat_window_header_title.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ import * as useChatActionsExports from '../../hooks/use_chat_actions';
import * as useSaveChatExports from '../../hooks/use_save_chat';
import * as chatContextExports from '../../contexts/chat_context';
import * as coreContextExports from '../../contexts/core_context';
import { IMessage } from '../../../common/types/chat_saved_object_attributes';

import { ChatWindowHeaderTitle } from '../chat_window_header_title';

const setup = () => {
const setup = ({
messages = [],
...rest
}: { messages?: IMessage[]; sessionId?: string | undefined } = {}) => {
const useCoreMock = {
services: {
...coreMock.createStart(),
Expand All @@ -38,10 +42,10 @@ const setup = () => {
useCoreMock.services.http.put.mockImplementation(() => Promise.resolve());

const useChatStateMock = {
chatState: { messages: [] },
chatState: { messages },
};
const useChatContextMock = {
sessionId: '1',
sessionId: 'sessionId' in rest ? rest.sessionId : '1',
title: 'foo',
setSessionId: jest.fn(),
setTitle: jest.fn(),
Expand All @@ -68,6 +72,7 @@ const setup = () => {
useCoreMock,
useChatStateMock,
useChatContextMock,
useChatActionsMock,
renderResult,
};
};
Expand Down Expand Up @@ -100,4 +105,106 @@ describe('<ChatWindowHeaderTitle />', () => {
expect(useCoreMock.services.sessions.reload).toHaveBeenCalled();
});
});

it('should show "Rename conversation", "New conversation" and "Save to notebook" actions after title click', () => {
const { renderResult } = setup();

expect(
renderResult.queryByRole('button', { name: 'Rename conversation' })
).not.toBeInTheDocument();
expect(
renderResult.queryByRole('button', { name: 'New conversation' })
).not.toBeInTheDocument();
expect(
renderResult.queryByRole('button', { name: 'Save to notebook' })
).not.toBeInTheDocument();

act(() => {
fireEvent.click(renderResult.getByText('foo'));
});

expect(renderResult.getByRole('button', { name: 'Rename conversation' })).toBeInTheDocument();
expect(renderResult.getByRole('button', { name: 'New conversation' })).toBeInTheDocument();
expect(renderResult.getByRole('button', { name: 'Save to notebook' })).toBeInTheDocument();
});

it('should show rename modal and hide rename actions after rename button clicked', async () => {
const { renderResult } = setup();

act(() => {
fireEvent.click(renderResult.getByText('foo'));
});

act(() => {
fireEvent.click(renderResult.getByRole('button', { name: 'Rename conversation' }));
});

await waitFor(() => {
expect(renderResult.getByText('Edit conversation name')).toBeInTheDocument();
expect(
renderResult.queryByRole('button', { name: 'Rename conversation' })
).not.toBeInTheDocument();
});
});

it('should call loadChat with undefined, hide actions and show success toasts after new conversation button clicked', async () => {
const { renderResult, useCoreMock, useChatActionsMock } = setup();

act(() => {
fireEvent.click(renderResult.getByText('foo'));
});

expect(useChatActionsMock.loadChat).not.toHaveBeenCalled();
expect(useCoreMock.services.notifications.toasts.addSuccess).not.toHaveBeenCalled();

act(() => {
fireEvent.click(renderResult.getByRole('button', { name: 'New conversation' }));
});

await waitFor(() => {
expect(useChatActionsMock.loadChat).toHaveBeenCalledWith(undefined);
expect(
renderResult.queryByRole('button', { name: 'New conversation' })
).not.toBeInTheDocument();
expect(useCoreMock.services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
'A new conversation is started and the previous one is saved.'
);
});
});

it('should show save to notebook modal after "Save to notebook" clicked', async () => {
const { renderResult } = setup();

act(() => {
fireEvent.click(renderResult.getByText('foo'));
});

act(() => {
fireEvent.click(renderResult.getByRole('button', { name: 'Save to notebook' }));
});

await waitFor(() => {
expect(renderResult.queryByText('Save to notebook')).toBeInTheDocument();
});
});

it('should disable "Save to notebook" button when message does not include input', async () => {
const { renderResult } = setup({
messages: [{ type: 'output', content: 'bar', contentType: 'markdown' }],
});

act(() => {
fireEvent.click(renderResult.getByText('foo'));
});

expect(renderResult.getByRole('button', { name: 'Save to notebook' })).toBeDisabled();
});

it('should show "OpenSearch Assistant" when sessionId is undefined', async () => {
const { renderResult } = setup({
sessionId: undefined,
});

expect(renderResult.getByText('OpenSearch Assistant')).toBeInTheDocument();
});
});
9 changes: 8 additions & 1 deletion public/components/chat_experimental_badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ export const ChatExperimentalBadge = ({ onClick }: ChatExperimentalBadgeProps) =
return (
<EuiPopover
isOpen={visible}
button={<EuiButtonIcon color="text" iconType="beaker" onClick={handleIconClick} />}
button={
<EuiButtonIcon
color="text"
iconType="beaker"
onClick={handleIconClick}
aria-label="Experimental badge"
/>
}
closePopover={closePopover}
onClick={onClick}
>
Expand Down
109 changes: 109 additions & 0 deletions public/tabs/__tests__/chat_window_header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';

import { ChatWindowHeader, ChatWindowHeaderProps } from '../chat_window_header';
import * as chatContextExports from '../../contexts/chat_context';
import { TabId } from '../../types';

jest.mock('../../components/chat_window_header_title', () => {
return { ChatWindowHeaderTitle: () => <div>OpenSearch Assistant</div> };
});

const setup = ({
selectedTabId,
...props
}: Partial<ChatWindowHeaderProps> & { selectedTabId?: TabId } = {}) => {
const useChatContextMock = {
sessionId: '1',
title: 'foo',
selectedTabId: selectedTabId || 'chat',
setSessionId: jest.fn(),
setTitle: jest.fn(),
setFlyoutVisible: jest.fn(),
setSelectedTabId: jest.fn(),
setFlyoutComponent: jest.fn(),
};
jest.spyOn(chatContextExports, 'useChatContext').mockReturnValue(useChatContextMock);
const renderResult = render(
<ChatWindowHeader flyoutFullScreen={false} toggleFlyoutFullScreen={jest.fn()} {...props} />
);

return {
renderResult,
useChatContextMock,
};
};

describe('<ChatWindowHeader />', () => {
it('should render title, history, fullscreen and close button', () => {
const { renderResult } = setup();

expect(renderResult.getByText('OpenSearch Assistant')).toBeInTheDocument();
expect(renderResult.getByLabelText('history')).toBeInTheDocument();
expect(renderResult.getByLabelText('fullScreen')).toBeInTheDocument();
expect(renderResult.getByLabelText('close')).toBeInTheDocument();
});

it('should call setFlyoutVisible with false after close button clicked', () => {
const { renderResult, useChatContextMock } = setup();

expect(useChatContextMock.setFlyoutVisible).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('close'));
});
expect(useChatContextMock.setFlyoutVisible).toHaveBeenLastCalledWith(false);
});

it('should call setFlyoutComponent with undefined after history button click', () => {
const { renderResult, useChatContextMock } = setup();

expect(useChatContextMock.setFlyoutComponent).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('history'));
});
expect(useChatContextMock.setFlyoutComponent).toHaveBeenLastCalledWith(undefined);
});

it('should call setSelectedTabId with "chat" when selectedTabId is "history"', () => {
const { renderResult, useChatContextMock } = setup({
selectedTabId: 'history',
});

expect(useChatContextMock.setSelectedTabId).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('history'));
});
expect(useChatContextMock.setSelectedTabId).toHaveBeenLastCalledWith('chat');
});

it('should call setSelectedTabId with "history" when selectedTabId is "chat"', () => {
const { renderResult, useChatContextMock } = setup({
selectedTabId: 'chat',
});

expect(useChatContextMock.setSelectedTabId).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('history'));
});
expect(useChatContextMock.setSelectedTabId).toHaveBeenLastCalledWith('history');
});

it('should call toggleFullScreen after fullScreen clicked', () => {
const toggleFlyoutFullScreenMock = jest.fn();
const { renderResult } = setup({
flyoutFullScreen: true,
toggleFlyoutFullScreen: toggleFlyoutFullScreenMock,
});

expect(toggleFlyoutFullScreenMock).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('fullScreen'));
});
expect(toggleFlyoutFullScreenMock).toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion public/tabs/chat_window_header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useChatContext } from '../contexts/chat_context';
import { ChatWindowHeaderTitle } from '../components/chat_window_header_title';
import chatIcon from '../assets/chat.svg';
import { TAB_ID } from '../utils/constants';
interface ChatWindowHeaderProps {

export interface ChatWindowHeaderProps {
flyoutFullScreen: boolean;
toggleFlyoutFullScreen: () => void;
}
Expand Down

0 comments on commit 540bdb7

Please sign in to comment.