Skip to content

Commit

Permalink
Add tests for ChatPageContent component (#54)
Browse files Browse the repository at this point in the history
* Add tests for ChatPageContent component

add MessageBubble unit tests
add MessageContent unit tests
remove MessageFooter which is not needed anymore

---------

Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl authored Dec 11, 2023
1 parent fa7d6ff commit 3d46971
Show file tree
Hide file tree
Showing 8 changed files with 565 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ interface Props {
username: string;
}

export const TermsAndConditions = (props: Props) => {
export const WelcomeMessage = (props: Props) => {
return (
<EuiEmptyPrompt
aria-label="chat welcome message"
iconType="cheer"
iconColor="primary"
titleSize="s"
Expand Down
243 changes: 243 additions & 0 deletions public/tabs/chat/chat_page_content.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ChatPageContent } from './chat_page_content';
import * as chatContextExports from '../../contexts/chat_context';
import * as chatStateHookExports from '../../hooks/use_chat_state';
import * as chatActionHookExports from '../../hooks/use_chat_actions';
import { IMessage } from '../../../common/types/chat_saved_object_attributes';

jest.mock('./messages/message_bubble', () => {
return {
MessageBubble: ({ children }: { children?: React.ReactNode }) => (
<div aria-label="chat message bubble">{children}</div>
),
};
});

jest.mock('./messages/message_content', () => {
return { MessageContent: () => <div /> };
});

describe('<ChatPageContent />', () => {
const abortActionMock = jest.fn();
const executeActionMock = jest.fn();

beforeEach(() => {
jest.spyOn(chatContextExports, 'useChatContext').mockReturnValue({
sessionId: 'test_session_id',
actionExecutors: {
view_ppl_visualization: jest.fn(),
},
currentAccount: {
username: 'test_user',
tenant: 'private',
},
});

jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages: [], llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});

jest.spyOn(chatActionHookExports, 'useChatActions').mockReturnValue({
regenerate: jest.fn(),
send: jest.fn(),
loadChat: jest.fn(),
openChatUI: jest.fn(),
executeAction: executeActionMock,
abortAction: abortActionMock,
});
});

afterEach(() => {
jest.resetAllMocks();
});

afterAll(() => {
jest.restoreAllMocks();
});

it('should display welcome message by default', () => {
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat message bubble')).toHaveLength(1);
expect(screen.queryByLabelText('chat welcome message')).toBeInTheDocument();
});

it('should display a default suggested action', () => {
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat suggestions')).toHaveLength(1);
expect(screen.queryByText('What are the indices in my cluster?')).toBeInTheDocument();
});

it('should display messages', () => {
const messages: IMessage[] = [
{
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
},
{
type: 'output',
content: 'here are the indices in your cluster: .alert',
contentType: 'markdown',
suggestedActions: [{ actionType: 'send_as_input', message: 'suggested action mock' }],
},
];
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages, llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat message bubble')).toHaveLength(3);
});

it('should only display the suggested actions of last output', () => {
const messages: IMessage[] = [
{
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
},
{
type: 'output',
content: 'here are the indices in your cluster: .kibana',
contentType: 'markdown',
suggestedActions: [{ actionType: 'send_as_input', message: 'suggested action mock' }],
},
{
type: 'input',
content: 'Are there any alerts in my system?',
contentType: 'text',
},
{
type: 'output',
content: 'there is no alert in the system',
contentType: 'markdown',
suggestedActions: [{ actionType: 'send_as_input', message: 'suggested action mock' }],
},
];
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages, llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat suggestions')).toHaveLength(1);
expect(screen.queryByText('suggested action mock')).toBeInTheDocument();
});

it('should NOT display the suggested actions if no suggested actions', () => {
const messages: IMessage[] = [
{
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
},
{
type: 'output',
content: 'here are the indices in your cluster: .kibana',
contentType: 'markdown',
suggestedActions: [],
},
];
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages, llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat suggestions')).toHaveLength(0);
});

it('should not display suggested actions on user input message bubble', () => {
const messages: IMessage[] = [
{
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
},
{
type: 'output',
content: 'here are the indices in your cluster: .kibana',
contentType: 'markdown',
suggestedActions: [{ actionType: 'send_as_input', message: 'suggested action mock' }],
},
{
type: 'input',
content: 'show me visualizations about sales',
contentType: 'text',
},
];
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages, llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryAllByLabelText('chat suggestions')).toHaveLength(0);
});

it('should display loading screen when loading the messages', () => {
render(<ChatPageContent messagesLoading={true} onRefresh={jest.fn()} />);
expect(screen.queryByText('Loading conversation')).toBeInTheDocument();
expect(screen.queryAllByLabelText('chat message bubble')).toHaveLength(0);
});

it('should show error message with refresh button', () => {
const onRefreshMock = jest.fn();
render(
<ChatPageContent
messagesLoading={false}
messagesLoadingError={new Error('failed to get response')}
onRefresh={onRefreshMock}
/>
);
expect(screen.queryByText('failed to get response')).toBeInTheDocument();
expect(screen.queryAllByLabelText('chat message bubble')).toHaveLength(0);

fireEvent.click(screen.getByText('Refresh'));
expect(onRefreshMock).toHaveBeenCalled();
});

it('should display `Stop generating response` when llm is responding', () => {
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages: [], llmResponding: true, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryByText('Stop generating response')).toBeInTheDocument();
fireEvent.click(screen.getByText('Stop generating response'));
expect(abortActionMock).toHaveBeenCalledWith('test_session_id');
});

it('should display `How was this generated?`', () => {
const messages: IMessage[] = [
{
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
},
{
type: 'output',
content: 'here are the indices in your cluster: .kibana',
contentType: 'markdown',
suggestedActions: [],
traceId: 'trace_id_mock',
},
];
jest.spyOn(chatStateHookExports, 'useChatState').mockReturnValue({
chatState: { messages, llmResponding: false, interactions: [] },
chatStateDispatch: jest.fn(),
});
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
expect(screen.queryByText('How was this generated?')).toBeInTheDocument();
});

it('should call executeAction', () => {
render(<ChatPageContent messagesLoading={false} onRefresh={jest.fn()} />);
fireEvent.click(screen.getByText('What are the indices in my cluster?'));
expect(executeActionMock).toHaveBeenCalled();
});
});
6 changes: 3 additions & 3 deletions public/tabs/chat/chat_page_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
ISuggestedAction,
Interaction,
} from '../../../common/types/chat_saved_object_attributes';
import { TermsAndConditions } from '../../components/terms_and_conditions';
import { WelcomeMessage } from '../../components/chat_welcome_message';
import { useChatContext } from '../../contexts';
import { useChatState, useChatActions } from '../../hooks';
import { MessageBubble } from './messages/message_bubble';
Expand Down Expand Up @@ -89,7 +89,7 @@ export const ChatPageContent: React.FC<ChatPageContentProps> = React.memo((props
message={{ type: 'output', contentType: 'markdown', content: '' }}
showActionBar={false}
>
<TermsAndConditions username={chatContext.currentAccount.username} />
<WelcomeMessage username={chatContext.currentAccount.username} />
</MessageBubble>
{firstInputIndex < 0 && (
<Suggestions
Expand Down Expand Up @@ -220,7 +220,7 @@ const Suggestions: React.FC<SuggestionsProps> = (props) => {
}

return (
<div style={{ marginLeft: '55px', marginTop: '5px' }}>
<div aria-label="chat suggestions" style={{ marginLeft: '55px', marginTop: '5px' }}>
<EuiText color="subdued" size="xs" style={{ paddingLeft: 10 }}>
<small>Available suggestions</small>
</EuiText>
Expand Down
Loading

0 comments on commit 3d46971

Please sign in to comment.