diff --git a/public/tabs/chat/chat_page.test.tsx b/public/tabs/chat/chat_page.test.tsx
new file mode 100644
index 00000000..d768a183
--- /dev/null
+++ b/public/tabs/chat/chat_page.test.tsx
@@ -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: () =>
};
+});
+
+jest.mock('./chat_page_content', () => {
+ return {
+ ChatPageContent: ({ onRefresh }: { onRefresh: () => void }) => (
+
+ ),
+ };
+});
+
+describe('', () => {
+ 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();
+ 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();
+ fireEvent.click(screen.getByText('refresh'));
+
+ expect(loadMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx
index a35c7ad2..ff302eed 100644
--- a/public/tabs/chat/chat_page.tsx
+++ b/public/tabs/chat/chat_page.tsx
@@ -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';
@@ -20,7 +20,6 @@ export const ChatPage: React.FC = (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';
@@ -46,8 +45,6 @@ export const ChatPage: React.FC = (props) => {
>;
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 = React.memo((props) => {
const chatContext = useChatContext();
const { chatState } = useChatState();
@@ -114,7 +105,6 @@ export const ChatPageContent: React.FC = React.memo((props
/>
)}
- {props.showGreetings && props.setShowGreetings(false)} />}
{chatState.messages.map((message, i) => {
// The latest llm output, just after the last user input
const isLatestOutput = lastInputIndex >= 0 && i > lastInputIndex;
@@ -142,7 +132,6 @@ export const ChatPageContent: React.FC = React.memo((props
interaction={interaction}
>
- {/* */}
{showSuggestions && }
diff --git a/public/tabs/chat/chat_page_greetings.tsx b/public/tabs/chat/chat_page_greetings.tsx
deleted file mode 100644
index 82fcc64b..00000000
--- a/public/tabs/chat/chat_page_greetings.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {
- EuiButtonIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiIcon,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
-import React from 'react';
-import chatIcon from '../../assets/chat.svg';
-import { GreetingCard } from '../../components/greeting_card';
-
-interface ChatPageGreetingsProps {
- dismiss: () => void;
-}
-
-const messages = [
- {
- title: 'example',
- details: "Show me the most important SLO's in my system",
- },
- {
- title: 'limitations',
- details: 'May occasionally generate incorrect information',
- },
- {
- title: 'capability',
- details: 'Allows user to provide follow-up corrections',
- },
-];
-
-export const ChatPageGreetings: React.FC = (props) => {
- return (
- <>
-
-
-
-
-
-
-
- OS ASSISTANT
-
-
-
-
-
-
-
- {messages.map((message) => (
-
- {message.details}
-
-
- ))}
- >
- );
-};
diff --git a/public/tabs/chat/controls/chat_input_controls.test.tsx b/public/tabs/chat/controls/chat_input_controls.test.tsx
new file mode 100644
index 00000000..c15528d7
--- /dev/null
+++ b/public/tabs/chat/controls/chat_input_controls.test.tsx
@@ -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('', () => {
+ 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();
+ expect(screen.getByRole('button')).toBeDisabled();
+ expect(screen.getByRole('textbox')).toBeDisabled();
+ expect(screen.getByRole('button')).toHaveTextContent('Generating...');
+
+ rerender();
+ expect(screen.getByRole('button')).toBeDisabled();
+ expect(screen.getByRole('textbox')).toBeDisabled();
+ expect(screen.getByRole('button')).toHaveTextContent('Go');
+
+ rerender();
+ expect(screen.getByRole('button')).toBeEnabled();
+ expect(screen.getByRole('textbox')).toBeEnabled();
+ expect(screen.getByRole('button')).toHaveTextContent('Generating...');
+
+ rerender();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ fireEvent.change(screen.getByRole('textbox'), {
+ target: { value: ' ' },
+ });
+ fireEvent.click(screen.getByRole('button'));
+ expect(sendMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/test/setup.jest.ts b/test/setup.jest.ts
index 4ef34444..c7c9ed97 100644
--- a/test/setup.jest.ts
+++ b/test/setup.jest.ts
@@ -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' });