diff --git a/app/utils/config.test.ts b/app/utils/config.test.ts new file mode 100644 index 000000000..2caf194a7 --- /dev/null +++ b/app/utils/config.test.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {hasReliableWebsocket} from './config'; + +describe('Config utilities', () => { + test('hasReliableWebsocket', () => { + expect(hasReliableWebsocket('5.8.0')).toBe(false); + expect(hasReliableWebsocket('6.4.0', 'false')).toBe(false); + expect(hasReliableWebsocket('6.4.0', 'true')).toBe(true); + expect(hasReliableWebsocket('9.4.0', 'false')).toBe(true); + expect(hasReliableWebsocket('9.4.0', 'true')).toBe(true); + expect(hasReliableWebsocket('9.4.0')).toBe(true); + }); +}); diff --git a/app/utils/datetime.test.ts b/app/utils/datetime.test.ts new file mode 100644 index 000000000..8244c2015 --- /dev/null +++ b/app/utils/datetime.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isSameDate, isSameMonth, isSameYear, isToday, isYesterday} from './datetime'; + +describe('Datetime', () => { + test('isSameDate (isSameMonth / isSameYear)', () => { + expect(isSameDate(new Date('2024-03-28 02:23:27'), new Date('2025-03-28 02:23:27'))).toBe(false); + expect(isSameDate(new Date('2024-03-28 02:23:27'), new Date('2024-02-28 02:23:27'))).toBe(false); + expect(isSameDate(new Date('2024-03-28 02:23:27'), new Date('2024-03-18 02:23:27'))).toBe(false); + expect(isSameDate(new Date('2024-03-28 02:23:27'), new Date('2024-03-28 00:00:00'))).toBe(true); + expect(isSameDate(new Date('2024-03-28 02:23:27'))).toBe(false); + expect(isSameDate(new Date())).toBe(true); + }); + + test('isSameMonth with default', () => { + expect(isSameMonth(new Date('2024-03-28 02:23:27'))).toBe(false); + expect(isSameMonth(new Date())).toBe(true); + }); + + test('isSameYear with default', () => { + expect(isSameYear(new Date('2022-03-28 02:23:27'))).toBe(false); + expect(isSameYear(new Date())).toBe(true); + }); + + test('isToday', () => { + expect(isToday(new Date('2024-03-28 02:23:27'))).toBe(false); + expect(isToday(new Date())).toBe(true); + }); + + test('isYesteday', () => { + expect(isYesterday(new Date('2024-03-28 02:23:27'))).toBe(false); + const today = new Date(); + today.setDate(today.getDate() - 1); + expect(isYesterday(today)).toBe(true); + }); +}); diff --git a/app/utils/error_handling.test.ts b/app/utils/error_handling.test.ts new file mode 100644 index 000000000..b898d1f55 --- /dev/null +++ b/app/utils/error_handling.test.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert} from 'react-native'; +import Exception from 'react-native-exception-handler'; + +import {dismissAllModals, dismissAllOverlays} from '@screens/navigation'; +import testHelper from '@test/test_helper'; +import * as Sentry from '@utils/sentry'; + +import errorHandling from './error_handling'; +import * as Log from './log'; + +jest.mock('react-native-exception-handler', () => ({ + setJSExceptionHandler: jest.fn((callback: () => void, allowInDevMode: boolean) => { + if (!allowInDevMode) { + callback(); + } + }), +})); + +jest.mock('@utils/log', () => ({ + logWarning: jest.fn(() => ''), +})); + +describe('JavascriptAndNativeErrorHandler', () => { + const warning = jest.spyOn(Log, 'logWarning'); + const error = 'some error'; + + test('Initialization', () => { + const setJSExceptionHandler = jest.spyOn(Exception, 'setJSExceptionHandler'); + const initializeSentry = jest.spyOn(Sentry, 'initializeSentry'); + errorHandling.initializeErrorHandling(); + expect(setJSExceptionHandler).toHaveBeenCalledTimes(1); + expect(initializeSentry).toHaveBeenCalledTimes(1); + expect(setJSExceptionHandler).toHaveBeenCalledWith(errorHandling.errorHandler, false); + }); + + test('nativeErrorHander', () => { + const captureException = jest.spyOn(Sentry, 'captureException'); + errorHandling.nativeErrorHandler(error); + expect(warning).toHaveBeenCalledTimes(1); + expect(warning).toHaveBeenCalledWith(`Handling native error ${error}`); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(error); + }); + + test('errorHandler', async () => { + const captureJSException = jest.spyOn(Sentry, 'captureJSException'); + + errorHandling.errorHandler(null, false); + expect(warning).toHaveBeenCalledTimes(0); + + errorHandling.errorHandler(error, true); + expect(warning).toHaveBeenCalledTimes(1); + expect(warning).toHaveBeenCalledWith('Handling Javascript error', error, true); + expect(captureJSException).toHaveBeenCalledTimes(1); + expect(captureJSException).toHaveBeenCalledWith(error, true); + + const throwError = new Error(error); + const alert = jest.spyOn(Alert, 'alert'); + errorHandling.errorHandler(throwError, true); + expect(alert?.mock?.calls?.[0]?.length).toBe(4); + alert?.mock.calls?.[0]?.[2]?.[0]?.onPress?.(); + expect(dismissAllModals).toHaveBeenCalledTimes(1); + await testHelper.wait(20); + expect(dismissAllOverlays).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/utils/errors.test.ts b/app/utils/errors.test.ts new file mode 100644 index 000000000..33ea2075c --- /dev/null +++ b/app/utils/errors.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {createIntl} from 'react-intl'; + +import {DEFAULT_LOCALE, getTranslations} from '@i18n'; + +import { + isServerError, + isErrorWithMessage, + isErrorWithDetails, + isErrorWithIntl, + isErrorWithStatusCode, + isErrorWithUrl, + getFullErrorMessage, +} from './errors'; + +describe('Errors', () => { + test('isServerError', () => { + expect(isServerError('error')).toBe(false); + expect(isServerError({message: 'some error'})).toBe(false); + expect(isServerError({server_error_id: 'error_id'})).toBe(true); + }); + + test('isErrorWithMessage', () => { + expect(isErrorWithMessage('error')).toBe(false); + expect(isErrorWithMessage({message: 'some error'})).toBe(true); + expect(isErrorWithMessage({server_error_id: 'error_id'})).toBe(false); + }); + + test('isErrorWithDetails', () => { + expect(isErrorWithDetails('error')).toBe(false); + expect(isErrorWithDetails({message: 'some error'})).toBe(false); + expect(isErrorWithDetails({details: 'more info'})).toBe(true); + }); + + test('isErrorWithIntl', () => { + expect(isErrorWithIntl('error')).toBe(false); + expect(isErrorWithIntl({message: 'some error'})).toBe(false); + expect(isErrorWithIntl({intl: {id: 'some_error_id', defaultMessage: 'message text'}})).toBe(true); + }); + + test('isErrorWithStatusCode', () => { + expect(isErrorWithStatusCode('error')).toBe(false); + expect(isErrorWithStatusCode({message: 'some error'})).toBe(false); + expect(isErrorWithStatusCode({status_code: 95})).toBe(true); + }); + + test('isErrorWithUrl', () => { + expect(isErrorWithUrl('error')).toBe(false); + expect(isErrorWithUrl({message: 'some error'})).toBe(false); + expect(isErrorWithUrl({url: 'http://localhost:8065'})).toBe(true); + }); + + test('getFullErrorMessage', () => { + const locale = DEFAULT_LOCALE; + const intl = createIntl({locale, messages: getTranslations(locale)}); + expect(getFullErrorMessage('error', intl)).toBe('error'); + expect(getFullErrorMessage({details: 'more info', message: 'error message'}, intl)).toBe('error message; more info'); + expect(getFullErrorMessage({details: 'more info', message: 'error message'}, intl, 3)).toBe('error message; error message'); + expect(getFullErrorMessage({ + details: 'more info', + message: 'error message', + intl: {id: 'some_error_id', defaultMessage: 'message text'}, + }, intl)).toBe('message text; more info'); + expect(getFullErrorMessage({ + details: 'more info', + message: 'error message', + intl: {id: 'some_error_id', defaultMessage: 'default message text'}, + })).toBe('default message text; more info'); + + expect(getFullErrorMessage({ + details: 'more info', + })).toBe('Unknown error; more info'); + }); +}); diff --git a/app/utils/errors.ts b/app/utils/errors.ts index f0f013541..4d96d371e 100644 --- a/app/utils/errors.ts +++ b/app/utils/errors.ts @@ -8,9 +8,8 @@ export function isServerError(obj: unknown): obj is {server_error_id?: string} { typeof obj === 'object' && obj !== null && ( - !('server_error_id' in obj) || - typeof obj.server_error_id === 'string' || - typeof obj.server_error_id === 'undefined' + ('server_error_id' in obj) && + typeof obj.server_error_id === 'string' ) ); } @@ -56,11 +55,8 @@ export function isErrorWithUrl(obj: unknown): obj is {url?: string} { return ( typeof obj === 'object' && obj !== null && - ( - !('url' in obj) || - typeof obj.url === 'string' || - typeof obj.url === 'undefined' - ) + ('url' in obj) && + typeof obj.url === 'string' ); } diff --git a/app/utils/files.test.ts b/app/utils/files.test.ts new file mode 100644 index 000000000..fd8de8b9b --- /dev/null +++ b/app/utils/files.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import testHelper from '@test/test_helper'; + +import {toMilliseconds} from './datetime'; +import {getNumberFileMenuOptions, getChannelNamesWithID, getOrderedFileInfos, getFileInfosIndexes, getOrderedGalleryItems, pathWithPrefix} from './files'; + +import type ChannelModel from '@typings/database/models/servers/channel'; + +describe('Files utils', () => { + const buildFileInfos = (): FileInfo[] => { + return [{ + id: testHelper.generateId(), + create_at: Date.now() + toMilliseconds({days: 3, hours: 12, minutes: 8, seconds: 23}), + post_id: '123', + size: 10, + height: 0, + width: 0, + mime_type: 'application/pdf', + user_id: 'me', + extension: 'pdf', + name: 'file 3', + has_preview_image: false, + }, { + id: testHelper.generateId(), + create_at: Date.now() + toMilliseconds({days: 1, hours: 14}), + post_id: '123', + size: 10, + height: 100, + width: 100, + mime_type: 'image/png', + user_id: 'me', + extension: 'png', + name: 'file 2', + has_preview_image: true, + }, { + id: testHelper.generateId(), + create_at: Date.now() + toMilliseconds({days: 1, hours: 12, minutes: 10}), + post_id: '123', + size: 10, + height: 200, + width: 200, + mime_type: 'video/mp4', + user_id: 'me', + extension: 'mp4', + name: 'file 1', + has_preview_image: false, + }]; + }; + + test('getNumberFileMenuOptions', () => { + expect(getNumberFileMenuOptions(false, false)).toBe(1); + expect(getNumberFileMenuOptions(true, false)).toBe(2); + expect(getNumberFileMenuOptions(false, true)).toBe(2); + expect(getNumberFileMenuOptions(true, true)).toBe(3); + }); + + test('getChannelNamesWithID', () => { + const displayNames = ['channel 1', 'channel 2', 'channel 3']; + const channels = displayNames.map((d, i) => ({ + id: i.toString(), + displayName: d, + } as ChannelModel)); + const expected = channels.reduce>((obj, channel) => { + obj[channel.id] = channel.displayName; + return obj; + }, {}); + + expect(getChannelNamesWithID(channels)).toEqual(expected); + }); + + test('getOrderedFileInfos', () => { + let fileInfos = buildFileInfos(); + const result = getOrderedFileInfos(fileInfos); + const names = result.map((f) => f.name); + expect(names).toEqual(['file 3', 'file 2', 'file 1']); + + // testing the variant without setting create_at + fileInfos = fileInfos.map((f) => ({ + ...f, + create_at: undefined, + })); + const result2 = getOrderedFileInfos(fileInfos); + const names2 = result2.map((f) => f.name); + expect(names2).toEqual(names); + }); + + test('getFileInfosIndexes', () => { + const fileInfos = getOrderedFileInfos(buildFileInfos()); + const result = [0, 1, 2].reduce>((obj, index) => { + obj[fileInfos[index].id!] = index; + return obj; + }, {}); + + expect(getFileInfosIndexes(fileInfos)).toEqual(result); + }); + + test('getOrderedGalleryItems', () => { + const fileInfos = getOrderedFileInfos(buildFileInfos()); + const result = getOrderedGalleryItems(fileInfos); + const types = result.map((f) => f.type); + expect(types).toEqual(['file', 'image', 'video']); + }); + + test('pathWithPrefix', () => { + expect(pathWithPrefix('file://', 'file://something')).toEqual('file://something'); + expect(pathWithPrefix('file://', 'something')).toEqual('file://something'); + }); +}); diff --git a/test/setup.ts b/test/setup.ts index 78abb3806..ca777b0bc 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -369,6 +369,7 @@ jest.mock('@screens/navigation', () => ({ dismissAllModalsAndPopToScreen: jest.fn(), dismissAllModalsAndPopToRoot: jest.fn(), dismissOverlay: jest.fn(() => Promise.resolve()), + dismissAllOverlays: jest.fn(() => Promise.resolve()), })); jest.mock('@mattermost/react-native-emm', () => ({