Skip to content

Commit c4a057d

Browse files
AhyoungRyugit-babelHoonBaek
authored
[CLNP-6019][CLNP-6023][CLNP-6024] Add state management migration tests (#1279)
Addresses - https://sendbird.atlassian.net/browse/CLNP-6019 - https://sendbird.atlassian.net/browse/CLNP-6023 - https://sendbird.atlassian.net/browse/CLNP-6024 to verify legacy Provider props compatibility and test context hook return values. This will help validate that the state management migration doesn't break existing implementations and maintains backward compatibility. --------- Co-authored-by: junyoung.lim <[email protected]> Co-authored-by: HoonBaek <[email protected]>
1 parent 9200909 commit c4a057d

File tree

9 files changed

+1192
-1
lines changed

9 files changed

+1192
-1
lines changed

jest-setup.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import './__mocks__/intersectionObserverMock';
2+
import '@testing-library/jest-dom';
23

34
const { JSDOM } = require('jsdom');
45

@@ -24,4 +25,44 @@ global.requestAnimationFrame = function (callback) {
2425
global.cancelAnimationFrame = function (id) {
2526
clearTimeout(id);
2627
};
28+
29+
// MediaRecorder and MediaRecorder.isTypeSupported are used within SendbirdProvider's internal logic.
30+
class MockMediaRecorder {
31+
static isTypeSupported(type) {
32+
const supportedMimeTypes = ['audio/webm', 'audio/wav'];
33+
return supportedMimeTypes.includes(type);
34+
}
35+
36+
constructor() {
37+
this.state = 'inactive';
38+
this.ondataavailable = null;
39+
this.onerror = null;
40+
this.onpause = null;
41+
this.onresume = null;
42+
this.onstart = null;
43+
this.onstop = null;
44+
}
45+
46+
start() {
47+
this.state = 'recording';
48+
if (this.onstart) this.onstart(new Event('start'));
49+
}
50+
51+
stop() {
52+
this.state = 'inactive';
53+
if (this.onstop) this.onstop(new Event('stop'));
54+
}
55+
56+
pause() {
57+
this.state = 'paused';
58+
if (this.onpause) this.onpause(new Event('pause'));
59+
}
60+
61+
resume() {
62+
this.state = 'recording';
63+
if (this.onresume) this.onresume(new Event('resume'));
64+
}
65+
}
66+
global.MediaRecorder = MockMediaRecorder;
67+
2768
copyProps(window, global);
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import { render, renderHook, screen } from '@testing-library/react';
4+
import SendbirdProvider, { SendbirdProviderProps } from './Sendbird';
5+
import useSendbirdStateContext from '../hooks/useSendbirdStateContext';
6+
import { match } from 'ts-pattern';
7+
import { DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT, DEFAULT_UPLOAD_SIZE_LIMIT } from '../utils/consts';
8+
9+
const mockProps: SendbirdProviderProps = {
10+
appId: 'test-app-id',
11+
userId: 'test-user-id',
12+
accessToken: 'test-access-token',
13+
customApiHost: 'api.sendbird.com',
14+
customWebSocketHost: 'socket.sendbird.com',
15+
theme: 'light',
16+
config: { logLevel: 'info', isREMUnitEnabled: true },
17+
nickname: 'test-nickname',
18+
colorSet: { primary: '#000' },
19+
stringSet: { CHANNEL_PREVIEW_MOBILE_LEAVE: 'Leave Channel' },
20+
dateLocale: {} as Locale,
21+
profileUrl: 'test-profile-url',
22+
voiceRecord: { maxRecordingTime: 3000, minRecordingTime: 1000 },
23+
userListQuery: () => ({ limit: 10 } as any),
24+
imageCompression: { compressionRate: 0.8, outputFormat: 'png' },
25+
allowProfileEdit: true,
26+
disableMarkAsDelivered: false,
27+
breakpoint: '768px',
28+
htmlTextDirection: 'ltr',
29+
forceLeftToRightMessageLayout: true,
30+
uikitOptions: { groupChannel: { enableReactions: true } },
31+
isUserIdUsedForNickname: true,
32+
sdkInitParams: { localCacheEnabled: true },
33+
customExtensionParams: { feature: 'custom' },
34+
isMultipleFilesMessageEnabled: true,
35+
renderUserProfile: jest.fn(),
36+
onStartDirectMessage: jest.fn(),
37+
onUserProfileMessage: jest.fn(),
38+
eventHandlers: {},
39+
children: <div>Test Child</div>,
40+
};
41+
42+
const mockDisconnect = jest.fn();
43+
const mockConnect = jest.fn();
44+
const mockUpdateCurrentUserInfo = jest.fn();
45+
46+
/**
47+
* Mocking Sendbird SDK
48+
* sdk.connect causes DOMException issue in jest.
49+
* Because it retries many times to connect indexDB.
50+
*/
51+
jest.mock('@sendbird/chat', () => {
52+
return {
53+
__esModule: true,
54+
default: jest.fn().mockImplementation(() => {
55+
return {
56+
connect: mockConnect.mockResolvedValue({
57+
userId: 'test-user-id',
58+
nickname: 'test-nickname',
59+
profileUrl: 'test-profile-url',
60+
}),
61+
disconnect: mockDisconnect.mockResolvedValue(null),
62+
updateCurrentUserInfo: mockUpdateCurrentUserInfo.mockResolvedValue(null),
63+
GroupChannel: { createMyGroupChannelListQuery: jest.fn() },
64+
appInfo: {
65+
uploadSizeLimit: 1024 * 1024 * 5, // 5MB
66+
multipleFilesMessageFileCountLimit: 10,
67+
},
68+
};
69+
}),
70+
};
71+
});
72+
73+
describe('SendbirdProvider Props & Context Interface Validation', () => {
74+
const originalConsoleError = console.error;
75+
let originalFetch;
76+
77+
beforeAll(() => {
78+
originalFetch = global.fetch;
79+
global.fetch = jest.fn(() => Promise.resolve({ ok: true }));
80+
81+
console.error = jest.fn((...args) => {
82+
if (typeof args[0] === 'string'
83+
&& (args[0].includes('Warning: An update to %s inside a test was not wrapped in act')
84+
|| args[0].includes('WebSocket connection'))) {
85+
return;
86+
}
87+
originalConsoleError(...args);
88+
});
89+
});
90+
91+
afterAll(() => {
92+
console.error = originalConsoleError;
93+
global.fetch = originalFetch;
94+
});
95+
96+
beforeEach(() => {
97+
jest.clearAllMocks();
98+
mockConnect.mockClear();
99+
mockDisconnect.mockClear();
100+
mockUpdateCurrentUserInfo.mockClear();
101+
102+
global.MediaRecorder = {
103+
isTypeSupported: jest.fn((type) => {
104+
const supportedMimeTypes = ['audio/webm', 'audio/wav'];
105+
return supportedMimeTypes.includes(type);
106+
}),
107+
} as any;
108+
109+
window.matchMedia = jest.fn().mockImplementation((query) => ({
110+
matches: false,
111+
media: query,
112+
onchange: null,
113+
addListener: jest.fn(),
114+
removeListener: jest.fn(),
115+
addEventListener: jest.fn(),
116+
removeEventListener: jest.fn(),
117+
dispatchEvent: jest.fn(),
118+
}));
119+
});
120+
121+
it('should accept all legacy props without type errors', async () => {
122+
const { rerender } = render(
123+
<SendbirdProvider {...mockProps}>
124+
{mockProps.children}
125+
</SendbirdProvider>,
126+
);
127+
128+
rerender(
129+
<SendbirdProvider {...mockProps}>
130+
{mockProps.children}
131+
</SendbirdProvider>,
132+
);
133+
});
134+
135+
it('should provide all expected keys in context', () => {
136+
const expectedKeys = [
137+
'config',
138+
'stores',
139+
'dispatchers',
140+
'eventHandlers',
141+
'emojiManager',
142+
'utils',
143+
];
144+
145+
const TestComponent = () => {
146+
const context = useSendbirdStateContext();
147+
return (
148+
<div>
149+
{Object.keys(context).map((key) => (
150+
<div key={key} data-testid={`context-${key}`}>
151+
{match(context[key])
152+
.with('function', () => 'function')
153+
.with('object', () => JSON.stringify(context[key]))
154+
.with('string', () => String(context[key]))
155+
.otherwise(() => 'unknown')}
156+
</div>
157+
))}
158+
</div>
159+
);
160+
};
161+
162+
render(
163+
<SendbirdProvider appId="test-app-id" userId="test-user-id">
164+
<TestComponent />
165+
</SendbirdProvider>,
166+
);
167+
168+
expectedKeys.forEach((key) => {
169+
const element = screen.getByTestId(`context-${key}`);
170+
expect(element).toBeInTheDocument();
171+
});
172+
});
173+
174+
it('should pass all expected values to the config object', () => {
175+
const mockProps: SendbirdProviderProps = {
176+
appId: 'test-app-id',
177+
userId: 'test-user-id',
178+
theme: 'light',
179+
allowProfileEdit: true,
180+
disableMarkAsDelivered: false,
181+
voiceRecord: { maxRecordingTime: 3000, minRecordingTime: 1000 },
182+
config: {
183+
userMention: { maxMentionCount: 5, maxSuggestionCount: 10 },
184+
},
185+
imageCompression: { compressionRate: 0.7, resizingWidth: 800 },
186+
htmlTextDirection: 'ltr',
187+
forceLeftToRightMessageLayout: false,
188+
isMultipleFilesMessageEnabled: true,
189+
};
190+
191+
const wrapper = ({ children }) => (
192+
<SendbirdProvider {...mockProps}>{children}</SendbirdProvider>
193+
);
194+
195+
const { result } = renderHook(() => useSendbirdStateContext(), { wrapper });
196+
197+
const config = result.current.config;
198+
199+
expect(config.appId).toBe(mockProps.appId);
200+
expect(config.userId).toBe(mockProps.userId);
201+
expect(config.theme).toBe(mockProps.theme);
202+
expect(config.allowProfileEdit).toBe(mockProps.allowProfileEdit);
203+
expect(config.disableMarkAsDelivered).toBe(mockProps.disableMarkAsDelivered);
204+
expect(config.voiceRecord.maxRecordingTime).toBe(mockProps.voiceRecord?.maxRecordingTime);
205+
expect(config.voiceRecord.minRecordingTime).toBe(mockProps.voiceRecord?.minRecordingTime);
206+
expect(config.userMention.maxMentionCount).toBe(mockProps.config?.userMention?.maxMentionCount);
207+
expect(config.userMention.maxSuggestionCount).toBe(mockProps.config?.userMention?.maxSuggestionCount);
208+
expect(config.imageCompression.compressionRate).toBe(mockProps.imageCompression?.compressionRate);
209+
expect(config.imageCompression.resizingWidth).toBe(mockProps.imageCompression?.resizingWidth);
210+
expect(config.htmlTextDirection).toBe(mockProps.htmlTextDirection);
211+
expect(config.forceLeftToRightMessageLayout).toBe(mockProps.forceLeftToRightMessageLayout);
212+
expect(config.isMultipleFilesMessageEnabled).toBe(mockProps.isMultipleFilesMessageEnabled);
213+
214+
// Default values validation
215+
expect(config.uikitUploadSizeLimit).toBe(DEFAULT_UPLOAD_SIZE_LIMIT);
216+
expect(config.uikitMultipleFilesMessageLimit).toBe(DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT);
217+
expect(config.logger).toBeDefined();
218+
expect(config.pubSub).toBeDefined();
219+
expect(config.markAsReadScheduler).toBeDefined();
220+
expect(config.markAsDeliveredScheduler).toBeDefined();
221+
});
222+
223+
it('should handle optional and default values correctly', () => {
224+
const wrapper = ({ children }) => (
225+
<SendbirdProvider {...mockProps} appId="test-app-id" userId="test-user-id">
226+
{children}
227+
</SendbirdProvider>
228+
);
229+
230+
const { result } = renderHook(() => useSendbirdStateContext(), { wrapper });
231+
232+
expect(result.current.config.pubSub).toBeDefined();
233+
expect(result.current.config.logger).toBeDefined();
234+
expect(result.current.config.imageCompression.compressionRate).toBe(0.8);
235+
});
236+
});

0 commit comments

Comments
 (0)