Skip to content

Commit

Permalink
test: add unit test for chat history page (#55)
Browse files Browse the repository at this point in the history
* test: add unit test for chst history page

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

* test: add unit tests for chat history list

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

* test: add unit tests for chat history search list

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

* test: remove unnecessary act

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 11e5779 commit 634053e
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 36 deletions.
1 change: 1 addition & 0 deletions public/services/sessions_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class SessionsService {
this.abortController = new AbortController();
this._options = query;
try {
this.status$.next('loading');
this.sessions$.next(
await this._http.get<ISessionFindResponse>(ASSISTANT_API.SESSIONS, {
query: this._options as HttpFetchQuery,
Expand Down
70 changes: 70 additions & 0 deletions public/tabs/history/__tests__/chat_history_list.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

import { ChatHistoryList } from '../chat_history_list';

describe('<ChatHistoryList />', () => {
it('should render two history titles, update times and one horizontal rule', async () => {
const { getByText, getAllByLabelText } = render(
<ChatHistoryList
chatHistories={[
{ id: '1', title: 'foo', updatedTimeMs: 0 },
{ id: '2', title: 'bar', updatedTimeMs: 360000 },
]}
/>
);

expect(getByText('foo')).toBeInTheDocument();
expect(getByText('bar')).toBeInTheDocument();
expect(getByText('January 1, 1970 at 12:0 AM')).toBeInTheDocument();
expect(getByText('January 1, 1970 at 12:6 AM')).toBeInTheDocument();
expect(getAllByLabelText('history horizontal rule')).toHaveLength(1);
});

it('should call onChatHistoryTitleClick with id and title', () => {
const onChatHistoryTitleClickMock = jest.fn();
const { getByText } = render(
<ChatHistoryList
chatHistories={[{ id: '1', title: 'foo', updatedTimeMs: 0 }]}
onChatHistoryTitleClick={onChatHistoryTitleClickMock}
/>
);

expect(onChatHistoryTitleClickMock).not.toHaveBeenCalled();
fireEvent.click(getByText('foo'));
expect(onChatHistoryTitleClickMock).toHaveBeenCalledWith('1', 'foo');
});

it('should call onChatHistoryEditClick with id and title', () => {
const onChatHistoryEditClickMock = jest.fn();
const { getByLabelText } = render(
<ChatHistoryList
chatHistories={[{ id: '1', title: 'foo', updatedTimeMs: 0 }]}
onChatHistoryEditClick={onChatHistoryEditClickMock}
/>
);

expect(onChatHistoryEditClickMock).not.toHaveBeenCalled();
fireEvent.click(getByLabelText('Edit conversation name'));
expect(onChatHistoryEditClickMock).toHaveBeenCalledWith({ id: '1', title: 'foo' });
});

it('should call onChatHistoryDeleteClick with id and title', () => {
const onChatHistoryDeleteClickMock = jest.fn();
const { getByLabelText } = render(
<ChatHistoryList
chatHistories={[{ id: '1', title: 'foo', updatedTimeMs: 0 }]}
onChatHistoryDeleteClick={onChatHistoryDeleteClickMock}
/>
);

expect(onChatHistoryDeleteClickMock).not.toHaveBeenCalled();
fireEvent.click(getByLabelText('Delete conversation'));
expect(onChatHistoryDeleteClickMock).toHaveBeenCalledWith({ id: '1' });
});
});
212 changes: 188 additions & 24 deletions public/tabs/history/__tests__/chat_history_page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,45 @@
*/

import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { I18nProvider } from '@osd/i18n/react';

import { coreMock } from '../../../../../../src/core/public/mocks';
import { HttpStart } from '../../../../../../src/core/public';

import * as useChatStateExports from '../../../hooks/use_chat_state';
import * as chatContextExports from '../../../contexts/chat_context';
import * as coreContextExports from '../../../contexts/core_context';
import { SessionsService } from '../../../services/sessions_service';

import { ChatHistoryPage } from '../chat_history_page';

const setup = () => {
const mockGetSessionsHttp = () => {
const http = coreMock.createStart().http;
http.get.mockImplementation(async () => ({
objects: [
{
id: '1',
title: 'foo',
},
],
total: 1,
}));
return http;
};

const setup = ({
http = mockGetSessionsHttp(),
chatContext = {},
}: {
http?: HttpStart;
chatContext?: { flyoutFullScreen?: boolean };
} = {}) => {
const useCoreMock = {
services: {
notifications: {
toasts: {
addSuccess: jest.fn(),
addDanger: jest.fn(),
addError: jest.fn(),
},
},
sessions: {
sessions$: new BehaviorSubject({
objects: [
{
id: '1',
title: 'foo',
},
],
total: 1,
}),
status$: new BehaviorSubject('idle'),
load: jest.fn(),
},
...coreMock.createStart(),
http,
sessions: new SessionsService(http),
sessionLoad: {},
},
};
Expand All @@ -47,6 +53,8 @@ const setup = () => {
sessionId: '1',
setSessionId: jest.fn(),
setTitle: jest.fn(),
setSelectedTabId: jest.fn(),
...chatContext,
};
jest.spyOn(coreContextExports, 'useCore').mockReturnValue(useCoreMock);
jest.spyOn(useChatStateExports, 'useChatState').mockReturnValue(useChatStateMock);
Expand All @@ -68,7 +76,13 @@ const setup = () => {

describe('<ChatHistoryPage />', () => {
it('should clear old session data after current session deleted', async () => {
const { renderResult, useChatStateMock, useChatContextMock } = setup();
const { renderResult, useChatStateMock, useChatContextMock } = setup({
http: mockGetSessionsHttp(),
});

await waitFor(() => {
expect(renderResult.getByLabelText('Delete conversation')).toBeTruthy();
});

act(() => {
fireEvent.click(renderResult.getByLabelText('Delete conversation'));
Expand All @@ -86,4 +100,154 @@ describe('<ChatHistoryPage />', () => {
expect(useChatContextMock.setTitle).toHaveBeenLastCalledWith(undefined);
expect(useChatStateMock.chatStateDispatch).toHaveBeenLastCalledWith({ type: 'reset' });
});

it('should render empty screen', async () => {
const http = coreMock.createStart().http;
http.get.mockImplementation(async () => {
return {
objects: [],
total: 0,
};
});
const { renderResult } = setup({
http,
});

await waitFor(async () => {
expect(
renderResult.getByText(
'No conversation has been recorded. Start a conversation in the assistant to have it saved.'
)
).toBeTruthy();
});
});

it('should render full screen back icon button instead of back', async () => {
const { renderResult } = setup({
chatContext: {
flyoutFullScreen: true,
},
});
await waitFor(async () => {
expect(renderResult.getByLabelText('full screen back')).toBeTruthy();
expect(renderResult.queryByRole('button', { name: 'Back' })).toBeFalsy();
});
});

it('should render back button and history list', async () => {
const { renderResult } = setup();
await waitFor(async () => {
expect(renderResult.getByRole('button', { name: 'Back' })).toBeTruthy();
expect(renderResult.getByText('foo')).toBeTruthy();
});
});

it('should call get sessions with search text', async () => {
const { renderResult, useCoreMock } = setup();
await waitFor(async () => {
expect(renderResult.getByPlaceholderText('Search by conversation name')).toBeTruthy();
});
act(() => {
fireEvent.change(renderResult.getByPlaceholderText('Search by conversation name'), {
target: {
value: 'bar',
},
});
});
await waitFor(() => {
expect(useCoreMock.services.http.get).toHaveBeenLastCalledWith(
expect.any(String),
expect.objectContaining({
query: expect.objectContaining({
search: 'bar',
page: 1,
}),
})
);
});
});

it('should call get sessions with new page size', async () => {
const { renderResult, useCoreMock } = setup();
act(() => {
fireEvent.click(renderResult.getByTestId('tablePaginationPopoverButton'));
});
act(() => {
fireEvent.click(renderResult.getByTestId('tablePagination-50-rows'));
});
await waitFor(() => {
expect(useCoreMock.services.http.get).toHaveBeenLastCalledWith(
expect.any(String),
expect.objectContaining({
query: expect.objectContaining({
page: 1,
perPage: 50,
}),
})
);
});
});

it('should call setSelectedTabId with "chat" after back button click', async () => {
const { renderResult, useChatContextMock } = setup();

expect(useChatContextMock.setSelectedTabId).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByRole('button', { name: 'Back' }));
});
await waitFor(() => {
expect(useChatContextMock.setSelectedTabId).toHaveBeenLastCalledWith('chat');
});
});

it('should call setSelectedTabId with "chat" after full screen back button click', async () => {
const { renderResult, useChatContextMock } = setup({
chatContext: {
flyoutFullScreen: true,
},
});

expect(useChatContextMock.setSelectedTabId).not.toHaveBeenCalled();
act(() => {
fireEvent.click(renderResult.getByLabelText('full screen back'));
});
await waitFor(() => {
expect(useChatContextMock.setSelectedTabId).toHaveBeenLastCalledWith('chat');
});
});

it('should call sessions.reload after shouldRefresh change', async () => {
const { renderResult, useCoreMock } = setup();

jest.spyOn(useCoreMock.services.sessions, 'reload');

expect(useCoreMock.services.sessions.reload).not.toHaveBeenCalled();

renderResult.rerender(
<I18nProvider>
<ChatHistoryPage shouldRefresh={true} />
</I18nProvider>
);

await waitFor(() => {
expect(useCoreMock.services.sessions.reload).toHaveBeenCalled();
});
});

it('should call sessions.abortController.abort after unmount', async () => {
const { renderResult, useCoreMock } = setup();

await waitFor(() => {
expect(useCoreMock.services.sessions.abortController).toBeTruthy();
});
const abortMock = jest.spyOn(useCoreMock.services.sessions.abortController!, 'abort');

expect(abortMock).not.toHaveBeenCalled();

renderResult.unmount();

await waitFor(() => {
expect(abortMock).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 634053e

Please sign in to comment.