Skip to content

Commit

Permalink
add unit tests for ChatPage and ChatInputControls (#45)
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 authored Dec 8, 2023
1 parent 20d8af2 commit 9663fb1
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 78 deletions.
87 changes: 87 additions & 0 deletions public/tabs/chat/chat_page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 chatContextExports from '../../contexts/chat_context';
import * as coreContextExports from '../../contexts/core_context';
import * as hookExports from '../../hooks/use_chat_state';

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: [],
interactions: [],
});
const sessionLoadService = new SessionLoadService(coreMock.createStart().http);

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

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

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

jest.spyOn(coreContextExports, '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: { messages: [], interactions: [] },
});
});
});

it('should NOT call reload if current conversation is not set', async () => {
jest.spyOn(chatContextExports, '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 @@ -46,8 +45,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 @@ -22,25 +22,16 @@ import {
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 @@ -114,7 +105,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 Down Expand Up @@ -142,7 +132,6 @@ export const ChatPageContent: React.FC<ChatPageContentProps> = React.memo((props
interaction={interaction}
>
<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/chat_context';
import * as hookExports from '../../../hooks/use_chat_actions';

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 @@ -5,6 +5,7 @@

import { configure } from '@testing-library/react';
import { TextDecoder, TextEncoder } from 'util';
import '@testing-library/jest-dom';

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

Expand Down

0 comments on commit 9663fb1

Please sign in to comment.