Skip to content

Commit

Permalink
add unit tests for ChatPage and ChatInputControls
Browse files Browse the repository at this point in the history
removed ChatPageGreetings which is not need anymore

Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl committed Dec 5, 2023
1 parent e635963 commit a7c96e5
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 78 deletions.
82 changes: 82 additions & 0 deletions public/tabs/chat/chat_page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

import { coreMock } from '../../../../../src/core/public/mocks';
import { SessionLoadService } from '../../services/session_load_service';
import { ChatPage } from './chat_page';
import * as contextExports from '../../contexts';
import * as hookExports from '../../hooks';

jest.mock('./controls/chat_input_controls', () => {
return { ChatInputControls: () => <div /> };
});

jest.mock('./chat_page_content', () => {
return {
ChatPageContent: ({ onRefresh }: { onRefresh: () => void }) => (
<button onClick={onRefresh}>refresh</button>
),
};
});

describe('<ChatPage />', () => {
const dispatchMock = jest.fn();
const loadMock = jest.fn().mockResolvedValue({
title: 'session title',
version: 1,
createdTimeMs: new Date().getTime(),
updatedTimeMs: new Date().getTime(),
messages: [],
});
const sessionLoadService = new SessionLoadService(coreMock.createStart().http);

beforeEach(() => {
jest.spyOn(sessionLoadService, 'load').mockImplementation(loadMock);

jest.spyOn(contextExports, 'useChatContext').mockReturnValue({
sessionId: 'mocked_session_id',
chatEnabled: true,
});

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

jest.spyOn(contextExports, 'useCore').mockReturnValue({
services: {
sessionLoad: sessionLoadService,
},
});
});

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

it('should reload the current conversation when user click refresh', async () => {
render(<ChatPage />);
fireEvent.click(screen.getByText('refresh'));

expect(loadMock).toHaveBeenCalledWith('mocked_session_id');
await waitFor(() => {
expect(dispatchMock).toHaveBeenCalledWith({ type: 'receive', payload: [] });
});
});

it('should NOT call reload if current conversation is not set', async () => {
jest.spyOn(contextExports, 'useChatContext').mockReturnValue({
sessionId: undefined,
chatEnabled: true,
});
render(<ChatPage />);
fireEvent.click(screen.getByText('refresh'));

expect(loadMock).not.toHaveBeenCalled();
});
});
5 changes: 1 addition & 4 deletions public/tabs/chat/chat_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { EuiFlyoutBody, EuiFlyoutFooter, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import cs from 'classnames';
import { useObservable } from 'react-use';
import { useChatContext, useCore } from '../../contexts';
Expand All @@ -20,7 +20,6 @@ export const ChatPage: React.FC<ChatPageProps> = (props) => {
const core = useCore();
const chatContext = useChatContext();
const { chatState, chatStateDispatch } = useChatState();
const [showGreetings, setShowGreetings] = useState(false);
const sessionLoadStatus = useObservable(core.services.sessionLoad.status$);
const messagesLoading = sessionLoadStatus === 'loading';

Expand All @@ -40,8 +39,6 @@ export const ChatPage: React.FC<ChatPageProps> = (props) => {
<EuiPage paddingSize="s">
<EuiPageBody component="div">
<ChatPageContent
showGreetings={showGreetings}
setShowGreetings={setShowGreetings}
messagesLoading={messagesLoading}
messagesLoadingError={
typeof sessionLoadStatus !== 'string' ? sessionLoadStatus?.error : undefined
Expand Down
11 changes: 0 additions & 11 deletions public/tabs/chat/chat_page_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,16 @@ import { IMessage, ISuggestedAction } from '../../../common/types/chat_saved_obj
import { TermsAndConditions } from '../../components/terms_and_conditions';
import { useChatContext } from '../../contexts';
import { useChatState, useChatActions } from '../../hooks';
import { ChatPageGreetings } from './chat_page_greetings';
import { MessageBubble } from './messages/message_bubble';
import { MessageContent } from './messages/message_content';
import { SuggestionBubble } from './suggestions/suggestion_bubble';

interface ChatPageContentProps {
showGreetings: boolean;
setShowGreetings: React.Dispatch<React.SetStateAction<boolean>>;
messagesLoading: boolean;
messagesLoadingError?: Error;
onRefresh: () => void;
}

const findPreviousInput = (messages: IMessage[], index: number) => {
for (let i = index - 1; i >= 0; i--) {
if (messages[i].type === 'input') return messages[i];
}
};

export const ChatPageContent: React.FC<ChatPageContentProps> = React.memo((props) => {
const chatContext = useChatContext();
const { chatState } = useChatState();
Expand Down Expand Up @@ -110,7 +101,6 @@ export const ChatPageContent: React.FC<ChatPageContentProps> = React.memo((props
/>
)}
<EuiSpacer />
{props.showGreetings && <ChatPageGreetings dismiss={() => props.setShowGreetings(false)} />}
{chatState.messages.map((message, i) => {
// The latest llm output, just after the last user input
const isLatestOutput = lastInputIndex >= 0 && i > lastInputIndex;
Expand All @@ -130,7 +120,6 @@ export const ChatPageContent: React.FC<ChatPageContentProps> = React.memo((props
onRegenerate={chatActions.regenerate}
>
<MessageContent message={message} />
{/* <MessageFooter message={message} previousInput={findPreviousInput(array, i)} />*/}
</MessageBubble>
{showSuggestions && <Suggestions message={message} inputDisabled={loading} />}
<EuiSpacer />
Expand Down
63 changes: 0 additions & 63 deletions public/tabs/chat/chat_page_greetings.tsx

This file was deleted.

117 changes: 117 additions & 0 deletions public/tabs/chat/controls/chat_input_controls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

import { ChatInputControls } from './chat_input_controls';
import * as contextExports from '../../../contexts';
import * as hookExports from '../../../hooks';

describe('<ChatInputControls />', () => {
const sendMock = jest.fn();

beforeEach(() => {
jest.spyOn(contextExports, 'useChatContext').mockReturnValue({
appId: 'mocked_app_id',
});
jest.spyOn(hookExports, 'useChatActions').mockReturnValue({
send: sendMock,
});
});

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

it('should display submit button and text box in different state accordingly', () => {
const { rerender } = render(<ChatInputControls loading={true} disabled={true} />);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.getByRole('button')).toHaveTextContent('Generating...');

rerender(<ChatInputControls loading={false} disabled={true} />);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.getByRole('button')).toHaveTextContent('Go');

rerender(<ChatInputControls loading={true} disabled={false} />);
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByRole('textbox')).toBeEnabled();
expect(screen.getByRole('button')).toHaveTextContent('Generating...');

rerender(<ChatInputControls loading={false} disabled={false} />);
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByRole('textbox')).toBeEnabled();
expect(screen.getByRole('button')).toHaveTextContent('Go');
});

it('should send message when clicking submit button', () => {
render(<ChatInputControls loading={false} disabled={false} />);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'what indices are in my cluster?' },
});
fireEvent.click(screen.getByRole('button'));
expect(sendMock).toHaveBeenCalledWith({
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
context: {
appId: 'mocked_app_id',
},
});
});

it('should send message when pressing `Enter`', () => {
render(<ChatInputControls loading={false} disabled={false} />);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'what indices are in my cluster?' },
});
fireEvent.keyPress(screen.getByRole('textbox'), {
key: 'Enter',
keyCode: 13,
shiftKey: false,
});
expect(sendMock).toHaveBeenCalledWith({
type: 'input',
content: 'what indices are in my cluster?',
contentType: 'text',
context: {
appId: 'mocked_app_id',
},
});
});

it('should NOT send message when pressing `shift+Enter`', () => {
render(<ChatInputControls loading={false} disabled={false} />);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'what indices are in my cluster?' },
});
fireEvent.keyPress(screen.getByRole('textbox'), {
key: 'Enter',
keyCode: 13,
shiftKey: true,
});
expect(sendMock).not.toHaveBeenCalled();
});

it('should NOT send message if disabled', () => {
render(<ChatInputControls loading={false} disabled={true} />);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'what indices are in my cluster?' },
});
fireEvent.click(screen.getByRole('button'));
expect(sendMock).not.toHaveBeenCalled();
});

it('should NOT send message if input is trimmed empty', () => {
render(<ChatInputControls loading={false} disabled={false} />);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: ' ' },
});
fireEvent.click(screen.getByRole('button'));
expect(sendMock).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions test/setup.jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { configure } from '@testing-library/react';
import { TextDecoder, TextEncoder } from 'util';
import 'web-streams-polyfill';
import '@testing-library/jest-dom';
import '../server/fetch-polyfill';

configure({ testIdAttribute: 'data-test-subj' });
Expand Down

0 comments on commit a7c96e5

Please sign in to comment.