more utils unit test coverage (#8068)

This commit is contained in:
Elias Nahum 2024-07-11 20:18:55 +08:00 committed by GitHub
parent b9f4000dd0
commit 9bdb5c15e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1030 additions and 18 deletions

View file

@ -0,0 +1,246 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createIntl} from 'react-intl';
import {Channel, General, Permissions} from '@constants';
import {hasPermission} from '@utils/role';
import {compareNotifyProps, filterChannelsMatchingTerm, generateChannelNameFromDisplayName, getDirectChannelName, isArchived, isDMorGM, isTypeDMorGM, selectDefaultChannelForTeam, sortChannelsByDisplayName, sortChannelsModelByDisplayName, validateDisplayName} from '.';
import type ChannelModel from '@typings/database/models/servers/channel';
jest.mock('@utils/role', () => ({
hasPermission: jest.fn(() => true),
}));
jest.mock('@utils/general', () => ({
generateId: jest.fn(() => 'generated-id'),
}));
describe('getDirectChannelName', () => {
it('should return the correct direct channel name', () => {
expect(getDirectChannelName('id1', 'id2')).toBe('id1__id2');
expect(getDirectChannelName('id2', 'id1')).toBe('id1__id2');
});
});
describe('isDMorGM', () => {
it('should return true for DM or GM channels', () => {
expect(isDMorGM({type: General.DM_CHANNEL} as Channel)).toBe(true);
expect(isDMorGM({type: General.GM_CHANNEL} as Channel)).toBe(true);
});
it('should return false for other channel types', () => {
expect(isDMorGM({type: General.OPEN_CHANNEL} as Channel)).toBe(false);
});
});
describe('isTypeDMorGM', () => {
it('should return true for DM or GM channel types', () => {
expect(isTypeDMorGM(General.DM_CHANNEL)).toBe(true);
expect(isTypeDMorGM(General.GM_CHANNEL)).toBe(true);
});
it('should return false for other channel types', () => {
expect(isTypeDMorGM(General.OPEN_CHANNEL)).toBe(false);
expect(isTypeDMorGM(undefined)).toBe(false);
});
});
describe('isArchived', () => {
it('should return true if channel is archived', () => {
expect(isArchived({delete_at: 12345} as Channel)).toBe(true);
expect(isArchived({deleteAt: 12345} as ChannelModel)).toBe(true);
});
it('should return false if channel is not archived', () => {
expect(isArchived({delete_at: 0} as Channel)).toBe(false);
expect(isArchived({deleteAt: 0} as ChannelModel)).toBe(false);
});
});
describe('selectDefaultChannelForTeam', () => {
const channels = [
{id: '1', name: General.DEFAULT_CHANNEL, team_id: 'team1', type: General.OPEN_CHANNEL},
{id: '2', name: 'random', team_id: 'team1', type: General.OPEN_CHANNEL},
] as Channel[];
const memberships = [
{channel_id: '2'},
] as ChannelMembership[];
const roles = [
{permissions: [Permissions.JOIN_PUBLIC_CHANNELS]},
] as Role[];
it('should select the default channel if user is a member', () => {
const result = selectDefaultChannelForTeam(channels, memberships, 'team1', roles);
expect(result).toEqual(channels[0]);
const channelModels = [
{id: '1', name: General.DEFAULT_CHANNEL, teamId: 'team1', type: General.OPEN_CHANNEL},
{id: '2', name: 'random', teamId: 'team1', type: General.OPEN_CHANNEL},
] as ChannelModel[];
const result2 = selectDefaultChannelForTeam(channelModels, memberships, 'team1', roles);
expect(result2).toEqual(channelModels[0]);
});
it('should select the first channel in the team if user can join public channels', () => {
const result = selectDefaultChannelForTeam(channels, [], 'team1', roles);
expect(result).toEqual(channels[0]);
});
it('should select the first channel in the team if user cannot join public channels', () => {
(hasPermission as jest.Mock).mockReturnValueOnce(false);
const result = selectDefaultChannelForTeam(channels, memberships, 'team1', roles);
expect(result).toEqual(channels[1]);
});
it('should select the default channel in the team if user cannot join public channels', () => {
(hasPermission as jest.Mock).mockReturnValueOnce(false);
const memberOf = [
{channel_id: '1'},
] as ChannelMembership[];
const result = selectDefaultChannelForTeam(channels, memberOf, 'team1', roles);
expect(result).toEqual(channels[0]);
});
it('should return undefined if no channels are available', () => {
const result = selectDefaultChannelForTeam([], [], 'team1', roles);
expect(result).toBeUndefined();
});
});
describe('sortChannelsByDisplayName', () => {
it('should sort channels by display name', () => {
const channels = [
{name: 'name1', display_name: 'Zeta'},
{name: 'name2', display_name: 'Alpha'},
] as Channel[];
const channelsModels = [
{name: 'name1', displayName: 'Zeta'},
{name: 'name2', displayName: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result[0].name).toBe('name2');
const result2 = channelsModels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result2[0].name).toBe('name2');
});
it('should sort channels by name if display name is not defined', () => {
const channels = [
{name: 'Zeta'},
{name: 'Alpha'},
] as Channel[];
const result = channels.sort((a, b) => sortChannelsByDisplayName('en', a, b));
expect(result[0].name).toBe('Alpha');
});
});
describe('sortChannelsModelByDisplayName', () => {
it('should sort channel models by display name', () => {
const channels = [
{name: 'name1', displayName: 'Zeta'},
{name: 'name2', displayName: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsModelByDisplayName('en', a, b));
expect(result[0].name).toBe('name2');
});
it('should sort channel models by name if display name is not defined', () => {
const channels = [
{name: 'Zeta'},
{name: 'Alpha'},
] as ChannelModel[];
const result = channels.sort((a, b) => sortChannelsModelByDisplayName('en', a, b));
expect(result[0].name).toBe('Alpha');
});
});
describe('validateDisplayName', () => {
const intl = createIntl({locale: 'en', messages: {}});
it('should return an error if display name is empty', () => {
const result = validateDisplayName(intl, '');
expect(result.error).toBe('Channel name is required');
});
it('should return an error if display name is too long', () => {
const result = validateDisplayName(intl, 'a'.repeat(100));
expect(result.error).toBe('Channel name must be less than 64 characters');
});
it('should return an error if display name is too short', () => {
const minLength = Channel.MIN_CHANNEL_NAME_LENGTH;
Channel.MIN_CHANNEL_NAME_LENGTH = 2;
const result = validateDisplayName(intl, 'a');
expect(result.error).toBe('Channel name must be 2 or more characters');
Channel.MIN_CHANNEL_NAME_LENGTH = minLength;
});
it('should return no error if display name is valid', () => {
const result = validateDisplayName(intl, 'valid name');
expect(result.error).toBe('');
});
});
describe('generateChannelNameFromDisplayName', () => {
it('should generate a channel name from display name', () => {
const result = generateChannelNameFromDisplayName('Valid Name');
expect(result).toBe('valid-name');
});
it('should generate an ID if display name is empty after cleanup', () => {
const result = generateChannelNameFromDisplayName('');
expect(result).toBe('generated-id');
});
});
describe('compareNotifyProps', () => {
it('should return true if notify props are equal', () => {
const propsA: Partial<ChannelNotifyProps> = {desktop: 'default', email: 'default', mark_unread: 'all', push: 'default', ignore_channel_mentions: 'default'};
const propsB = {...propsA};
expect(compareNotifyProps(propsA, propsB)).toBe(true);
});
it('should return false if notify props are different', () => {
const propsA: Partial<ChannelNotifyProps> = {desktop: 'default', email: 'default', mark_unread: 'all', push: 'default', ignore_channel_mentions: 'default'};
const propsB: Partial<ChannelNotifyProps> = {...propsA, desktop: 'none'};
expect(compareNotifyProps(propsA, propsB)).toBe(false);
});
});
describe('filterChannelsMatchingTerm', () => {
const channels = [
{name: 'channel1', display_name: 'Channel One'},
{name: 'channel2', display_name: 'Channel Two'},
{display_name: 'Undefined name'},
{},
] as Channel[];
it('should filter channels by name or display name', () => {
let result = filterChannelsMatchingTerm(channels, 'channel1');
expect(result.length).toBe(1);
expect(result[0].name).toBe('channel1');
result = filterChannelsMatchingTerm(channels, 'Channel One');
expect(result.length).toBe(1);
expect(result[0].display_name).toBe('Channel One');
result = filterChannelsMatchingTerm([...channels, undefined] as Channel[], 'Channel One');
expect(result.length).toBe(1);
expect(result[0].display_name).toBe('Channel One');
});
it('should return an empty array if no channels match the term', () => {
const result = filterChannelsMatchingTerm(channels, 'nonexistent');
expect(result.length).toBe(0);
});
});

View file

@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Channel, General, Permissions} from '@constants';
import {t, DEFAULT_LOCALE} from '@i18n';
import {hasPermission} from '@utils/role';
import {defineMessages, type IntlShape} from 'react-intl';
import {generateId} from '../general';
import {cleanUpUrlable} from '../url';
import {Channel, General, Permissions} from '@constants';
import {DEFAULT_LOCALE} from '@i18n';
import {generateId} from '@utils/general';
import {hasPermission} from '@utils/role';
import {cleanUpUrlable} from '@utils/url';
import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl';
export function getDirectChannelName(id: string, otherId: string): string {
let handle;
@ -83,20 +83,20 @@ export function sortChannelsModelByDisplayName(locale: string, a: ChannelModel,
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
}
const displayNameValidationMessages = {
const displayNameValidationMessages = defineMessages({
display_name_required: {
id: t('mobile.rename_channel.display_name_required'),
id: 'mobile.rename_channel.display_name_required',
defaultMessage: 'Channel name is required',
},
display_name_maxLength: {
id: t('mobile.rename_channel.display_name_maxLength'),
id: 'mobile.rename_channel.display_name_maxLength',
defaultMessage: 'Channel name must be less than {maxLength, number} characters',
},
display_name_minLength: {
id: t('mobile.rename_channel.display_name_minLength'),
id: 'mobile.rename_channel.display_name_minLength',
defaultMessage: 'Channel name must be {minLength, number} or more characters',
},
};
});
export const validateDisplayName = (intl: IntlShape, displayName: string): {error: string} => {
let errorMessage;
@ -117,6 +117,7 @@ export const validateDisplayName = (intl: IntlShape, displayName: string): {erro
default:
errorMessage = '';
break;
}
return {error: errorMessage};
};

View file

@ -0,0 +1,227 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createIntl} from 'react-intl';
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
import {showPermalink} from '@actions/remote/permalink';
import {fetchUsersByUsernames} from '@actions/remote/user';
import {DeepLink, Launch, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import WebsocketManager from '@managers/websocket_manager';
import {getActiveServerUrl} from '@queries/app/servers';
import {queryUsersByUsername} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
import {logError} from '@utils/log';
import {addNewServer} from '@utils/server';
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
import {alertInvalidDeepLink, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
jest.mock('@actions/remote/user', () => ({
fetchUsersByUsernames: jest.fn(),
}));
jest.mock('@actions/remote/permalink', () => ({
showPermalink: jest.fn(),
}));
jest.mock('@queries/app/servers', () => ({
getActiveServerUrl: jest.fn(),
}));
jest.mock('@queries/servers/user', () => ({
getCurrentUser: jest.fn(),
queryUsersByUsername: jest.fn(() => ({fetchIds: jest.fn(() => ['user-id'])})),
}));
jest.mock('@database/manager', () => ({
searchUrl: jest.fn(),
setActiveServerDatabase: jest.fn(),
getServerDatabaseAndOperator: jest.fn(() => ({database: {}, operator: {}})),
}));
jest.mock('@managers/websocket_manager', () => ({
initializeClient: jest.fn(),
}));
jest.mock('@store/navigation_store', () => ({
getVisibleScreen: jest.fn(() => 'HOME'),
hasModalsOpened: jest.fn(() => false),
waitUntilScreenHasLoaded: jest.fn(),
}));
jest.mock('@utils/server', () => ({
addNewServer: jest.fn(),
}));
jest.mock('@actions/remote/channel', () => ({
makeDirectChannel: jest.fn(),
switchToChannelByName: jest.fn(),
}));
jest.mock('@utils/draft', () => ({
errorBadChannel: jest.fn(),
errorUnkownUser: jest.fn(),
alertErrorWithFallback: jest.fn(),
}));
jest.mock('@utils/log', () => ({
logError: jest.fn(),
}));
jest.mock('@i18n', () => ({
DEFAULT_LOCALE: 'en',
getTranslations: jest.fn(() => ({})),
t: jest.fn((id) => id),
}));
describe('handleDeepLink', () => {
const intl = createIntl({locale: 'en', messages: {}});
beforeEach(() => {
jest.clearAllMocks();
});
it('should return error for invalid deep link', async () => {
const result = await handleDeepLink('invalid-url');
expect(result).toEqual({error: true});
});
it('should add new server if not existing', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('');
const result = await handleDeepLink('https://newserver.com/team/channels/town-square');
expect(addNewServer).toHaveBeenCalledWith(Preferences.THEMES.denim, 'newserver.com', undefined, {type: DeepLink.Channel,
data: {
serverUrl: 'newserver.com',
channelName: 'town-square',
teamName: 'team',
},
url: 'https://newserver.com/team/channels/town-square',
});
expect(result).toEqual({error: false});
});
it('should handle existing server and switch to home screen', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square');
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com');
expect(result).toEqual({error: false});
});
it('should switch to channel by name for Channel deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square', intl);
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', 'town-square', 'team', errorBadChannel, intl);
expect(result).toEqual({error: false});
});
it('should create direct message for DirectMessage deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => ['user-id'])});
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
expect(result).toEqual({error: false});
});
it('should fetch user and create direct message if user not found locally', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: [{id: 'user-id'}]});
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
expect(result).toEqual({error: false});
});
it('should show unknown user error if user not found', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: []});
const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(errorUnkownUser).toHaveBeenCalledWith(intl);
expect(result).toEqual({error: false});
});
it('should switch to group message channel for GroupMessage deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f', intl);
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', '7b35c77a645e1906e03a2c330f89203385db102f', 'team', errorBadChannel, intl);
expect(result).toEqual({error: false});
});
it('should show permalink for Permalink deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
const postid = '7b35c77a645e1906e03a2c330f';
const result = await handleDeepLink(`https://existingserver.com/team/pl/${postid}`, intl);
expect(showPermalink).toHaveBeenCalledWith('https://existingserver.com', 'team', postid);
expect(result).toEqual({error: false});
});
it('should log error and return error true on failure', async () => {
(getActiveServerUrl as jest.Mock).mockImplementationOnce(() => {
throw new Error('DB does not exist error');
});
const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f');
expect(logError).toHaveBeenCalledWith('Failed to open channel from deeplink', expect.any(Error), undefined);
expect(result).toEqual({error: true});
});
});
describe('getLaunchPropsFromDeepLink', () => {
it('should return launch props with launchError when deep link is invalid', () => {
const result = getLaunchPropsFromDeepLink('invalid-url');
expect(result).toEqual({
launchType: Launch.DeepLink,
coldStart: false,
launchError: true,
});
});
it('should return launch props with extra data when deep link is valid', () => {
const extraData = {
type: DeepLink.Channel,
data: {
channelName: 'town-square',
serverUrl: 'existingserver.com',
teamName: 'team',
},
url: 'https://existingserver.com/team/channels/town-square',
};
const result = getLaunchPropsFromDeepLink('https://existingserver.com/team/channels/town-square', true);
expect(result).toEqual({
launchType: Launch.DeepLink,
coldStart: true,
extra: extraData,
});
});
});
describe('alertInvalidDeepLink', () => {
it('should call alertErrorWithFallback with correct arguments', () => {
const intl = createIntl({locale: 'en', messages: {}});
const message = {
id: 'mobile.deep_link.invalid',
defaultMessage: 'This link you are trying to open is invalid.',
};
(t as jest.Mock).mockReturnValue(message.id);
alertInvalidDeepLink(intl);
expect(alertErrorWithFallback).toHaveBeenCalledWith(intl, {}, message);
});
});

View file

@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {alertFailedToOpenDocument, alertDownloadDocumentDisabled, alertDownloadFailed} from './';
import type {IntlShape} from 'react-intl';
jest.mock('react-native', () => ({
Alert: {
alert: jest.fn(),
},
}));
describe('alertUtils', () => {
const intlMock = {
formatMessage: jest.fn(),
} as unknown as IntlShape;
beforeEach(() => {
jest.clearAllMocks();
});
it('should alert failed to open document with correct messages', () => {
const file = {extension: 'pdf'} as FileInfo;
intlMock.formatMessage.
//@ts-expect-error type definition
mockReturnValueOnce('Open Document failed').
mockReturnValueOnce('An error occurred while opening the document. Please make sure you have a PDF viewer installed and try again.\n').
mockReturnValueOnce('OK');
alertFailedToOpenDocument(file, intlMock);
expect(Alert.alert).toHaveBeenCalledWith(
'Open Document failed',
'An error occurred while opening the document. Please make sure you have a PDF viewer installed and try again.\n',
[{text: 'OK'}],
);
});
it('should alert download document disabled with correct messages', () => {
intlMock.formatMessage.
//@ts-expect-error type definition
mockReturnValueOnce('Download disabled').
mockReturnValueOnce('File downloads are disabled on this server. Please contact your System Admin for more details.\n').
mockReturnValueOnce('OK');
alertDownloadDocumentDisabled(intlMock);
expect(Alert.alert).toHaveBeenCalledWith(
'Download disabled',
'File downloads are disabled on this server. Please contact your System Admin for more details.\n',
[{text: 'OK'}],
);
});
it('should alert download failed with correct messages', () => {
intlMock.formatMessage.
//@ts-expect-error type definition
mockReturnValueOnce('Download failed').
mockReturnValueOnce('An error occurred while downloading the file. Please check your internet connection and try again.\n').
mockReturnValueOnce('OK');
alertDownloadFailed(intlMock);
expect(Alert.alert).toHaveBeenCalledWith(
'Download failed',
'An error occurred while downloading the file. Please check your internet connection and try again.\n',
[{text: 'OK'}],
);
});
});

View file

@ -0,0 +1,194 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createIntl} from 'react-intl';
import {Alert} from 'react-native';
import {General} from '@constants';
import {
errorBadChannel,
errorUnkownUser,
permalinkBadTeam,
alertErrorWithFallback,
alertAttachmentFail,
textContainsAtAllAtChannel,
textContainsAtHere,
buildChannelWideMentionMessage,
alertChannelWideMention,
alertSendToGroups,
getStatusFromSlashCommand,
alertSlashCommandFailed,
} from './';
jest.mock('react-native', () => {
const RN = jest.requireActual('react-native');
return {
Platform: RN.Platform,
NativeModules: {
...RN.NativeModules,
RNUtils: {
getConstants: () => ({
appGroupIdentifier: 'group.mattermost.rnbeta',
appGroupSharedDirectory: {
sharedDirectory: '',
databasePath: '',
},
}),
addListener: jest.fn(),
removeListeners: jest.fn(),
isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}),
getDeliveredNotifications: jest.fn().mockResolvedValue([]),
removeChannelNotifications: jest.fn().mockImplementation(),
removeThreadNotifications: jest.fn().mockImplementation(),
removeServerNotifications: jest.fn().mockImplementation(),
},
},
Alert: {
alert: jest.fn(),
},
};
});
jest.mock('@i18n', () => ({
t: (id: string) => id,
}));
describe('draft utils', () => {
const intl = createIntl({locale: 'en', messages: {}});
beforeEach(() => {
jest.clearAllMocks();
});
it('should alert error bad channel', () => {
errorBadChannel(intl);
expect(Alert.alert).toHaveBeenCalledWith('', 'This link belongs to a deleted channel or to a channel to which you do not have access.', undefined);
});
it('should alert error unknown user', () => {
errorUnkownUser(intl);
expect(Alert.alert).toHaveBeenCalledWith('', 'We can\'t redirect you to the DM. The user specified is unknown.', undefined);
});
it('should alert permalink bad team', () => {
permalinkBadTeam(intl);
expect(Alert.alert).toHaveBeenCalledWith('', 'This link belongs to a deleted team or to a team to which you do not have access.', undefined);
});
it('should alert error with fallback', () => {
const error = {message: 'Test Error'};
const fallback = {id: 'fallback.id', defaultMessage: 'Fallback Message'};
alertErrorWithFallback(intl, error, fallback);
expect(Alert.alert).toHaveBeenCalledWith('', 'Test Error', undefined);
});
it('should alert error with fallback when network request failed', () => {
const error = {message: 'Network request failed'};
const fallback = {id: 'fallback.id', defaultMessage: 'Fallback Message'};
alertErrorWithFallback(intl, error, fallback);
expect(Alert.alert).toHaveBeenCalledWith('', 'Fallback Message', undefined);
});
it('should alert attachment fail', () => {
const accept = jest.fn();
const cancel = jest.fn();
alertAttachmentFail(intl, accept, cancel);
expect(Alert.alert).toHaveBeenCalledWith(
'Attachment failure',
'Some attachments failed to upload to the server. Are you sure you want to post the message?',
[
{text: 'No', onPress: cancel},
{text: 'Yes', onPress: accept},
],
);
});
it('should detect @all and @channel in text', () => {
const text = 'Hello @all';
expect(textContainsAtAllAtChannel(text)).toBe(true);
});
it('should detect @here in text', () => {
const text = 'Hello @here';
expect(textContainsAtHere(text)).toBe(true);
});
it('should build channel-wide mention message with timezones', () => {
const membersCount = 10;
const channelTimezoneCount = 3;
let message = buildChannelWideMentionMessage(intl, membersCount, channelTimezoneCount, true);
expect(message).toBe('By using @here you are about to send notifications up to 9 people in 3 timezones. Are you sure you want to do this?');
message = buildChannelWideMentionMessage(intl, membersCount, channelTimezoneCount, false);
expect(message).toBe('By using @all or @channel you are about to send notifications to 9 people in 3 timezones. Are you sure you want to do this?');
});
it('should build channel-wide mention message without timezones', () => {
const membersCount = 10;
const channelTimezoneCount = 0;
let message = buildChannelWideMentionMessage(intl, membersCount, channelTimezoneCount, true);
expect(message).toBe('By using @here you are about to send notifications to up to 9 people. Are you sure you want to do this?');
message = buildChannelWideMentionMessage(intl, membersCount, channelTimezoneCount, false);
expect(message).toBe('By using @all or @channel you are about to send notifications to 9 people. Are you sure you want to do this?');
});
it('should alert channel-wide mention', () => {
const notifyAllMessage = 'Notify all message';
const accept = jest.fn();
const cancel = jest.fn();
alertChannelWideMention(intl, notifyAllMessage, accept, cancel);
expect(Alert.alert).toHaveBeenCalledWith(
'Confirm sending notifications to entire channel',
'Notify all message',
[
{text: 'Cancel', onPress: cancel},
{text: 'Confirm', onPress: accept},
],
);
});
it('should alert send to groups', () => {
const notifyAllMessage = 'Notify all message';
const accept = jest.fn();
const cancel = jest.fn();
alertSendToGroups(intl, notifyAllMessage, accept, cancel);
expect(Alert.alert).toHaveBeenCalledWith(
'Confirm sending notifications to groups',
'Notify all message',
[
{text: 'Cancel', onPress: cancel},
{text: 'Confirm', onPress: accept},
],
);
});
it('should get status from slash command', () => {
General.STATUS_COMMANDS = ['status'];
const message = '/status';
expect(getStatusFromSlashCommand(message)).toBe('status');
});
it('should not get status from non-status slash command', () => {
General.STATUS_COMMANDS = ['status'];
const message = '/nonstatus';
expect(getStatusFromSlashCommand(message)).toBe('');
});
it('should alert slash command failed', () => {
const error = 'Error message';
alertSlashCommandFailed(intl, error);
expect(Alert.alert).toHaveBeenCalledWith(
'Error Executing Command',
'Error message',
);
});
});

View file

@ -0,0 +1,259 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Fuse from 'fuse.js';
import {
isEmoticon,
isUnicodeEmoji,
getEmoticonName,
matchEmoticons,
getValidEmojis,
getEmojiName,
isReactionMatch,
isValidNamedEmoji,
hasJumboEmojiOnly,
doesMatchNamedEmoji,
getEmojiFirstAlias,
getEmojiByName,
mapCustomEmojiNames,
compareEmojis,
isCustomEmojiEnabled,
fillEmoji,
getSkin,
getEmojis,
searchEmojis,
} from './helpers';
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
import type SystemModel from '@typings/database/models/servers/system';
jest.mock('@database/models/server/system');
describe('helpers.ts tests', () => {
describe('isEmoticon', () => {
it('should return true for valid emoticons', () => {
expect(isEmoticon(':)')).toBe(true);
expect(isEmoticon(';)')).toBe(true);
});
it('should return false for invalid emoticons', () => {
expect(isEmoticon(':-*')).toBe(false);
expect(isEmoticon('hello')).toBe(false);
});
});
describe('isUnicodeEmoji', () => {
it('should return true for unicode emoji', () => {
expect(isUnicodeEmoji('😊')).toBe(true);
});
it('should return false for non-emoji text', () => {
expect(isUnicodeEmoji('hello')).toBe(false);
});
});
describe('getEmoticonName', () => {
it('should return correct emoticon name', () => {
expect(getEmoticonName(':)')).toBe('slightly_smiling_face');
});
it('should return undefined for invalid emoticon', () => {
expect(getEmoticonName('hello')).toBeUndefined();
});
});
describe('matchEmoticons', () => {
it('should match named, unicode, and emoticon emojis', () => {
const text = 'Hello :) :smile: 😊';
const result = matchEmoticons(text);
expect(result).toEqual([':smile:', ' :)', '😊']);
});
it('should return empty array for no matches', () => {
expect(matchEmoticons('hello')).toEqual([]);
});
});
describe('getValidEmojis', () => {
it('should return valid emoji names', () => {
const emojis = [':smile:', '😊', ':)'];
const customEmojis = [{name: 'custom_emoji'}] as CustomEmojiModel[];
const result = getValidEmojis(emojis, customEmojis);
expect(result).toContain('smile');
});
it('should return empty array for no valid emojis', () => {
const emojis = ['hello'];
const customEmojis = [{name: 'custom_emoji'}] as CustomEmojiModel[];
const result = getValidEmojis(emojis, customEmojis);
expect(result).toEqual([]);
});
});
describe('getEmojiName', () => {
it('should return correct emoji name for named emoji', () => {
expect(getEmojiName(':smile:', [])).toBe('smile');
});
it('should return correct emoji name for unicode emoji', () => {
expect(getEmojiName('😊', [])).toBe('blush');
});
it('should return correct emoji name for emoticon', () => {
expect(getEmojiName(':)', [])).toBe('slightly_smiling_face');
});
it('should return undefined for invalid emoji', () => {
expect(getEmojiName('hello', [])).toBeUndefined();
});
});
describe('isReactionMatch', () => {
it('should return correct reaction match for valid reaction', () => {
const custom = [{name: 'custom'}] as CustomEmojiModel[];
expect(isReactionMatch('+:smile:', custom)).toEqual({add: true, emoji: 'smile'});
expect(isReactionMatch('+:custom:', custom)).toEqual({add: true, emoji: 'custom'});
expect(isReactionMatch('+:lala:', custom)).toEqual(null);
});
it('should return null for invalid reaction', () => {
expect(isReactionMatch(':hello:', [])).toBeNull();
});
});
describe('isValidNamedEmoji', () => {
it('should return true for valid named emoji', () => {
expect(isValidNamedEmoji('smile', [])).toBe(true);
});
it('should return false for invalid named emoji', () => {
expect(isValidNamedEmoji('hello', [])).toBe(false);
});
});
describe('hasJumboEmojiOnly', () => {
it('should return true for jumbo emojis only', () => {
expect(hasJumboEmojiOnly('😊', [])).toBe(true);
expect(hasJumboEmojiOnly('😊 😊', [])).toBe(true);
});
it('should return false for non-jumbo emojis', () => {
expect(hasJumboEmojiOnly('hello', [])).toBe(false);
});
it('should return false for strings with spaces only', () => {
expect(hasJumboEmojiOnly(' ', [])).toBe(false);
});
it('should return true for strings with custom emojis', () => {
const custom = ['custom'];
expect(hasJumboEmojiOnly('😊 :custom: :blush:', custom)).toBe(true);
});
it('should return false for strings with more than 8 emojis', () => {
expect(hasJumboEmojiOnly('😊 😊 😊 😊 😊 😊 😊 😊 😊', [])).toBe(false);
});
});
describe('doesMatchNamedEmoji', () => {
it('should return true for valid named emoji', () => {
expect(doesMatchNamedEmoji(':smile:')).toBe(true);
});
it('should return false for invalid named emoji', () => {
expect(doesMatchNamedEmoji('hello')).toBe(false);
});
});
describe('getEmojiFirstAlias', () => {
it('should return first alias of emoji', () => {
expect(getEmojiFirstAlias('smile')).toBe('smile');
expect(getEmojiFirstAlias('lolo')).toBe('lolo');
});
});
describe('getEmojiByName', () => {
it('should return emoji by name for standard emoji', () => {
expect(getEmojiByName('smile', [])).toEqual(expect.objectContaining({short_names: ['smile'], category: 'smileys-emotion'}));
});
it('should return emoji by name for custom emoji', () => {
const customEmojis = [{name: 'custom_emoji'}] as CustomEmojiModel[];
expect(getEmojiByName('custom_emoji', customEmojis)).toEqual({name: 'custom_emoji'});
});
it('should return undefined for invalid emoji', () => {
expect(getEmojiByName('hello', [])).toBeUndefined();
});
});
describe('mapCustomEmojiNames', () => {
it('should return names of custom emojis', () => {
const customEmojis = [{name: 'custom_emoji'}] as CustomEmojiModel[];
expect(mapCustomEmojiNames(customEmojis)).toEqual(['custom_emoji']);
});
});
describe('compareEmojis', () => {
it('should compare emojis correctly based on search term', () => {
expect(compareEmojis('smile', 'grin', 'smi')).toBe(-1);
expect(compareEmojis('grin', 'smile', 'smi')).toBe(1);
expect(compareEmojis('smile', 'grin', '')).toBe(1);
expect(compareEmojis('smile', 'smile', 'smi')).toBe(0);
});
});
describe('isCustomEmojiEnabled', () => {
it('should return true if custom emoji is enabled in config', () => {
expect(isCustomEmojiEnabled({EnableCustomEmoji: 'true'} as any)).toBe(true);
expect(isCustomEmojiEnabled({value: {EnableCustomEmoji: 'true'}} as SystemModel)).toBe(true);
});
it('should return false if custom emoji is disabled in config', () => {
expect(isCustomEmojiEnabled({EnableCustomEmoji: 'false'} as any)).toBe(false);
});
});
describe('fillEmoji', () => {
it('should fill emoji correctly', () => {
expect(fillEmoji('category', 1)).toEqual({name: 'smiley', aliases: ['smiley'], category: 'category'});
});
});
describe('getSkin', () => {
it('should return default for skin variations', () => {
const emoji = {skin_variations: []};
expect(getSkin(emoji)).toBe('default');
});
it('should return first skin for skins array', () => {
const emoji = {skins: ['light']};
expect(getSkin(emoji)).toBe('light');
});
it('should return null for no skin variations', () => {
const emoji = {};
expect(getSkin(emoji)).toBeNull();
});
});
describe('getEmojis', () => {
it('should return emojis for specified skin tone', () => {
const custom = [{name: 'hey_custom'}] as CustomEmojiModel[];
expect(getEmojis('dark', custom)).toEqual(expect.arrayContaining(['grinning', 'smiley', 'smile', 'grin', 'laughing', 'satisfied', 'sweat_smile', 'rolling_on_the_floor_laughing', 'rofl', 'joy', 'hey_custom']));
});
});
describe('searchEmojis', () => {
it('should return emojis matching search term', () => {
const options = {findAllMatches: true, ignoreLocation: true, includeMatches: true, shouldSort: false, includeScore: true};
const emojis = getEmojis('light', []);
const fuse = new Fuse(emojis, options);
const result = searchEmojis(fuse, 'smile');
expect(result).toEqual(['smile', 'smile_cat', 'smiley', 'smiley_cat', 'sweat_smile']);
expect(searchEmojis(fuse, 'cawabunga')).toEqual([]);
});
});
});

View file

@ -42,7 +42,7 @@ const RE_REACTION = /^(\+|-):([^:\s]+):\s*$/;
const MAX_JUMBO_EMOJIS = 8;
function isEmoticon(text: string) {
export function isEmoticon(text: string) {
for (const emoticon of Object.keys(RE_EMOTICON)) {
const reEmoticon = RE_EMOTICON[emoticon];
const matchEmoticon = text.match(reEmoticon);
@ -106,9 +106,17 @@ export function getEmojiName(emoji: string, customEmojiNames: string[]) {
const matchUnicodeEmoji = emoji.match(RE_UNICODE_EMOJI);
if (matchUnicodeEmoji) {
const index = EmojiIndicesByUnicode.get(matchUnicodeEmoji[0]);
if (index != null) {
return fillEmoji('', Emojis[index]).name;
function getEmojiHexValue(code: string) {
let hexValue = '';
for (const char of code) {
hexValue += char.codePointAt(0)?.toString(16).toLowerCase() + ' ';
}
return hexValue.trim();
}
const hex = getEmojiHexValue(matchUnicodeEmoji[0]);
const index = EmojiIndicesByUnicode.get(hex);
if (index != null && Emojis[index]) {
return fillEmoji('', index).name;
}
return undefined;
}
@ -303,11 +311,11 @@ export function compareEmojis(emojiA: string | Partial<EmojiType>, emojiB: strin
}
export const isCustomEmojiEnabled = (config: ClientConfig | SystemModel) => {
if (config instanceof SystemModel) {
return config?.value?.EnableCustomEmoji === 'true';
if ('value' in config) {
return config.value?.EnableCustomEmoji === 'true';
}
return config?.EnableCustomEmoji === 'true';
return config.EnableCustomEmoji === 'true';
};
export function fillEmoji(category: string, index: number) {