MM-59740 + others - Add tests for various files in hooks/ (#8448)

* Add tests for hooks/android_back_handler

* Add tests for hooks/gallery

* Add tests for hooks/files

* Add tests for hooks/emoji_category_bar

* Add tests for hooks/markdown

* Increase more coverage for hooks/gallery.ts

---------

Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
This commit is contained in:
Joram Wilander 2025-01-21 08:41:52 -05:00 committed by GitHub
parent 2e027d2d98
commit e8ed761d30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 726 additions and 2 deletions

View file

@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook} from '@testing-library/react-hooks';
import {BackHandler} from 'react-native';
import NavigationStore from '@store/navigation_store';
import useAndroidHardwareBackHandler from './android_back_handler';
jest.mock('react-native', () => ({
BackHandler: {
addEventListener: jest.fn(),
},
}));
jest.mock('@store/navigation_store', () => ({
getVisibleScreen: jest.fn(),
}));
test('useAndroidHardwareBackHandler - calls callback when visible screen matches componentId', () => {
const componentId = 'About';
const callback = jest.fn();
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValue(componentId);
renderHook(() => useAndroidHardwareBackHandler(componentId, callback));
const hardwareBackPressHandler = (BackHandler.addEventListener as jest.Mock).mock.calls[0][1];
hardwareBackPressHandler();
expect(callback).toHaveBeenCalled();
});
test('useAndroidHardwareBackHandler - does not call callback when visible screen does not match componentId', () => {
const componentId = 'About';
const callback = jest.fn();
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValue('otherScreen');
renderHook(() => useAndroidHardwareBackHandler(componentId, callback));
const hardwareBackPressHandler = (BackHandler.addEventListener as jest.Mock).mock.calls[0][1];
hardwareBackPressHandler();
expect(callback).not.toHaveBeenCalled();
});
test('useAndroidHardwareBackHandler - removes event listener on unmount', () => {
const componentId = 'About';
const callback = jest.fn();
const remove = jest.fn();
(BackHandler.addEventListener as jest.Mock).mockReturnValue({remove});
const {unmount} = renderHook(() => useAndroidHardwareBackHandler(componentId, callback));
unmount();
expect(remove).toHaveBeenCalled();
});

View file

@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook, act} from '@testing-library/react-hooks';
import {BehaviorSubject} from 'rxjs';
import {
selectEmojiCategoryBarSection,
setEmojiCategoryBarSection,
setEmojiCategoryBarIcons,
setEmojiSkinTone,
useEmojiCategoryBar,
useEmojiSkinTone,
} from './emoji_category_bar';
describe('EmojiCategoryBar', () => {
const mockSubject = {
next: jest.fn(),
value: {
icons: undefined,
currentIndex: 0,
selectedIndex: undefined,
skinTone: 'default',
},
subscribe: jest.fn().mockImplementation((callback) => {
callback(mockSubject.value);
return {unsubscribe: jest.fn()};
}),
};
beforeEach(() => {
jest.spyOn(BehaviorSubject.prototype, 'next').mockImplementation(mockSubject.next);
jest.spyOn(BehaviorSubject.prototype, 'subscribe').mockImplementation(mockSubject.subscribe);
jest.spyOn(BehaviorSubject.prototype, 'value', 'get').mockReturnValue(mockSubject.value);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('setEmojiSkinTone', () => {
it('should update the skin tone in the state', () => {
setEmojiSkinTone('light');
expect(mockSubject.next).toHaveBeenCalledWith({
...mockSubject.value,
skinTone: 'light',
});
});
});
describe('useEmojiSkinTone', () => {
it('should return the current skin tone', () => {
const {result} = renderHook(() => useEmojiSkinTone());
expect(result.current).toBe('default');
});
it('should update the skin tone when it changes', () => {
const {result} = renderHook(() => useEmojiSkinTone());
act(() => {
mockSubject.subscribe.mock.calls[0][0]({skinTone: 'dark'});
});
expect(result.current).toBe('dark');
});
});
describe('selectEmojiCategoryBarSection', () => {
it('should update the selected index in the state', () => {
selectEmojiCategoryBarSection(1);
expect(mockSubject.next).toHaveBeenCalledWith({
...mockSubject.value,
selectedIndex: 1,
});
});
});
describe('setEmojiCategoryBarSection', () => {
it('should update the current index in the state', () => {
setEmojiCategoryBarSection(1);
expect(mockSubject.next).toHaveBeenCalledWith({
...mockSubject.value,
currentIndex: 1,
});
});
});
describe('setEmojiCategoryBarIcons', () => {
it('should update the icons in the state', () => {
const icons = [{key: 'smile', name: 'smile', icon: 'smile'}];
setEmojiCategoryBarIcons(icons);
expect(mockSubject.next).toHaveBeenCalledWith({
...mockSubject.value,
icons,
});
});
});
describe('useEmojiCategoryBar', () => {
it('should return the current state', () => {
const {result} = renderHook(() => useEmojiCategoryBar());
expect(result.current).toEqual(mockSubject.value);
});
it('should update the state when it changes', () => {
const {result} = renderHook(() => useEmojiCategoryBar());
const newState = {
icons: [{name: 'smile', icon: 'smile'}],
currentIndex: 1,
selectedIndex: 2,
skinTone: 'dark',
};
act(() => {
mockSubject.subscribe.mock.calls[0][0](newState);
});
expect(result.current).toEqual(newState);
});
});
});

View file

@ -3,12 +3,16 @@
import {renderHook} from '@testing-library/react-hooks';
import {getLocalFileInfo} from '@actions/local/file';
import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file';
import {useServerUrl} from '@context/server';
import {mockFileInfo} from '@test/api_mocks/file';
import {isGif, isImage, isVideo} from '@utils/file';
import {getImageSize} from '@utils/gallery';
import {useImageAttachments} from './files';
import {useChannelBookmarkFiles, useImageAttachments} from './files';
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
jest.mock('@actions/remote/file', () => ({
buildFilePreviewUrl: jest.fn(),
@ -25,6 +29,9 @@ jest.mock('@context/server', () => ({
useServerUrl: jest.fn(),
}));
jest.mock('@utils/gallery');
jest.mock('@actions/local/file');
describe('useImageAttachments', () => {
const serverUrl = 'https://example.com';
@ -143,3 +150,42 @@ describe('useImageAttachments', () => {
]);
});
});
describe('useChannelBookmarkFiles', () => {
it('should fetch and set file info correctly', async () => {
const serverUrl = 'https://example.com';
(useServerUrl as jest.Mock).mockReturnValue(serverUrl);
const bookmarks = [
{fileId: '1', ownerId: 'user1'},
{fileId: '2', ownerId: 'user2'},
] as ChannelBookmarkModel[];
const file1 = {id: '1', localPath: 'path/to/image1', has_preview_image: true, toFileInfo: jest.fn().mockReturnValue({id: '1'})};
const file2 = {id: '2', localPath: 'path/to/video1', has_preview_image: false, toFileInfo: jest.fn().mockReturnValue({id: '2'})};
(getLocalFileInfo as jest.Mock).mockImplementation((url, id) => {
if (id === '1') {
return {file: file1};
} else if (id === '2') {
return {file: file2};
}
return {file: null};
});
(isImage as jest.Mock).mockImplementation((file) => file.id === '1');
(isVideo as jest.Mock).mockImplementation((file) => file.id === '2');
(isGif as jest.Mock).mockImplementation(() => false);
(buildFileUrl as jest.Mock).mockImplementation((url, id) => `${url}/files/${id}`);
(buildFilePreviewUrl as jest.Mock).mockImplementation((url, id) => `${url}/files/${id}/preview`);
(getImageSize as jest.Mock).mockImplementation(() => ({width: 100, height: 100}));
const {result, waitForNextUpdate} = renderHook(() => useChannelBookmarkFiles(bookmarks, true));
await waitForNextUpdate();
expect(result.current).toHaveLength(2);
expect(result.current[0].id).toBe('1');
expect(result.current[1].id).toBe('2');
});
});

398
app/hooks/gallery.test.ts Normal file
View file

@ -0,0 +1,398 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook, act} from '@testing-library/react-hooks';
import {useSharedValue} from 'react-native-reanimated';
import {useGallery} from '@context/gallery';
import {useGalleryControls, useGalleryItem, diff, useCreateAnimatedGestureHandler} from './gallery';
jest.mock('react-native-reanimated', () => ({
Easing: {
bezier: jest.fn(),
},
runOnJS: jest.fn((fn) => fn),
useAnimatedRef: jest.fn(() => ({})),
useAnimatedStyle: jest.fn((fn) => fn()),
useEvent: jest.fn(),
useSharedValue: jest.fn(),
withTiming: jest.fn(),
}));
jest.mock('@context/gallery', () => ({
useGallery: jest.fn(),
}));
jest.mock('react-native', () => ({
Platform: {
OS: 'ios',
},
}));
describe('gallery hooks', () => {
test('diff', () => {
const context = {} as {___diffs?: any};
const name = 'test';
const value = 10;
const result = diff(context, name, value);
expect(result).toBe(0);
expect(context.___diffs[name].stash).toBe(0);
expect(context.___diffs[name].prev).toBe(value);
});
test('diff should return 0 for the same value', () => {
const context = {} as {___diffs?: any};
const name = 'test';
const value = 10;
diff(context, name, value);
const result = diff(context, name, value);
expect(result).toBe(0);
expect(context.___diffs[name].stash).toBe(0);
expect(context.___diffs[name].prev).toBe(value);
});
describe('useCreateAnimatedGestureHandler', () => {
const handlers = {
onInit: jest.fn(),
onGesture: jest.fn(),
shouldHandleEvent: jest.fn(),
onEvent: jest.fn(),
shouldCancel: jest.fn(),
onEnd: jest.fn(),
onStart: jest.fn(),
onActive: jest.fn(),
onFail: jest.fn(),
onCancel: jest.fn(),
onFinish: jest.fn(),
beforeEach: jest.fn(),
afterEach: jest.fn(),
};
test('iOS and state is active', () => {
const context = {__initialized: false, _shouldSkip: false} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
expect(handler).toBeInstanceOf(Function);
const event = {
nativeEvent: {
state: 4,
oldState: 1,
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.onInit).toHaveBeenCalled();
expect(handlers.onGesture).toHaveBeenCalled();
expect(handlers.shouldHandleEvent).not.toHaveBeenCalled();
expect(handlers.onEvent).toHaveBeenCalled();
expect(handlers.shouldCancel).toHaveBeenCalled();
expect(handlers.onEnd).not.toHaveBeenCalled();
expect(handlers.onStart).not.toHaveBeenCalled();
expect(handlers.onActive).toHaveBeenCalled();
expect(handlers.onFail).not.toHaveBeenCalled();
expect(handlers.onCancel).not.toHaveBeenCalled();
expect(handlers.onFinish).not.toHaveBeenCalled();
expect(handlers.beforeEach).toHaveBeenCalled();
expect(handlers.afterEach).toHaveBeenCalled();
// Check the expected values of context
expect(context.__initialized).toBe(true);
expect(context._shouldSkip).toBe(false);
});
test('State is end, oldState is active', () => {
const context = {__initialized: false, _shouldSkip: false} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
expect(handler).toBeInstanceOf(Function);
const event = {
nativeEvent: {
state: 5,
oldState: 4,
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.onEnd).toHaveBeenCalled();
// Check the expected values of context
expect(context.__initialized).toBe(true);
expect(context._shouldCancel).toBeUndefined();
expect(context._shouldSkip).toBeUndefined();
});
test('State is began, oldState is active, velocity non-zero', () => {
const context = {__initialized: false, _shouldSkip: false} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
expect(handler).toBeInstanceOf(Function);
const event = {
nativeEvent: {
state: 2,
oldState: 4,
velocityX: 1,
velocityY: 1,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.shouldHandleEvent).toHaveBeenCalled();
// Check the expected values of context
expect(context.__initialized).toBe(true);
expect(context._shouldCancel).toBeUndefined();
expect(context._shouldSkip).toBeUndefined();
});
test('should skip handling event if shouldHandleEvent returns false', () => {
const context = {__initialized: false, _shouldSkip: undefined} as any;
handlers.shouldHandleEvent.mockReturnValueOnce(false);
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
expect(handler).toBeInstanceOf(Function);
const event = {
nativeEvent: {
state: 2,
velocityX: 1,
velocityY: 1,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.shouldHandleEvent).toHaveBeenCalled();
expect(context._shouldSkip).toBe(true);
});
test('handles shouldCancel returning true', () => {
const context = {__initialized: true, _shouldSkip: false} as any;
handlers.shouldCancel.mockReturnValueOnce(true);
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
const event = {
nativeEvent: {
state: 4, // ACTIVE
oldState: 2, // BEGAN
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.shouldCancel).toHaveBeenCalled();
expect(handlers.onEnd).toHaveBeenCalledWith(
event.nativeEvent,
context,
true, // cancelled = true
);
expect(handlers.onActive).not.toHaveBeenCalled();
});
test('skips event handling when context._shouldSkip is undefined', () => {
const context = {__initialized: true} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
const event = {
nativeEvent: {
state: 4, // ACTIVE
oldState: 3, // CANCELLED
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.onEvent).not.toHaveBeenCalled();
expect(handlers.onActive).not.toHaveBeenCalled();
});
test('handles CANCELLED state when oldState is ACTIVE', () => {
const context = {__initialized: true, _shouldSkip: false} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
const event = {
nativeEvent: {
state: 3, // CANCELLED
oldState: 4, // ACTIVE
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.onCancel).toHaveBeenCalled();
expect(handlers.onFinish).toHaveBeenCalledWith(
event.nativeEvent,
context,
true, // cancelled = true
);
});
test('handles FAILED state when oldState is ACTIVE', () => {
const context = {__initialized: true, _shouldSkip: false} as any;
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: context}));
const {result} = renderHook(() => useCreateAnimatedGestureHandler(handlers));
const handler = result.current;
const event = {
nativeEvent: {
state: 1, // FAILED
oldState: 4, // ACTIVE
velocityX: 0,
velocityY: 0,
},
};
act(() => {
handler(event.nativeEvent as any);
});
expect(handlers.onFail).toHaveBeenCalled();
expect(handlers.onFinish).toHaveBeenCalledWith(
event.nativeEvent,
context,
true, // failed = true
);
});
});
test('useGalleryControls', () => {
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: false}));
const {result} = renderHook(() => useGalleryControls());
act(() => {
result.current.setControlsHidden(true);
});
expect(result.current.controlsHidden.value).toBe(true);
act(() => {
result.current.setControlsHidden(false);
});
expect(result.current.controlsHidden.value).toBe(false);
});
test('useGalleryControls does not update controlsHidden when same value is set', () => {
const mockWithTiming = jest.fn();
(useSharedValue as jest.Mock).mockImplementationOnce(() => ({value: true}));
const {result} = renderHook(() => useGalleryControls());
act(() => {
result.current.setControlsHidden(true);
});
expect(mockWithTiming).not.toHaveBeenCalled();
});
test('useGalleryItem', () => {
const identifier = 'test';
const index = 0;
const onPress = jest.fn();
const gallery = {
sharedValues: {
opacity: {value: 1},
activeIndex: {value: 0},
},
registerItem: jest.fn(),
};
(useGallery as jest.Mock).mockReturnValue(gallery);
const {result} = renderHook(() => useGalleryItem(identifier, index, onPress));
expect(result.current.ref).toBeDefined();
expect(result.current.styles).toBeDefined();
expect(result.current.onGestureEvent).toBeInstanceOf(Function);
act(() => {
result.current.onGestureEvent();
});
expect(gallery.sharedValues.activeIndex.value).toBe(index);
expect(onPress).toHaveBeenCalledWith(identifier, index);
});
test('useGalleryItem updates activeIndex when onGestureEvent is triggered', () => {
const identifier = 'test';
const index = 0;
const onPress = jest.fn();
const gallery = {
sharedValues: {
opacity: {value: 1},
activeIndex: {value: 1}, // Initial value different from index
},
registerItem: jest.fn(),
};
(useGallery as jest.Mock).mockReturnValue(gallery);
const {result} = renderHook(() => useGalleryItem(identifier, index, onPress));
expect(result.current.styles.opacity).toBe(1);
act(() => {
result.current.onGestureEvent();
});
expect(gallery.sharedValues.activeIndex.value).toBe(index);
expect(onPress).toHaveBeenCalledWith(identifier, index);
});
});

View file

@ -14,7 +14,7 @@ import {useGallery} from '@context/gallery';
import type {Context, GestureHandlers, OnGestureEvent} from '@typings/screens/gallery';
import type {GestureHandlerGestureEvent} from 'react-native-gesture-handler';
function diff(context: any, name: string, value: any) {
export function diff(context: any, name: string, value: any) {
'worklet';
if (!context.___diffs) {

View file

@ -0,0 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook} from '@testing-library/react-hooks';
import {getUsersByUsername} from '@utils/user';
import {useMemoMentionedUser, useMemoMentionedGroup} from './markdown';
import type {GroupModel, UserModel} from '@database/models/server';
jest.mock('@utils/user', () => ({
getUsersByUsername: jest.fn(),
}));
const mockGetUsersByUsername = jest.mocked(getUsersByUsername);
describe('useMemoMentionedUser', () => {
it('should return the mentioned user if found', () => {
const users = [{username: 'john_doe'} as UserModel];
const mentionName = 'john_doe';
const usersByUsername = {john_doe: users[0]};
mockGetUsersByUsername.mockReturnValue(usersByUsername);
const {result} = renderHook(() => useMemoMentionedUser(users, mentionName));
expect(result.current).toBe(users[0]);
});
it('should return undefined if the mentioned user is not found', () => {
const users = [{username: 'john_doe'} as UserModel];
const mentionName = 'jane_doe';
const usersByUsername = {john_doe: users[0]};
mockGetUsersByUsername.mockReturnValue(usersByUsername);
const {result} = renderHook(() => useMemoMentionedUser(users, mentionName));
expect(result.current).toBeUndefined();
});
it('should trim trailing punctuation from the mention name', () => {
const users = [{username: 'john_doe'} as UserModel];
const mentionName = 'john_doe.';
const usersByUsername = {john_doe: users[0]};
mockGetUsersByUsername.mockReturnValue(usersByUsername);
const {result} = renderHook(() => useMemoMentionedUser(users, mentionName));
expect(result.current).toBe(users[0]);
});
});
describe('useMemoMentionedGroup', () => {
it('should return the mentioned group if found', () => {
const groups = [{name: 'developers'} as GroupModel];
const user = undefined;
const mentionName = 'developers';
const {result} = renderHook(() => useMemoMentionedGroup(groups, user, mentionName));
expect(result.current).toBe(groups[0]);
});
it('should return undefined if the mentioned group is not found', () => {
const groups = [{name: 'developers'} as GroupModel];
const user = undefined;
const mentionName = 'designers';
const {result} = renderHook(() => useMemoMentionedGroup(groups, user, mentionName));
expect(result.current).toBeUndefined();
});
it('should return undefined if the user is defined', () => {
const groups = [{name: 'developers'} as GroupModel];
const user = {username: 'john_doe'} as UserModel;
const mentionName = 'developers';
const {result} = renderHook(() => useMemoMentionedGroup(groups, user, mentionName));
expect(result.current).toBeUndefined();
});
it('should trim trailing punctuation from the mention name', () => {
const groups = [{name: 'developers'} as GroupModel];
const user = undefined;
const mentionName = 'developers.';
const {result} = renderHook(() => useMemoMentionedGroup(groups, user, mentionName));
expect(result.current).toBe(groups[0]);
});
});