Skip to content

Commit

Permalink
[CLNP-5043] Add unit tests for improved MessageSearch module (#1228)
Browse files Browse the repository at this point in the history
https://sendbird.atlassian.net/browse/CLNP-5043 
Two things are handled based on what I mentioned in
https://sendbird.atlassian.net/wiki/spaces/UIKitreact/pages/2511765635/UIKit+React+new+State+Management+Method+Proposal#4.1-Unit-Test
- [x] Added unit tests for `useMessageSearch` hook and new
`MessageSearchProvider`
- [x] Added integration tests for `MessageSearchUI` component

So the MessageSearch module test coverage has been changed 
**from** 
File --------------------------------------------------| % Stmts | %
Branch | % Funcs | % Lines | Uncovered Line #s
<img width="814" alt="Screenshot 2024-10-08 at 2 36 55 PM"
src="https://github.com/user-attachments/assets/c0fef6fe-0fc1-4f37-b74f-0486d70352a7">
**to** 
<img width="941" alt="after"
src="https://github.com/user-attachments/assets/7fc19fb8-810c-4256-8230-3884d11e109a">
note that it used to be like 0%, but now the test coverage of the newly
added files is almost 100%; green 🟩.



### Checklist

Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If unsure, ask the members.
This is a reminder of what we look for before merging your code.

- [x] **All tests pass locally with my changes**
- [x] **I have added tests that prove my fix is effective or that my
feature works**
- [ ] **Public components / utils / props are appropriately exported**
- [ ] I have added necessary documentation (if appropriate)
  • Loading branch information
AhyoungRyu committed Dec 11, 2024
1 parent 77f5395 commit 4689d79
Show file tree
Hide file tree
Showing 9 changed files with 570 additions and 134 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import MessageSearchUI from '../components/MessageSearchUI';
import { LocalizationContext } from '../../../lib/LocalizationContext';
import * as useMessageSearchModule from '../context/hooks/useMessageSearch';

jest.mock('../context/hooks/useMessageSearch');

const mockStringSet = {
SEARCH_IN: 'Search in',
SEARCH_PLACEHOLDER: 'Search',
SEARCHING: 'Searching...',
NO_SEARCHED_MESSAGE: 'No results found',
NO_TITLE: 'No title',
PLACE_HOLDER__RETRY_TO_CONNECT: 'Retry',
};

const mockLocalizationContext = {
stringSet: mockStringSet,
};

const defaultMockState = {
isQueryInvalid: false,
searchString: '',
requestString: '',
currentChannel: null,
loading: false,
scrollRef: { current: null },
hasMoreResult: false,
onScroll: jest.fn(),
allMessages: [],
onResultClick: jest.fn(),
selectedMessageId: null,
};

const defaultMockActions = {
setSelectedMessageId: jest.fn(),
handleRetryToConnect: jest.fn(),
};

describe('MessageSearchUI Integration Tests', () => {
const mockUseMessageSearch = useMessageSearchModule.default as jest.Mock;

const renderComponent = (mockState = {}, mockActions = {}) => {
mockUseMessageSearch.mockReturnValue({
state: { ...defaultMockState, ...mockState },
actions: { ...defaultMockActions, ...mockActions },
});

return render(
<LocalizationContext.Provider value={mockLocalizationContext as any}>
<MessageSearchUI />
</LocalizationContext.Provider>,
);
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders initial state correctly', () => {
renderComponent();
expect(screen.getByText('Search in')).toBeInTheDocument();
});

it('displays loading state when search is in progress', () => {
renderComponent({ loading: true, searchString: 'test query', requestString: 'test query' });
expect(screen.getByText(mockStringSet.SEARCHING)).toBeInTheDocument();
});

it('displays search results when available', () => {
renderComponent({
allMessages: [
{ messageId: 1, message: 'Message 1', sender: { nickname: 'Sender 1' } },
{ messageId: 2, message: 'Message 2', sender: { nickname: 'Sender 2' } },
],
searchString: 'test query',
});
expect(screen.getByText('Message 1')).toBeInTheDocument();
expect(screen.getByText('Message 2')).toBeInTheDocument();
});

it('handles no results state', () => {
renderComponent({ allMessages: [], searchString: 'no results query', requestString: 'no results query' });
expect(screen.getByText(mockStringSet.NO_SEARCHED_MESSAGE)).toBeInTheDocument();
});

it('handles error state and retry', async () => {
const handleRetryToConnect = jest.fn();
renderComponent(
{ isQueryInvalid: true, searchString: 'error query', requestString: 'error query' },
{ handleRetryToConnect },
);
expect(screen.getByText(mockStringSet.PLACE_HOLDER__RETRY_TO_CONNECT)).toBeInTheDocument();

const retryButton = screen.getByText('Retry');
fireEvent.click(retryButton);

expect(handleRetryToConnect).toHaveBeenCalled();
});

it('triggers loading more messages when scrolled near bottom', async () => {
const onScroll = jest.fn();
const loadMoreMessages = jest.fn();
const { container } = renderComponent({
allMessages: [{ messageId: 1, message: 'Message 1' }],
hasMoreResult: true,
onScroll,
});

const scrollContainer = container.firstChild as Element;

// define scroll container properties
Object.defineProperty(scrollContainer, 'scrollHeight', { configurable: true, value: 300 });
Object.defineProperty(scrollContainer, 'clientHeight', { configurable: true, value: 500 });
Object.defineProperty(scrollContainer, 'scrollTop', { configurable: true, value: 450 });

const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;

if (scrollTop + clientHeight >= scrollHeight - 1) {
loadMoreMessages();
}
};

fireEvent.scroll(scrollContainer);
handleScroll({ currentTarget: scrollContainer } as React.UIEvent<HTMLDivElement>);

expect(loadMoreMessages).toHaveBeenCalled();
});

it('handles message click', () => {
const setSelectedMessageId = jest.fn();
const onResultClick = jest.fn();
renderComponent(
{
allMessages: [{ messageId: 1, message: 'Message 1', sender: { nickname: 'Sender 1' } }],
searchString: 'Message 1',
onResultClick,
},
{ setSelectedMessageId },
);

const message = screen.getByText('Message 1');
fireEvent.click(message);

expect(setSelectedMessageId).toHaveBeenCalledWith(1);
expect(onResultClick).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const MessageSearchUI: React.FC<MessageSearchUIProps> = ({
}: MessageSearchUIProps) => {
const {
state: {
isInvalid,
isQueryInvalid,
searchString,
requestString,
currentChannel,
Expand Down Expand Up @@ -83,7 +83,7 @@ export const MessageSearchUI: React.FC<MessageSearchUIProps> = ({
return stringSet.NO_TITLE;
};

if (isInvalid && searchString && requestString) {
if (isQueryInvalid && searchString && requestString) {
return renderPlaceHolderError?.() || (
<div className="sendbird-message-search">
<PlaceHolder
Expand Down
4 changes: 2 additions & 2 deletions src/modules/MessageSearch/context/MessageSearchProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface MessageSearchState extends MessageSearchProviderProps {
channelUrl: string;
allMessages: ClientSentMessages[];
loading: boolean;
isInvalid: boolean;
isQueryInvalid: boolean;
initialized: boolean;
currentChannel: GroupChannel | null;
currentMessageSearchQuery: MessageSearchQuery | null;
Expand All @@ -45,7 +45,7 @@ const initialState: MessageSearchState = {
channelUrl: '',
allMessages: [],
loading: false,
isInvalid: false,
isQueryInvalid: false,
initialized: false,
currentChannel: null,
currentMessageSearchQuery: null,
Expand Down
Loading

0 comments on commit 4689d79

Please sign in to comment.