From 766d1d7fb54259e67b4e8fa57416e05bb885c825 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Mon, 9 Dec 2024 13:20:22 +0900 Subject: [PATCH] [CLNP-6010] Add more unit tests for existing message search custom hook (#1278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Before Screenshot 2024-12-06 at 6 23 24 PM #### After Screenshot 2024-12-06 at 6 36 19 PM --- .../__test__/MessageSearchProvider.spec.tsx | 104 ++++++ .../__test__/useGetSearchedMessages.spec.ts | 301 ++++++++++++++++++ .../__test__/useScrollCallback.spec.ts | 209 ++++++++++++ .../__test__/useSearchStringEffect.spec.ts | 176 ++++++++++ .../context/__test__/useSetChannel.spec.ts | 103 ++++++ 5 files changed, 893 insertions(+) create mode 100644 src/modules/MessageSearch/context/__test__/useGetSearchedMessages.spec.ts create mode 100644 src/modules/MessageSearch/context/__test__/useScrollCallback.spec.ts create mode 100644 src/modules/MessageSearch/context/__test__/useSearchStringEffect.spec.ts create mode 100644 src/modules/MessageSearch/context/__test__/useSetChannel.spec.ts diff --git a/src/modules/MessageSearch/context/__test__/MessageSearchProvider.spec.tsx b/src/modules/MessageSearch/context/__test__/MessageSearchProvider.spec.tsx index 60d9c8590..4096e6bde 100644 --- a/src/modules/MessageSearch/context/__test__/MessageSearchProvider.spec.tsx +++ b/src/modules/MessageSearch/context/__test__/MessageSearchProvider.spec.tsx @@ -5,6 +5,7 @@ import { MessageSearchQuery } from '@sendbird/chat/message'; import { MessageSearchProvider } from '../MessageSearchProvider'; import useMessageSearch from '../hooks/useMessageSearch'; +import useScrollCallback from '../hooks/useScrollCallback'; jest.mock('../../../../lib/Sendbird/context/hooks/useSendbird', () => ({ __esModule: true, @@ -46,6 +47,11 @@ jest.mock('../hooks/useSearchStringEffect', () => ({ })); describe('MessageSearchProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useScrollCallback as jest.Mock).mockClear(); + }); + const initialState = { allMessages: [], loading: false, @@ -290,4 +296,102 @@ describe('MessageSearchProvider', () => { }); }); }); + + it('handles onResultClick callback correctly', async () => { + const onResultClick = jest.fn(); + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useMessageSearch(), { wrapper }); + + expect(result.current.state.onResultClick).toBe(onResultClick); + }); + + it('uses provided messageSearchQuery prop correctly', async () => { + const customQuery = { + limit: 20, + reverse: true, + exactMatch: false, + }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useMessageSearch(), { wrapper }); + + expect(result.current.state.messageSearchQuery).toEqual(customQuery); + }); + + it('executes onResultClick callback when clicking a search result', async () => { + const onResultClick = jest.fn(); + const mockMessage = { messageId: 1 }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useMessageSearch(), { wrapper }); + + await act(async () => { + expect(result.current.state.onResultClick).toBe(onResultClick); + result.current.state.onResultClick(mockMessage); + await waitFor(() => { + expect(onResultClick).toHaveBeenCalledWith(mockMessage); + }); + }); + }); + + it('does not trigger scroll callback when hasMoreResult is false', async () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useMessageSearch(), { wrapper }); + + await act(async () => { + const mockQuery = { channelUrl: 'test-channel', hasNext: false }; + result.current.actions.startGettingSearchedMessages(mockQuery as any); + result.current.actions.getSearchedMessages([{ messageId: 1 }] as any, mockQuery as any); + + await waitFor(() => { + expect(result.current.state.hasMoreResult).toBe(false); + }); + }); + + await act(async () => { + const mockEvent = { + target: { + scrollTop: 100, + scrollHeight: 100, + clientHeight: 50, + }, + }; + + const prevLoading = result.current.state.loading; + result.current.state.handleOnScroll(mockEvent); + + await waitFor(() => { + expect(result.current.state.loading).toBe(prevLoading); + }); + }); + }); }); diff --git a/src/modules/MessageSearch/context/__test__/useGetSearchedMessages.spec.ts b/src/modules/MessageSearch/context/__test__/useGetSearchedMessages.spec.ts new file mode 100644 index 000000000..adf774089 --- /dev/null +++ b/src/modules/MessageSearch/context/__test__/useGetSearchedMessages.spec.ts @@ -0,0 +1,301 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { GroupChannel } from '@sendbird/chat/groupChannel'; +import useGetSearchedMessages from '../hooks/useGetSearchedMessages'; +import useMessageSearch from '../hooks/useMessageSearch'; + +jest.mock('../hooks/useMessageSearch', () => ({ + __esModule: true, + default: jest.fn(), +})); + +describe('useGetSearchedMessages', () => { + const mockLogger = { + warning: jest.fn(), + info: jest.fn(), + }; + + const mockStartMessageSearch = jest.fn(); + const mockGetSearchedMessages = jest.fn(); + const mockSetQueryInvalid = jest.fn(); + const mockStartGettingSearchedMessages = jest.fn(); + const mockOnResultLoaded = jest.fn(); + + const mockSdk = { + createMessageSearchQuery: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + retryCount: 0, + }, + actions: { + startMessageSearch: mockStartMessageSearch, + getSearchedMessages: mockGetSearchedMessages, + setQueryInvalid: mockSetQueryInvalid, + startGettingSearchedMessages: mockStartGettingSearchedMessages, + }, + }); + }); + + it('should not proceed when requestString is empty', () => { + renderHook( + () => useGetSearchedMessages( + { + currentChannel: null, + channelUrl: 'channel-url', + requestString: '', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + expect(mockLogger.info).toHaveBeenCalledWith( + 'MessageSearch | useGetSearchedMessages: search string is empty', + ); + expect(mockStartMessageSearch).toHaveBeenCalled(); + }); + + it('should handle successful message search', async () => { + const mockMessages = [{ messageId: 1 }]; + const mockQuery = { + next: jest.fn().mockResolvedValue(mockMessages), + }; + + mockSdk.createMessageSearchQuery.mockReturnValue(mockQuery); + + const mockChannel = { + url: 'channel-url', + refresh: jest.fn().mockResolvedValue({ + invitedAt: 1234567890, + }), + }; + + renderHook( + () => useGetSearchedMessages( + { + currentChannel: mockChannel as unknown as GroupChannel, + channelUrl: 'channel-url', + requestString: 'search-term', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockStartMessageSearch).toHaveBeenCalled(); + expect(mockChannel.refresh).toHaveBeenCalled(); + expect(mockSdk.createMessageSearchQuery).toHaveBeenCalled(); + expect(mockStartGettingSearchedMessages).toHaveBeenCalled(); + expect(mockGetSearchedMessages).toHaveBeenCalledWith(mockMessages, mockQuery); + expect(mockOnResultLoaded).toHaveBeenCalledWith(mockMessages, undefined); + }); + + it('should handle channel refresh failure', async () => { + const mockError = new Error('Channel refresh failed'); + const mockChannel = { + url: 'channel-url', + refresh: jest.fn().mockRejectedValue(mockError), + }; + + renderHook( + () => useGetSearchedMessages( + { + currentChannel: mockChannel as unknown as GroupChannel, + channelUrl: 'channel-url', + requestString: 'search-term', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockStartMessageSearch).toHaveBeenCalled(); + expect(mockChannel.refresh).toHaveBeenCalled(); + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useGetSearchedMessages: failed getting channel.', + mockError, + ); + expect(mockSetQueryInvalid).toHaveBeenCalled(); + expect(mockOnResultLoaded).toHaveBeenCalledWith(undefined, mockError); + }); + + it('should handle message search failure', async () => { + const mockError = new Error('Search failed'); + const mockQuery = { + next: jest.fn().mockRejectedValue(mockError), + }; + + mockSdk.createMessageSearchQuery.mockReturnValue(mockQuery); + + const mockChannel = { + url: 'channel-url', + refresh: jest.fn().mockResolvedValue({ + invitedAt: 1234567890, + }), + }; + + renderHook( + () => useGetSearchedMessages( + { + currentChannel: mockChannel as unknown as GroupChannel, + channelUrl: 'channel-url', + requestString: 'search-term', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockStartMessageSearch).toHaveBeenCalled(); + expect(mockChannel.refresh).toHaveBeenCalled(); + expect(mockSdk.createMessageSearchQuery).toHaveBeenCalled(); + expect(mockStartGettingSearchedMessages).toHaveBeenCalled(); + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useGetSearchedMessages: failed getting search messages.', + mockError, + ); + expect(mockSetQueryInvalid).toHaveBeenCalled(); + expect(mockOnResultLoaded).toHaveBeenCalledWith(undefined, mockError); + }); + + it('should use custom messageSearchQuery params when provided', async () => { + const mockMessages = [{ messageId: 1 }]; + const mockQuery = { + next: jest.fn().mockResolvedValue(mockMessages), + }; + + mockSdk.createMessageSearchQuery.mockReturnValue(mockQuery); + + const mockChannel = { + url: 'channel-url', + refresh: jest.fn().mockResolvedValue({ + invitedAt: 1234567890, + }), + }; + + const customSearchQuery = { + limit: 20, + reverse: true, + exactMatch: false, + }; + + renderHook( + () => useGetSearchedMessages( + { + currentChannel: mockChannel as unknown as GroupChannel, + channelUrl: 'channel-url', + requestString: 'search-term', + messageSearchQuery: customSearchQuery, + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockSdk.createMessageSearchQuery).toHaveBeenCalledWith( + expect.objectContaining(customSearchQuery), + ); + }); + + it('should not proceed when required dependencies are missing', () => { + renderHook( + () => useGetSearchedMessages( + { + currentChannel: null, + channelUrl: 'channel-url', + requestString: 'search-term', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: null as any, + logger: mockLogger as any, + }, + ), + ); + + expect(mockStartMessageSearch).toHaveBeenCalled(); + expect(mockSdk.createMessageSearchQuery).not.toHaveBeenCalled(); + }); + + it('should handle retry mechanism when retryCount changes', async () => { + const mockMessages = [{ messageId: 1 }]; + const mockQuery = { + next: jest.fn().mockResolvedValue(mockMessages), + }; + + mockSdk.createMessageSearchQuery.mockReturnValue(mockQuery); + + const mockChannel = { + url: 'channel-url', + refresh: jest.fn().mockResolvedValue({ + invitedAt: 1234567890, + }), + }; + + const { rerender } = renderHook( + () => useGetSearchedMessages( + { + currentChannel: mockChannel as unknown as GroupChannel, + channelUrl: 'channel-url', + requestString: 'search-term', + onResultLoaded: mockOnResultLoaded, + }, + { + sdk: mockSdk as any, + logger: mockLogger as any, + }, + ), + ); + + // Simulate retry by changing retryCount + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + retryCount: 1, + }, + actions: { + startMessageSearch: mockStartMessageSearch, + getSearchedMessages: mockGetSearchedMessages, + setQueryInvalid: mockSetQueryInvalid, + startGettingSearchedMessages: mockStartGettingSearchedMessages, + }, + }); + + rerender(); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockStartMessageSearch).toHaveBeenCalledTimes(2); + expect(mockChannel.refresh).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/modules/MessageSearch/context/__test__/useScrollCallback.spec.ts b/src/modules/MessageSearch/context/__test__/useScrollCallback.spec.ts new file mode 100644 index 000000000..56fffd51c --- /dev/null +++ b/src/modules/MessageSearch/context/__test__/useScrollCallback.spec.ts @@ -0,0 +1,209 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useScrollCallback from '../hooks/useScrollCallback'; +import useMessageSearch from '../hooks/useMessageSearch'; + +jest.mock('../hooks/useMessageSearch', () => ({ + __esModule: true, + default: jest.fn(), +})); + +describe('useScrollCallback', () => { + const mockLogger = { + warning: jest.fn(), + info: jest.fn(), + }; + + const mockOnResultLoaded = jest.fn(); + const mockGetNextSearchedMessages = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: null, + hasMoreResult: false, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + }); + + it('should log warning when there are no more results', () => { + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: mockOnResultLoaded }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + result.current(callback); + + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useScrollCallback: no more searched results', + false, + ); + }); + + it('should log warning when there is no currentMessageSearchQuery', () => { + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: null, + hasMoreResult: true, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: mockOnResultLoaded }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + result.current(callback); + + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useScrollCallback: no currentMessageSearchQuery', + ); + }); + + it('should handle successful message search', async () => { + const mockMessages = [{ messageId: 1 }, { messageId: 2 }]; + const mockNext = jest.fn().mockResolvedValue(mockMessages); + + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: { + hasNext: true, + next: mockNext, + }, + hasMoreResult: true, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: mockOnResultLoaded }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + await result.current(callback); + + expect(mockNext).toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith( + 'MessageSearch | useScrollCallback: succeeded getting searched messages', + mockMessages, + ); + expect(mockGetNextSearchedMessages).toHaveBeenCalledWith(mockMessages); + expect(callback).toHaveBeenCalledWith(mockMessages, null); + expect(mockOnResultLoaded).toHaveBeenCalledWith(mockMessages, null); + }); + + it('should handle failed message search', async () => { + const mockError = new Error('Search failed'); + const mockNext = jest.fn().mockRejectedValue(mockError); + + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: { + hasNext: true, + next: mockNext, + }, + hasMoreResult: true, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: mockOnResultLoaded }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + + try { + await result.current(callback); + } catch (error) { + // execute even if error occurs + } + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockNext).toHaveBeenCalled(); + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useScrollCallback: failed getting searched messages', + mockError, + ); + expect(callback).toHaveBeenCalledWith(null, mockError); + expect(mockOnResultLoaded).toHaveBeenCalledWith(null, mockError); + }); + + it('should not call onResultLoaded if not provided', async () => { + const mockMessages = [{ messageId: 1 }]; + const mockNext = jest.fn().mockResolvedValue(mockMessages); + + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: { + hasNext: true, + next: mockNext, + }, + hasMoreResult: true, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: undefined }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + await result.current(callback); + + expect(mockNext).toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith(mockMessages, null); + expect(mockOnResultLoaded).not.toHaveBeenCalled(); + }); + + it('should not proceed with search if query has no next', () => { + (useMessageSearch as jest.Mock).mockReturnValue({ + state: { + currentMessageSearchQuery: { + hasNext: false, + }, + hasMoreResult: true, + }, + actions: { + getNextSearchedMessages: mockGetNextSearchedMessages, + }, + }); + + const { result } = renderHook(() => useScrollCallback( + { onResultLoaded: mockOnResultLoaded }, + { logger: mockLogger as any }, + ), + ); + + const callback = jest.fn(); + result.current(callback); + + expect(mockLogger.warning).toHaveBeenCalledWith( + 'MessageSearch | useScrollCallback: no currentMessageSearchQuery', + ); + }); +}); diff --git a/src/modules/MessageSearch/context/__test__/useSearchStringEffect.spec.ts b/src/modules/MessageSearch/context/__test__/useSearchStringEffect.spec.ts new file mode 100644 index 000000000..dbf3b2b66 --- /dev/null +++ b/src/modules/MessageSearch/context/__test__/useSearchStringEffect.spec.ts @@ -0,0 +1,176 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import useSearchStringEffect from '../hooks/useSearchStringEffect'; +import useMessageSearch from '../hooks/useMessageSearch'; + +jest.mock('../hooks/useMessageSearch', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.useFakeTimers(); + +describe('useSearchStringEffect', () => { + const mockResetSearchString = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useMessageSearch as jest.Mock).mockReturnValue({ + actions: { + resetSearchString: mockResetSearchString, + }, + }); + }); + + it('should set request string after debounce when search string is provided', async () => { + const { result } = renderHook(() => useSearchStringEffect({ searchString: 'test query' }), + ); + + // Initial state should be empty + expect(result.current).toBe(''); + + // Fast-forward debounce timer + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe('test query'); + }); + + it('should reset search string when empty string is provided', async () => { + const { result } = renderHook(() => useSearchStringEffect({ searchString: '' }), + ); + + // Fast-forward debounce timer + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe(''); + expect(mockResetSearchString).toHaveBeenCalled(); + }); + + it('should handle undefined search string', async () => { + const { result } = renderHook(() => useSearchStringEffect({ searchString: undefined }), + ); + + // Fast-forward debounce timer + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe(''); + expect(mockResetSearchString).toHaveBeenCalled(); + }); + + it('should clear previous timer when search string changes rapidly', () => { + const { result, rerender } = renderHook( + ({ searchString }) => useSearchStringEffect({ searchString }), + { initialProps: { searchString: 'initial' } }, + ); + + // Start first timer + act(() => { + jest.advanceTimersByTime(200); // Advance less than debounce time + }); + + // Change search string before first timer completes + rerender({ searchString: 'updated' }); + + // Advance timer to complete first debounce + act(() => { + jest.advanceTimersByTime(300); + }); + + // Result should not be 'initial' + expect(result.current).not.toBe('initial'); + + // Complete second debounce + act(() => { + jest.advanceTimersByTime(200); + }); + + // Result should be 'updated' + expect(result.current).toBe('updated'); + }); + + it('should clean up timer on unmount', () => { + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + + const { unmount } = renderHook(() => useSearchStringEffect({ searchString: 'test' }), + ); + + unmount(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); + + it('should not trigger unnecessary updates when search string remains the same', () => { + const { result, rerender } = renderHook( + ({ searchString }) => useSearchStringEffect({ searchString }), + { initialProps: { searchString: 'test' } }, + ); + + // Complete first debounce + act(() => { + jest.advanceTimersByTime(500); + }); + + const firstResult = result.current; + + // Rerender with same search string + rerender({ searchString: 'test' }); + + // Complete second debounce + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe(firstResult); + }); + + it('should handle multiple search string changes within debounce period', () => { + const { result, rerender } = renderHook( + ({ searchString }) => useSearchStringEffect({ searchString }), + { initialProps: { searchString: 'first' } }, + ); + + // Change search string multiple times rapidly + rerender({ searchString: 'second' }); + rerender({ searchString: 'third' }); + rerender({ searchString: 'final' }); + + // Advance timer to complete debounce + act(() => { + jest.advanceTimersByTime(500); + }); + + // Should only reflect the final value + expect(result.current).toBe('final'); + }); + + it('should maintain empty state when switching from empty to undefined', () => { + const { result, rerender } = renderHook( + ({ searchString }) => useSearchStringEffect({ searchString }), + { initialProps: { searchString: '' } }, + ); + + // Complete first debounce + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe(''); + expect(mockResetSearchString).toHaveBeenCalledTimes(1); + + // Switch to undefined + rerender({ searchString: undefined }); + + // Complete second debounce + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current).toBe(''); + expect(mockResetSearchString).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/modules/MessageSearch/context/__test__/useSetChannel.spec.ts b/src/modules/MessageSearch/context/__test__/useSetChannel.spec.ts new file mode 100644 index 000000000..6dabb4f49 --- /dev/null +++ b/src/modules/MessageSearch/context/__test__/useSetChannel.spec.ts @@ -0,0 +1,103 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useSetChannel from '../hooks/useSetChannel'; +import useMessageSearch from '../hooks/useMessageSearch'; + +jest.mock('../hooks/useMessageSearch', () => ({ + __esModule: true, + default: jest.fn(), +})); + +describe('useSetChannel', () => { + const mockLogger = { + info: jest.fn(), + warning: jest.fn(), + }; + + const mockSetCurrentChannel = jest.fn(); + const mockSetChannelInvalid = jest.fn(); + + const mockSdk = { + groupChannel: { + getChannel: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useMessageSearch as jest.Mock).mockReturnValue({ + actions: { + setCurrentChannel: mockSetCurrentChannel, + setChannelInvalid: mockSetChannelInvalid, + }, + }); + }); + + it('should set current channel when channelUrl and sdkInit are valid', async () => { + const mockChannel = { url: 'test-channel' }; + mockSdk.groupChannel.getChannel.mockResolvedValue(mockChannel); + + renderHook(() => useSetChannel( + { channelUrl: 'test-channel', sdkInit: true }, + { sdk: mockSdk as any, logger: mockLogger as any }, + )); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockSdk.groupChannel.getChannel).toHaveBeenCalledWith('test-channel'); + expect(mockLogger.info).toHaveBeenCalledWith( + 'MessageSearch | useSetChannel group channel', + mockChannel, + ); + expect(mockSetCurrentChannel).toHaveBeenCalledWith(mockChannel); + }); + + it('should set channel invalid when getChannel fails', async () => { + const mockError = new Error('Failed to get channel'); + mockSdk.groupChannel.getChannel.mockRejectedValue(mockError); + + renderHook(() => useSetChannel( + { channelUrl: 'test-channel', sdkInit: true }, + { sdk: mockSdk as any, logger: mockLogger as any }, + )); + + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(mockSdk.groupChannel.getChannel).toHaveBeenCalledWith('test-channel'); + expect(mockSetChannelInvalid).toHaveBeenCalled(); + }); + + it('should not attempt to get channel if sdkInit is false', () => { + renderHook(() => useSetChannel( + { channelUrl: 'test-channel', sdkInit: false }, + { sdk: mockSdk as any, logger: mockLogger as any }, + )); + + expect(mockSdk.groupChannel.getChannel).not.toHaveBeenCalled(); + expect(mockSetCurrentChannel).not.toHaveBeenCalled(); + expect(mockSetChannelInvalid).not.toHaveBeenCalled(); + }); + + it('should not attempt to get channel if channelUrl is empty', () => { + renderHook(() => useSetChannel( + { channelUrl: '', sdkInit: true }, + { sdk: mockSdk as any, logger: mockLogger as any }, + )); + + expect(mockSdk.groupChannel.getChannel).not.toHaveBeenCalled(); + expect(mockSetCurrentChannel).not.toHaveBeenCalled(); + expect(mockSetChannelInvalid).not.toHaveBeenCalled(); + }); + + it('should handle missing sdk gracefully', () => { + renderHook(() => useSetChannel( + { channelUrl: 'test-channel', sdkInit: true }, + { sdk: null as any, logger: mockLogger as any }, + )); + + expect(mockSdk.groupChannel.getChannel).not.toHaveBeenCalled(); + expect(mockSetCurrentChannel).not.toHaveBeenCalled(); + expect(mockSetChannelInvalid).not.toHaveBeenCalled(); + }); +});