Skip to content
Merged
5 changes: 3 additions & 2 deletions src/lib/Sendbird/context/hooks/useSendbird.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { useContext, useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { SendbirdError, User } from '@sendbird/chat';

import { SendbirdContext } from '../SendbirdContext';
Expand All @@ -11,7 +12,7 @@ export const useSendbird = () => {
const store = useContext(SendbirdContext);
if (!store) throw new Error(NO_CONTEXT_ERROR);

const state = useSyncExternalStore(store.subscribe, store.getState);
const state: SendbirdState = useSyncExternalStore(store.subscribe, store.getState);
const actions = useMemo(() => ({
/* Example: How to set the state basically */
// exampleAction: () => {
Expand Down
138 changes: 82 additions & 56 deletions src/lib/SendbirdProvider.migration.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
/* eslint-disable no-console */
import React from 'react';
import React, { act } from 'react';
import { render, renderHook, screen } from '@testing-library/react';
import SendbirdProvider, { SendbirdProviderProps } from './Sendbird';
import useSendbirdStateContext from './Sendbird/context/hooks/useSendbirdStateContext';
import { match } from 'ts-pattern';
import { DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT, DEFAULT_UPLOAD_SIZE_LIMIT } from '../utils/consts';

jest.mock('@sendbird/chat', () => {
const mockConnect = jest.fn().mockResolvedValue({
userId: 'test-user-id',
nickname: 'test-nickname',
profileUrl: 'test-profile-url',
});
const mockDisconnect = jest.fn().mockResolvedValue(null);
const mockUpdateCurrentUserInfo = jest.fn().mockResolvedValue(null);
const mockAddExtension = jest.fn().mockReturnThis();
const mockAddSendbirdExtensions = jest.fn().mockReturnThis();
const mockGetMessageTemplatesByToken = jest.fn().mockResolvedValue({
hasMore: false,
token: null,
templates: [],
});

const mockSdk = {
init: jest.fn().mockImplementation(() => mockSdk),
connect: mockConnect,
disconnect: mockDisconnect,
updateCurrentUserInfo: mockUpdateCurrentUserInfo,
addExtension: mockAddExtension,
addSendbirdExtensions: mockAddSendbirdExtensions,
GroupChannel: { createMyGroupChannelListQuery: jest.fn() },
message: {
getMessageTemplatesByToken: mockGetMessageTemplatesByToken,
},
appInfo: {
uploadSizeLimit: 1024 * 1024 * 5,
multipleFilesMessageFileCountLimit: 10,
},
};

return {
__esModule: true,
default: mockSdk,
SendbirdProduct: {
UIKIT_CHAT: 'UIKIT_CHAT',
},
SendbirdPlatform: {
JS: 'JS',
},
DeviceOsPlatform: {
WEB: 'WEB',
MOBILE_WEB: 'MOBILE_WEB',
},
};
});

const mockProps: SendbirdProviderProps = {
appId: 'test-app-id',
userId: 'test-user-id',
Expand Down Expand Up @@ -39,37 +88,6 @@ const mockProps: SendbirdProviderProps = {
children: <div>Test Child</div>,
};

const mockDisconnect = jest.fn();
const mockConnect = jest.fn();
const mockUpdateCurrentUserInfo = jest.fn();

/**
* Mocking Sendbird SDK
* sdk.connect causes DOMException issue in jest.
* Because it retries many times to connect indexDB.
*/
jest.mock('@sendbird/chat', () => {
return {
__esModule: true,
default: jest.fn().mockImplementation(() => {
return {
connect: mockConnect.mockResolvedValue({
userId: 'test-user-id',
nickname: 'test-nickname',
profileUrl: 'test-profile-url',
}),
disconnect: mockDisconnect.mockResolvedValue(null),
updateCurrentUserInfo: mockUpdateCurrentUserInfo.mockResolvedValue(null),
GroupChannel: { createMyGroupChannelListQuery: jest.fn() },
appInfo: {
uploadSizeLimit: 1024 * 1024 * 5, // 5MB
multipleFilesMessageFileCountLimit: 10,
},
};
}),
};
});

describe('SendbirdProvider Props & Context Interface Validation', () => {
const originalConsoleError = console.error;
let originalFetch;
Expand All @@ -95,9 +113,6 @@ describe('SendbirdProvider Props & Context Interface Validation', () => {

beforeEach(() => {
jest.clearAllMocks();
mockConnect.mockClear();
mockDisconnect.mockClear();
mockUpdateCurrentUserInfo.mockClear();

global.MediaRecorder = {
isTypeSupported: jest.fn((type) => {
Expand All @@ -119,24 +134,27 @@ describe('SendbirdProvider Props & Context Interface Validation', () => {
});

it('should accept all legacy props without type errors', async () => {
const { rerender } = render(
<SendbirdProvider {...mockProps}>
{mockProps.children}
</SendbirdProvider>,
);
const { rerender } = await act(async () => (
render(
<SendbirdProvider {...mockProps}>
{mockProps.children}
</SendbirdProvider>,
)
));

rerender(
<SendbirdProvider {...mockProps}>
{mockProps.children}
</SendbirdProvider>,
);
await act(async () => (
rerender(
<SendbirdProvider {...mockProps}>
{mockProps.children}
</SendbirdProvider>,
)
));
});

it('should provide all expected keys in context', () => {
it('should provide all expected keys in context', async () => {
const expectedKeys = [
'config',
'stores',
'dispatchers',
'eventHandlers',
'emojiManager',
'utils',
Expand All @@ -159,19 +177,21 @@ describe('SendbirdProvider Props & Context Interface Validation', () => {
);
};

render(
<SendbirdProvider appId="test-app-id" userId="test-user-id">
<TestComponent />
</SendbirdProvider>,
);
await act(() => (
render(
<SendbirdProvider appId="test-app-id" userId="test-user-id">
<TestComponent />
</SendbirdProvider>,
)
));

expectedKeys.forEach((key) => {
const element = screen.getByTestId(`context-${key}`);
expect(element).toBeInTheDocument();
});
});

it('should pass all expected values to the config object', () => {
it('should pass all expected values to the config object', async () => {
const mockProps: SendbirdProviderProps = {
appId: 'test-app-id',
userId: 'test-user-id',
Expand All @@ -192,7 +212,10 @@ describe('SendbirdProvider Props & Context Interface Validation', () => {
<SendbirdProvider {...mockProps}>{children}</SendbirdProvider>
);

const { result } = renderHook(() => useSendbirdStateContext(), { wrapper });
let result;
await act(async () => {
result = renderHook(() => useSendbirdStateContext(), { wrapper }).result;
});

const config = result.current.config;

Expand Down Expand Up @@ -220,14 +243,17 @@ describe('SendbirdProvider Props & Context Interface Validation', () => {
expect(config.markAsDeliveredScheduler).toBeDefined();
});

it('should handle optional and default values correctly', () => {
it('should handle optional and default values correctly', async () => {
const wrapper = ({ children }) => (
<SendbirdProvider {...mockProps} appId="test-app-id" userId="test-user-id">
{children}
</SendbirdProvider>
);

const { result } = renderHook(() => useSendbirdStateContext(), { wrapper });
let result;
await act(async () => {
result = renderHook(() => useSendbirdStateContext(), { wrapper }).result;
});

expect(result.current.config.pubSub).toBeDefined();
expect(result.current.config.logger).toBeDefined();
Expand Down
2 changes: 1 addition & 1 deletion src/lib/hooks/useMessageTemplateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export default function useMessageTemplateUtils({
}, [
actions.upsertMessageTemplates,
actions.upsertWaitingTemplateKeys,
sdk.message?.getMessageTemplatesByToken,
sdk?.message?.getMessageTemplatesByToken,
]);
return {
getCachedTemplate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,24 @@ const mockLogger = {
error: jest.fn(),
};

const mockStore = {
getState: jest.fn(),
setState: jest.fn(),
subscribe: jest.fn(() => jest.fn()),
};

const initialState = {
channelUrl: 'test-channel',
onCloseClick: undefined,
onLeaveChannel: undefined,
onChannelModified: undefined,
onBeforeUpdateChannel: undefined,
renderUserListItem: undefined,
queries: undefined,
overrideInviteUser: undefined,
channel: null,
loading: false,
invalidChannel: false,
forceUpdateUI: expect.any(Function),
setChannelUpdateId: expect.any(Function),
};

describe('ChannelSettingsProvider', () => {
let wrapper;

beforeEach(() => {
mockStore.getState.mockReturnValue(initialState);
useSendbird.mockReturnValue({
state: {
stores: { sdkStore: { sdk: {}, initialized: true } },
Expand All @@ -51,10 +49,13 @@ describe('ChannelSettingsProvider', () => {
jest.clearAllMocks();
});

it('provides the correct initial state', () => {
it('provides the correct initial state and actions', () => {
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });

expect(result.current.getState()).toEqual(expect.objectContaining(initialState));
expect(result.current.channelUrl).toBe(initialState.channelUrl);
expect(result.current.channel).toBe(initialState.channel);
expect(result.current.loading).toBe(initialState.loading);
expect(result.current.invalidChannel).toBe(initialState.invalidChannel);
});

it('logs a warning if SDK is not initialized', () => {
Expand All @@ -66,32 +67,29 @@ describe('ChannelSettingsProvider', () => {
});

renderHook(() => useChannelSettingsContext(), { wrapper });

expect(mockLogger.warning).toHaveBeenCalledWith('ChannelSettings: SDK or GroupChannelModule is not available');
});

it('updates state correctly when setChannelUpdateId is called', async () => {
it('updates channel state correctly', async () => {
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });
const newChannel = { url: 'new-channel' } as any;

await act(async () => {
result.current.setState({ channelUrl: 'new-channel' });
await waitForStateUpdate();
expect(result.current.getState().channelUrl).toBe('new-channel');
result.current.setChannel(newChannel);
});

expect(result.current.channel).toEqual(newChannel);
});

it('maintains other state values when channel changes', async () => {
it('maintains loading and invalid states', async () => {
const { result } = renderHook(() => useChannelSettingsContext(), { wrapper });

await act(async () => {
result.current.setState({ channel: { name: 'Updated Channel' } });
await waitForStateUpdate();
const updatedState = result.current.getState();
expect(updatedState.channel).toEqual({ name: 'Updated Channel' });
expect(updatedState.loading).toBe(false);
expect(updatedState.invalidChannel).toBe(false);
result.current.setLoading(true);
result.current.setInvalid(true);
});
});

const waitForStateUpdate = () => new Promise(resolve => { setTimeout(resolve, 0); });
expect(result.current.loading).toBe(true);
expect(result.current.invalidChannel).toBe(true);
});
});
Loading