Skip to content

Commit dd4d887

Browse files
committed
refactor: Add tests for ChannelSettings migration (#1234)
fix: Prevent destroy error by adding `AbortController` in `useSetChannel` - Added `AbortController` to cancel async operations when the component unmounts. - Ensured that state updates are skipped if the operation is aborted. - Prevented potential memory leaks and the `'destroy is not a function'` error. - Updated `useEffect` cleanup to properly handle pending async calls. feat: Add tests for ChannelSettings migration
1 parent e344b3b commit dd4d887

File tree

7 files changed

+488
-42
lines changed

7 files changed

+488
-42
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from 'react';
2+
import { renderHook, act } from '@testing-library/react-hooks';
3+
import { ChannelSettingsProvider, useChannelSettingsContext } from '../context/ChannelSettingsProvider';
4+
import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext';
5+
import { SendbirdSdkContext } from '../../../lib/SendbirdSdkContext';
6+
7+
jest.mock('../../../hooks/useSendbirdStateContext');
8+
jest.mock('../context/hooks/useSetChannel');
9+
10+
const mockLogger = {
11+
warning: jest.fn(),
12+
info: jest.fn(),
13+
error: jest.fn(),
14+
};
15+
16+
const initialState = {
17+
channelUrl: 'test-channel',
18+
onCloseClick: undefined,
19+
onLeaveChannel: undefined,
20+
onChannelModified: undefined,
21+
onBeforeUpdateChannel: undefined,
22+
renderUserListItem: undefined,
23+
queries: undefined,
24+
overrideInviteUser: undefined,
25+
channel: null,
26+
loading: false,
27+
invalidChannel: false,
28+
forceUpdateUI: expect.any(Function),
29+
setChannelUpdateId: expect.any(Function),
30+
};
31+
32+
describe('ChannelSettingsProvider', () => {
33+
let wrapper;
34+
35+
beforeEach(() => {
36+
useSendbirdStateContext.mockReturnValue({
37+
stores: { sdkStore: { sdk: {}, initialized: true } },
38+
config: { logger: mockLogger },
39+
});
40+
41+
wrapper = ({ children }) => (
42+
<SendbirdSdkContext.Provider value={{ config: { logger: mockLogger } } as any}>
43+
<ChannelSettingsProvider channelUrl="test-channel">
44+
{children}
45+
</ChannelSettingsProvider>
46+
</SendbirdSdkContext.Provider>
47+
);
48+
49+
jest.clearAllMocks();
50+
});
51+
52+
it('provides the correct initial state', () => {
53+
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });
54+
55+
expect(result.current.getState()).toEqual(expect.objectContaining(initialState));
56+
});
57+
58+
it('logs a warning if SDK is not initialized', () => {
59+
useSendbirdStateContext.mockReturnValue({
60+
stores: { sdkStore: { sdk: null, initialized: false } },
61+
config: { logger: mockLogger },
62+
});
63+
64+
renderHook(() => useChannelSettingsContext(), { wrapper });
65+
66+
expect(mockLogger.warning).toHaveBeenCalledWith('ChannelSettings: SDK or GroupChannelModule is not available');
67+
});
68+
69+
it('updates state correctly when setChannelUpdateId is called', async () => {
70+
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });
71+
72+
await act(async () => {
73+
result.current.setState({ channelUrl: 'new-channel' });
74+
await waitForStateUpdate();
75+
expect(result.current.getState().channelUrl).toBe('new-channel');
76+
});
77+
});
78+
79+
it('maintains other state values when channel changes', async () => {
80+
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });
81+
82+
await act(async () => {
83+
result.current.setState({ channel: { name: 'Updated Channel' } });
84+
await waitForStateUpdate();
85+
const updatedState = result.current.getState();
86+
expect(updatedState.channel).toEqual({ name: 'Updated Channel' });
87+
expect(updatedState.loading).toBe(false);
88+
expect(updatedState.invalidChannel).toBe(false);
89+
});
90+
});
91+
92+
const waitForStateUpdate = () => new Promise(resolve => { setTimeout(resolve, 0); });
93+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from 'react';
2+
import '@testing-library/jest-dom/extend-expect';
3+
import { render, screen } from '@testing-library/react';
4+
5+
import ChannelSettingsUI from '../components/ChannelSettingsUI';
6+
import { LocalizationContext } from '../../../lib/LocalizationContext';
7+
import * as useChannelSettingsModule from '../context/useChannelSettings';
8+
import { SendbirdSdkContext } from '../../../lib/SendbirdSdkContext';
9+
10+
jest.mock('../context/useChannelSettings');
11+
12+
const mockStringSet = {
13+
CHANNEL_SETTING__HEADER__TITLE: 'Channel information',
14+
CHANNEL_SETTING__OPERATORS__TITLE: 'Operators',
15+
CHANNEL_SETTING__MEMBERS__TITLE: 'Members',
16+
CHANNEL_SETTING__MUTED_MEMBERS__TITLE: 'Muted members',
17+
CHANNEL_SETTING__BANNED_MEMBERS__TITLE: 'Banned users',
18+
CHANNEL_SETTING__FREEZE_CHANNEL: 'Freeze Channel',
19+
CHANNEL_SETTING__LEAVE_CHANNEL__TITLE: 'Leave channel',
20+
};
21+
const mockChannelName = 'Test Channel';
22+
23+
const mockLocalizationContext = {
24+
stringSet: mockStringSet,
25+
};
26+
27+
const defaultMockState = {
28+
channel: { name: mockChannelName, members: [], isBroadcast: false },
29+
loading: false,
30+
invalidChannel: false,
31+
};
32+
33+
const defaultMockActions = {
34+
setChannel: jest.fn(),
35+
setLoading: jest.fn(),
36+
setInvalid: jest.fn(),
37+
};
38+
39+
describe('ChannelSettings Integration Tests', () => {
40+
const mockUseChannelSettings = useChannelSettingsModule.default as jest.Mock;
41+
42+
const renderComponent = (mockState = {}, mockActions = {}) => {
43+
mockUseChannelSettings.mockReturnValue({
44+
state: { ...defaultMockState, ...mockState },
45+
actions: { ...defaultMockActions, ...mockActions },
46+
});
47+
48+
return render(
49+
<SendbirdSdkContext.Provider value={{ config: { isOnline: true } } as any}>
50+
<LocalizationContext.Provider value={mockLocalizationContext as any}>
51+
<ChannelSettingsUI />
52+
</LocalizationContext.Provider>
53+
</SendbirdSdkContext.Provider>,
54+
);
55+
};
56+
57+
beforeEach(() => {
58+
jest.clearAllMocks();
59+
});
60+
61+
it('renders all necessary texts correctly', () => {
62+
renderComponent();
63+
64+
expect(screen.getByText(mockChannelName)).toBeInTheDocument();
65+
expect(screen.getByText(mockStringSet.CHANNEL_SETTING__HEADER__TITLE)).toBeInTheDocument();
66+
expect(screen.getByText(mockStringSet.CHANNEL_SETTING__LEAVE_CHANNEL__TITLE)).toBeInTheDocument();
67+
});
68+
69+
it('does not display texts when loading or invalidChannel is true', () => {
70+
// Case 1: loading = true
71+
renderComponent({ loading: true });
72+
73+
expect(screen.queryByText(mockChannelName)).not.toBeInTheDocument();
74+
expect(screen.queryByText(mockStringSet.CHANNEL_SETTING__HEADER__TITLE)).not.toBeInTheDocument();
75+
expect(screen.queryByText(mockStringSet.CHANNEL_SETTING__LEAVE_CHANNEL__TITLE)).not.toBeInTheDocument();
76+
77+
// Clear the render for the next case
78+
jest.clearAllMocks();
79+
renderComponent({ invalidChannel: true });
80+
81+
// Case 2: invalidChannel = true
82+
expect(screen.queryByText(mockChannelName)).not.toBeInTheDocument();
83+
expect(screen.queryByText(mockStringSet.CHANNEL_SETTING__HEADER__TITLE)).toBeInTheDocument(); // render Header
84+
expect(screen.queryByText(mockStringSet.CHANNEL_SETTING__LEAVE_CHANNEL__TITLE)).not.toBeInTheDocument();
85+
});
86+
87+
it('calls setChannel with the correct channel object', () => {
88+
const setChannel = jest.fn();
89+
renderComponent({}, { setChannel });
90+
91+
const newChannel = { name: 'New Channel', members: [] };
92+
setChannel(newChannel);
93+
94+
expect(setChannel).toHaveBeenCalledWith(newChannel);
95+
});
96+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { GroupChannelHandler } from '@sendbird/chat/groupChannel';
3+
import { useChannelHandler } from '../context/hooks/useChannelHandler';
4+
5+
// jest.mock('../../../utils/uuid', () => ({
6+
// v4: jest.fn(() => 'mock-uuid'),
7+
// }));
8+
9+
const mockLogger = {
10+
warning: jest.fn(),
11+
info: jest.fn(),
12+
error: jest.fn(),
13+
};
14+
15+
const mockSdk = {
16+
groupChannel: {
17+
addGroupChannelHandler: jest.fn(),
18+
removeGroupChannelHandler: jest.fn(),
19+
},
20+
};
21+
22+
const mockForceUpdateUI = jest.fn();
23+
24+
beforeEach(() => {
25+
jest.clearAllMocks();
26+
});
27+
28+
describe('useChannelHandler', () => {
29+
it('logs a warning if SDK or groupChannel is not available', () => {
30+
renderHook(() => useChannelHandler({ sdk: null, channelUrl: 'test-channel', logger: mockLogger, forceUpdateUI: mockForceUpdateUI }),
31+
);
32+
33+
expect(mockLogger.warning).toHaveBeenCalledWith('ChannelSettings: SDK or GroupChannelModule is not available');
34+
});
35+
36+
it('adds and removes GroupChannelHandler correctly', () => {
37+
const { unmount } = renderHook(() => useChannelHandler({
38+
sdk: mockSdk,
39+
channelUrl: 'test-channel',
40+
logger: mockLogger,
41+
forceUpdateUI: mockForceUpdateUI,
42+
}),
43+
);
44+
45+
expect(mockSdk.groupChannel.addGroupChannelHandler).toHaveBeenCalledWith(
46+
expect.any(String),
47+
expect.any(GroupChannelHandler),
48+
);
49+
50+
act(() => {
51+
unmount();
52+
});
53+
54+
expect(mockSdk.groupChannel.removeGroupChannelHandler).toHaveBeenCalled();
55+
});
56+
57+
it('calls forceUpdateUI when a user leaves the channel', () => {
58+
mockSdk.groupChannel.addGroupChannelHandler.mockImplementation((_, handler) => {
59+
handler.onUserLeft({ url: 'test-channel' }, { userId: 'user1' });
60+
});
61+
62+
renderHook(() => useChannelHandler({ sdk: mockSdk, channelUrl: 'test-channel', logger: mockLogger, forceUpdateUI: mockForceUpdateUI }),
63+
);
64+
65+
expect(mockForceUpdateUI).toHaveBeenCalled();
66+
});
67+
68+
it('calls forceUpdateUI when a user is banned from the channel', () => {
69+
mockSdk.groupChannel.addGroupChannelHandler.mockImplementation((_, handler) => {
70+
handler.onUserBanned({ url: 'test-channel', isGroupChannel: () => true }, { userId: 'user1' });
71+
});
72+
73+
renderHook(() => useChannelHandler({ sdk: mockSdk, channelUrl: 'test-channel', logger: mockLogger, forceUpdateUI: mockForceUpdateUI }),
74+
);
75+
76+
expect(mockForceUpdateUI).toHaveBeenCalled();
77+
});
78+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { useChannelSettings } from '../context/useChannelSettings';
3+
import { useChannelSettingsContext } from '../context/ChannelSettingsProvider';
4+
import type { GroupChannel } from '@sendbird/chat/groupChannel';
5+
6+
jest.mock('../context/ChannelSettingsProvider', () => ({
7+
useChannelSettingsContext: jest.fn(),
8+
}));
9+
10+
const mockStore = {
11+
getState: jest.fn(),
12+
setState: jest.fn(),
13+
subscribe: jest.fn(() => jest.fn()),
14+
};
15+
16+
const mockChannel: GroupChannel = {
17+
url: 'test-channel',
18+
name: 'Test Channel',
19+
} as GroupChannel;
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
(useChannelSettingsContext as jest.Mock).mockReturnValue(mockStore);
24+
});
25+
26+
describe('useChannelSettings', () => {
27+
it('throws an error if used outside of ChannelSettingsProvider', () => {
28+
(useChannelSettingsContext as jest.Mock).mockReturnValueOnce(null);
29+
30+
const { result } = renderHook(() => useChannelSettings());
31+
32+
expect(result.error).toEqual(
33+
new Error('useChannelSettings must be used within a ChannelSettingsProvider'),
34+
);
35+
});
36+
37+
it('returns the correct initial state', () => {
38+
const initialState = {
39+
channel: null,
40+
loading: false,
41+
invalidChannel: false,
42+
};
43+
44+
mockStore.getState.mockReturnValue(initialState);
45+
46+
const { result } = renderHook(() => useChannelSettings());
47+
48+
expect(result.current.state).toEqual(initialState);
49+
});
50+
51+
it('calls setChannel with the correct channel object', () => {
52+
const { result } = renderHook(() => useChannelSettings());
53+
54+
act(() => {
55+
result.current.actions.setChannel(mockChannel);
56+
});
57+
58+
expect(mockStore.setState).toHaveBeenCalledWith(expect.any(Function));
59+
const stateSetter = mockStore.setState.mock.calls[0][0];
60+
expect(stateSetter({})).toEqual({ channel: mockChannel });
61+
});
62+
63+
it('calls setLoading with the correct value', () => {
64+
const { result } = renderHook(() => useChannelSettings());
65+
66+
act(() => {
67+
result.current.actions.setLoading(true);
68+
});
69+
70+
expect(mockStore.setState).toHaveBeenCalledWith(expect.any(Function));
71+
const stateSetter = mockStore.setState.mock.calls[0][0];
72+
expect(stateSetter({})).toEqual({ loading: true });
73+
});
74+
75+
it('calls setInvalid with the correct value', () => {
76+
const { result } = renderHook(() => useChannelSettings());
77+
78+
act(() => {
79+
result.current.actions.setInvalid(true);
80+
});
81+
82+
expect(mockStore.setState).toHaveBeenCalledWith(expect.any(Function));
83+
const stateSetter = mockStore.setState.mock.calls[0][0];
84+
expect(stateSetter({})).toEqual({ invalidChannel: true });
85+
});
86+
});

0 commit comments

Comments
 (0)