Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add unit test for chat history page #55

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading