forked from Ivasoft/mattermost-mobile
added utils unit tests (#8040)
This commit is contained in:
@@ -44,8 +44,8 @@ exports[`Thread item in the channel list Threads Component should match snapshot
|
||||
"flexDirection": "row",
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
"minHeight": 40,
|
||||
},
|
||||
@@ -82,7 +82,7 @@ exports[`Thread item in the channel list Threads Component should match snapshot
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
@@ -137,8 +137,8 @@ exports[`Thread item in the channel list Threads Component should match snapshot
|
||||
"flexDirection": "row",
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
{
|
||||
"minHeight": 40,
|
||||
},
|
||||
@@ -177,7 +177,7 @@ exports[`Thread item in the channel list Threads Component should match snapshot
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
false,
|
||||
{
|
||||
"color": "#3f4350",
|
||||
},
|
||||
|
||||
766
app/utils/apps.test.ts
Normal file
766
app/utils/apps.test.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes} from '@constants/apps';
|
||||
|
||||
import {
|
||||
cleanBinding,
|
||||
validateBindings,
|
||||
cleanForm,
|
||||
makeCallErrorResponse,
|
||||
filterEmptyOptions,
|
||||
createCallContext,
|
||||
createCallRequest,
|
||||
} from './apps';
|
||||
|
||||
describe('cleanBinding', () => {
|
||||
it('should return the binding unchanged if it is null', () => {
|
||||
expect(cleanBinding(null as any, AppBindingLocations.COMMAND)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle default values for app_id, label, and location', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: '',
|
||||
location: '',
|
||||
label: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings![0].app_id).toBe('test_app');
|
||||
expect(result.bindings![0].label).toBeTruthy();
|
||||
expect(result.bindings![0].location).toMatch(/location\/.+/);
|
||||
});
|
||||
|
||||
it('should remove bindings without app_id', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: '',
|
||||
location: 'sub_location1',
|
||||
label: 'sub_label1',
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app',
|
||||
location: 'sub_location2',
|
||||
label: 'sub_label2',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].app_id).toBe('sub_app');
|
||||
});
|
||||
|
||||
it('should remove bindings with empty or whitespace labels', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app1',
|
||||
location: 'sub_location1',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app2',
|
||||
location: 'sub_location2',
|
||||
label: ' ',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app3',
|
||||
location: 'sub_location3',
|
||||
label: 'valid_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].label).toBe('valid_label');
|
||||
});
|
||||
|
||||
it('should remove bindings with duplicate labels in COMMAND location', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app1',
|
||||
location: 'sub_location1',
|
||||
label: 'sub_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app2',
|
||||
location: 'sub_location2',
|
||||
label: 'sub_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].app_id).toBe('sub_app1');
|
||||
});
|
||||
|
||||
it('should keep unique labels in IN_POST location', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app1',
|
||||
location: 'sub_location1',
|
||||
label: 'sub_label1',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app2',
|
||||
location: 'sub_location2',
|
||||
label: 'sub_label2',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.IN_POST);
|
||||
expect(result.bindings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should remove invalid sub-bindings without form or submit', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app1',
|
||||
location: 'sub_location1',
|
||||
label: 'sub_label1',
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app2',
|
||||
location: 'sub_location2',
|
||||
label: 'sub_label2',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app3',
|
||||
location: 'sub_location3',
|
||||
label: 'sub_label3',
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(2);
|
||||
expect(result.bindings![0].label).toBe('sub_label2');
|
||||
expect(result.bindings![1].label).toBe('sub_label3');
|
||||
});
|
||||
|
||||
it('should handle forms correctly', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app',
|
||||
location: 'sub_location',
|
||||
label: 'sub_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].form).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle submit calls correctly', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app',
|
||||
location: 'sub_location',
|
||||
label: 'sub_label',
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].submit).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should recursively clean sub-bindings', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app',
|
||||
location: 'sub_location',
|
||||
label: 'sub_label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: '',
|
||||
location: 'sub_sub_location1',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
app_id: 'sub_sub_app',
|
||||
location: 'sub_sub_location2',
|
||||
label: 'sub_sub_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].bindings![0].label).toBe('sub_sub_label');
|
||||
});
|
||||
|
||||
it('should handle bindings without bindings, form, or submit', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app',
|
||||
location: 'sub_location',
|
||||
label: 'sub_label',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle multiple levels of nested bindings correctly', () => {
|
||||
const binding: AppBinding = {
|
||||
app_id: 'test_app',
|
||||
location: 'location',
|
||||
label: 'label',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'sub_app1',
|
||||
location: 'sub_location1',
|
||||
label: 'sub_label1',
|
||||
bindings: [
|
||||
{
|
||||
app_id: '',
|
||||
location: 'sub_sub_location1',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
app_id: 'sub_sub_app',
|
||||
location: 'sub_sub_location2',
|
||||
label: 'sub_sub_label',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
app_id: 'sub_app2',
|
||||
location: 'sub_location2',
|
||||
label: 'sub_label2',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'submit_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = cleanBinding(binding, AppBindingLocations.COMMAND);
|
||||
expect(result.bindings).toHaveLength(2);
|
||||
expect(result.bindings![0].bindings).toHaveLength(1);
|
||||
expect(result.bindings![0].bindings![0].label).toBe('sub_sub_label');
|
||||
expect(result.bindings![1].label).toBe('sub_label2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBindings', () => {
|
||||
it('should return an empty array if no bindings are provided', () => {
|
||||
expect(validateBindings()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter and clean bindings by their locations', () => {
|
||||
const bindings: AppBinding[] = [
|
||||
{app_id: '1', location: AppBindingLocations.CHANNEL_HEADER_ICON, label: 'channel_header', bindings: []},
|
||||
{app_id: '2', location: AppBindingLocations.POST_MENU_ITEM, label: 'post_menu', bindings: []},
|
||||
{app_id: '3', location: AppBindingLocations.COMMAND, label: 'command', bindings: []},
|
||||
{app_id: '4', location: 'other', label: 'other', bindings: []},
|
||||
];
|
||||
|
||||
const result = validateBindings(bindings);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only bindings that have sub-bindings after cleaning', () => {
|
||||
const bindings: AppBinding[] = [
|
||||
{app_id: '1', location: AppBindingLocations.CHANNEL_HEADER_ICON, label: 'channel_header', bindings: []},
|
||||
{app_id: '2',
|
||||
location: AppBindingLocations.POST_MENU_ITEM,
|
||||
label: 'post_menu',
|
||||
bindings: [{
|
||||
app_id: '2.1',
|
||||
label: 'sub_binding',
|
||||
location: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'path_2',
|
||||
},
|
||||
},
|
||||
}]},
|
||||
{app_id: '3',
|
||||
location: AppBindingLocations.COMMAND,
|
||||
label: 'command',
|
||||
bindings: [{
|
||||
app_id: '3.1',
|
||||
label: 'sub_binding',
|
||||
location: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'path_3',
|
||||
},
|
||||
},
|
||||
}]},
|
||||
];
|
||||
|
||||
const result = validateBindings(bindings);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].app_id).toBe('2');
|
||||
expect(result[1].app_id).toBe('3');
|
||||
});
|
||||
|
||||
it('should filter out bindings that do not have sub-bindings after cleaning', () => {
|
||||
const bindings: AppBinding[] = [
|
||||
{app_id: '1',
|
||||
location: AppBindingLocations.CHANNEL_HEADER_ICON,
|
||||
label: 'channel_header',
|
||||
bindings: [{
|
||||
app_id: '1.1',
|
||||
label: 'sub_binding',
|
||||
location: '',
|
||||
}]},
|
||||
{app_id: '2', location: AppBindingLocations.POST_MENU_ITEM, label: 'post_menu', bindings: []},
|
||||
{app_id: '3', location: AppBindingLocations.COMMAND, label: 'command', bindings: []},
|
||||
];
|
||||
|
||||
const result = validateBindings(bindings);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle bindings with various sub-bindings and forms correctly', () => {
|
||||
const bindings: AppBinding[] = [
|
||||
{
|
||||
app_id: '1',
|
||||
location: AppBindingLocations.CHANNEL_HEADER_ICON,
|
||||
label: 'channel_header',
|
||||
bindings: [{
|
||||
app_id: '1.1',
|
||||
label: 'sub_binding1',
|
||||
location: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'path_1',
|
||||
},
|
||||
},
|
||||
}]},
|
||||
{
|
||||
app_id: '2',
|
||||
location: AppBindingLocations.POST_MENU_ITEM,
|
||||
label: 'post_menu',
|
||||
bindings: [{
|
||||
app_id: '2.1',
|
||||
label: 'sub_binding2',
|
||||
location: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: 'path_2',
|
||||
},
|
||||
},
|
||||
}]},
|
||||
{app_id: '3', location: AppBindingLocations.COMMAND, label: 'command', bindings: []},
|
||||
];
|
||||
|
||||
const result = validateBindings(bindings);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].app_id).toBe('2');
|
||||
expect(result[1].app_id).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanForm', () => {
|
||||
it('should return immediately if form is undefined', () => {
|
||||
expect(() => cleanForm(undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should remove fields without names', () => {
|
||||
const form: AppForm = {
|
||||
fields: [
|
||||
{name: '', type: 'text'} as AppField,
|
||||
{name: 'valid_name', type: 'text'} as AppField,
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(form);
|
||||
expect(form.fields).toHaveLength(1);
|
||||
expect(form.fields![0].name).toBe('valid_name');
|
||||
});
|
||||
|
||||
it('should remove fields with names containing spaces or tabs', () => {
|
||||
const form: AppForm = {
|
||||
fields: [
|
||||
{name: 'invalid name', type: 'text'} as AppField,
|
||||
{name: 'invalid\tname', type: 'text'} as AppField,
|
||||
{name: 'valid_name', type: 'text'} as AppField,
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(form);
|
||||
expect(form.fields).toHaveLength(1);
|
||||
expect(form.fields![0].name).toBe('valid_name');
|
||||
});
|
||||
|
||||
it('should remove fields with duplicate or invalid labels', () => {
|
||||
const form: AppForm = {
|
||||
fields: [
|
||||
{name: 'name1', type: 'text', label: 'label1'} as AppField,
|
||||
{name: 'name2', type: 'text', label: 'label1'} as AppField, // Duplicate label
|
||||
{name: 'name3', type: 'text', label: 'invalid label'} as AppField,
|
||||
{name: 'name4', type: 'text'} as AppField, // No label, should use name
|
||||
{name: 'name5', type: 'text', label: 'label5'} as AppField,
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(form);
|
||||
expect(form.fields).toHaveLength(3);
|
||||
expect(form.fields![0].name).toBe('name1');
|
||||
expect(form.fields![1].name).toBe('name4');
|
||||
expect(form.fields![2].name).toBe('name5');
|
||||
});
|
||||
|
||||
it('should handle STATIC_SELECT fields and remove invalid options', () => {
|
||||
const form: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
name: 'select_field',
|
||||
type: AppFieldTypes.STATIC_SELECT,
|
||||
options: [
|
||||
{label: 'option1', value: 'value1'},
|
||||
{label: '', value: 'value2'},
|
||||
{label: 'option1', value: 'value3'},
|
||||
{label: 'option4', value: ''},
|
||||
],
|
||||
} as AppField,
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(form);
|
||||
expect(form.fields).toHaveLength(1);
|
||||
expect(form.fields![0].options).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should retain valid fields and options', () => {
|
||||
const form: AppForm = {
|
||||
fields: [
|
||||
{name: 'valid_name', type: 'text'} as AppField,
|
||||
{
|
||||
name: 'select_field',
|
||||
type: AppFieldTypes.STATIC_SELECT,
|
||||
options: [
|
||||
{label: 'option1', value: 'value1'},
|
||||
{label: 'option2', value: 'value2'},
|
||||
],
|
||||
} as AppField,
|
||||
{name: 'dynamic_field_with_lookup', type: AppFieldTypes.DYNAMIC_SELECT, lookup: {path: 'lookup_path'}} as AppField,
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(form);
|
||||
expect(form.fields).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeCallErrorResponse', () => {
|
||||
it('should create an error response object', () => {
|
||||
const errorMessage = 'An error occurred';
|
||||
const errorResponse = makeCallErrorResponse(errorMessage);
|
||||
|
||||
expect(errorResponse.type).toBe(AppCallResponseTypes.ERROR);
|
||||
expect(errorResponse.text).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterEmptyOptions', () => {
|
||||
it('should filter out empty options', () => {
|
||||
const options: AppSelectOption[] = [
|
||||
{label: 'Option 1', value: 'value1'},
|
||||
{label: 'Option 2', value: ' '},
|
||||
{label: 'Option 3', value: ''},
|
||||
{label: 'Option 4', value: 'value2'},
|
||||
];
|
||||
|
||||
const filteredOptions = options.filter(filterEmptyOptions);
|
||||
|
||||
// Check that empty options have been filtered out
|
||||
expect(filteredOptions.length).toBe(2);
|
||||
expect(filteredOptions.map((option) => option.label)).toEqual(['Option 1', 'Option 4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCallContext', () => {
|
||||
it('should create context with all parameters provided', () => {
|
||||
const context = createCallContext('appID123', 'location123', 'channelID123', 'teamID123', 'postID123', 'rootID123');
|
||||
|
||||
expect(context).toEqual({
|
||||
app_id: 'appID123',
|
||||
location: 'location123',
|
||||
channel_id: 'channelID123',
|
||||
team_id: 'teamID123',
|
||||
post_id: 'postID123',
|
||||
root_id: 'rootID123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create context with only appID provided', () => {
|
||||
const context = createCallContext('appID123');
|
||||
|
||||
expect(context).toEqual({
|
||||
app_id: 'appID123',
|
||||
location: undefined,
|
||||
channel_id: undefined,
|
||||
team_id: undefined,
|
||||
post_id: undefined,
|
||||
root_id: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create context with some parameters provided', () => {
|
||||
const context = createCallContext('appID123', 'location123', 'channelID123');
|
||||
|
||||
expect(context).toEqual({
|
||||
app_id: 'appID123',
|
||||
location: 'location123',
|
||||
channel_id: 'channelID123',
|
||||
team_id: undefined,
|
||||
post_id: undefined,
|
||||
root_id: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCallRequest', () => {
|
||||
const mockAppCall: AppCall = {
|
||||
path: '/mock/path',
|
||||
expand: {
|
||||
app: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
const mockAppContext: AppContext = {
|
||||
app_id: 'appID123',
|
||||
location: 'location123',
|
||||
channel_id: 'channelID123',
|
||||
team_id: 'teamID123',
|
||||
post_id: 'postID123',
|
||||
root_id: 'rootID123',
|
||||
};
|
||||
|
||||
const mockDefaultExpand: AppExpand = {
|
||||
user: 'all',
|
||||
post: 'none',
|
||||
};
|
||||
|
||||
const mockValues: AppCallValues = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
};
|
||||
|
||||
it('should create call request with all parameters provided', () => {
|
||||
const rawCommand = '/mock command';
|
||||
const request = createCallRequest(mockAppCall, mockAppContext, mockDefaultExpand, mockValues, rawCommand);
|
||||
|
||||
expect(request).toEqual({
|
||||
...mockAppCall,
|
||||
context: mockAppContext,
|
||||
values: mockValues,
|
||||
expand: {
|
||||
...mockDefaultExpand,
|
||||
...mockAppCall.expand,
|
||||
},
|
||||
raw_command: rawCommand,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create call request with required parameters only', () => {
|
||||
const request = createCallRequest(mockAppCall, mockAppContext);
|
||||
|
||||
expect(request).toEqual({
|
||||
...mockAppCall,
|
||||
context: mockAppContext,
|
||||
expand: {
|
||||
...mockAppCall.expand,
|
||||
},
|
||||
values: undefined,
|
||||
raw_command: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create call request with default expand only', () => {
|
||||
const request = createCallRequest(mockAppCall, mockAppContext, mockDefaultExpand);
|
||||
|
||||
expect(request).toEqual({
|
||||
...mockAppCall,
|
||||
context: mockAppContext,
|
||||
expand: {
|
||||
...mockDefaultExpand,
|
||||
...mockAppCall.expand,
|
||||
},
|
||||
values: undefined,
|
||||
raw_command: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create call request with values only', () => {
|
||||
const request = createCallRequest(mockAppCall, mockAppContext, {}, mockValues);
|
||||
|
||||
expect(request).toEqual({
|
||||
...mockAppCall,
|
||||
context: mockAppContext,
|
||||
expand: {
|
||||
...mockAppCall.expand,
|
||||
},
|
||||
values: mockValues,
|
||||
raw_command: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create call request with raw command only', () => {
|
||||
const rawCommand = '/mock command';
|
||||
const request = createCallRequest(mockAppCall, mockAppContext, {}, undefined, rawCommand);
|
||||
|
||||
expect(request).toEqual({
|
||||
...mockAppCall,
|
||||
context: mockAppContext,
|
||||
expand: {
|
||||
...mockAppCall.expand,
|
||||
},
|
||||
values: undefined,
|
||||
raw_command: rawCommand,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create call request with overridden expand values', () => {
|
||||
const callWithExpandOverride: AppCall = {
|
||||
...mockAppCall,
|
||||
expand: {
|
||||
app: 'none',
|
||||
user: 'summary',
|
||||
},
|
||||
};
|
||||
const request = createCallRequest(callWithExpandOverride, mockAppContext, mockDefaultExpand);
|
||||
|
||||
expect(request).toEqual({
|
||||
...callWithExpandOverride,
|
||||
context: mockAppContext,
|
||||
expand: {
|
||||
...mockDefaultExpand,
|
||||
...callWithExpandOverride.expand,
|
||||
},
|
||||
values: undefined,
|
||||
raw_command: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes} from '@constants/apps';
|
||||
|
||||
import {generateId} from './general';
|
||||
@@ -22,7 +23,7 @@ function cleanBindingRec(binding: AppBinding, topLocation: string, depth: number
|
||||
}
|
||||
|
||||
if (!b.label) {
|
||||
b.label = b.location || '';
|
||||
b.label = binding.label || b.location || '';
|
||||
}
|
||||
|
||||
if (!b.location) {
|
||||
|
||||
129
app/utils/font_family.test.ts
Normal file
129
app/utils/font_family.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {cloneElement} from 'react';
|
||||
import {Text, StyleSheet} from 'react-native';
|
||||
|
||||
import setFontFamily from './font_family';
|
||||
|
||||
// Type for style object
|
||||
type StyleObject = { [key: string]: any };
|
||||
|
||||
// Custom function to flatten styles
|
||||
const flattenStyles = (styles: StyleObject | Array<StyleObject | StyleObject[]>): StyleObject|undefined => {
|
||||
if (styles === null || typeof styles !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!Array.isArray(styles)) {
|
||||
return styles;
|
||||
}
|
||||
|
||||
return styles.reduce((acc, style) => {
|
||||
if (!style) {
|
||||
return acc;
|
||||
} // Skip if style is null or undefined
|
||||
|
||||
if (Array.isArray(style)) {
|
||||
// Merge arrays of styles
|
||||
return style.reduce((prev, curr) => ({
|
||||
...prev,
|
||||
...flattenStyles(curr),
|
||||
}), acc);
|
||||
} else if (typeof style === 'object') {
|
||||
// Merge objects of styles
|
||||
return {...acc, ...style};
|
||||
}
|
||||
|
||||
// Skip if style is not an object or array
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Mock the necessary parts of react-native
|
||||
jest.mock('react-native', () => ({
|
||||
Text: {
|
||||
render: jest.fn(),
|
||||
},
|
||||
StyleSheet: {
|
||||
create: jest.fn((styles) => styles),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock cloneElement
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
cloneElement: jest.fn((element, props) => {
|
||||
// Merge the existing style with the new style
|
||||
const mergedStyle = flattenStyles([props.style, ...(element.props.style ? [element.props.style] : [])]);
|
||||
|
||||
// Return a new object with merged styles
|
||||
return {
|
||||
...element,
|
||||
props: {
|
||||
...element.props,
|
||||
style: mergedStyle,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('setFontFamily', () => {
|
||||
let originalTextRender: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Capture the original Text.render before modification
|
||||
// @ts-expect-error renderer is not exposed to TS definition
|
||||
originalTextRender = Text.render;
|
||||
});
|
||||
|
||||
// Restore the original implementations after each test
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('overrides Text.render and applies custom styles', () => {
|
||||
// Call the function to set the font family
|
||||
setFontFamily();
|
||||
|
||||
// Check if the StyleSheet.create was called with the correct styles
|
||||
expect(StyleSheet.create).toHaveBeenCalledWith({
|
||||
defaultText: {
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if Text.render was overridden
|
||||
// @ts-expect-error renderer is not exposed to TS definition
|
||||
expect(Text.render).not.toBe(originalTextRender);
|
||||
|
||||
// Create a mock origin render output
|
||||
const mockOriginRenderOutput = {
|
||||
props: {
|
||||
style: [{color: 'red'}],
|
||||
},
|
||||
};
|
||||
|
||||
// Set the old render function to return the mock output
|
||||
(originalTextRender as jest.Mock).mockReturnValue(mockOriginRenderOutput);
|
||||
|
||||
// Call the new render function
|
||||
// @ts-expect-error renderer is not exposed to TS definition
|
||||
const newRenderOutput = Text.render();
|
||||
|
||||
// Check if cloneElement was called with the correct arguments
|
||||
expect(cloneElement).toHaveBeenCalledWith(mockOriginRenderOutput, {
|
||||
style: [
|
||||
{
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 16,
|
||||
},
|
||||
[{color: 'red'}],
|
||||
],
|
||||
});
|
||||
|
||||
// Verify the new render output has the expected styles
|
||||
expect(newRenderOutput.props.style).toEqual({fontFamily: 'OpenSans', fontSize: 16, color: 'red'});
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,11 @@ const setFontFamily = () => {
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-expect-error renderer is not exposed to TS definition
|
||||
const oldRender = Text.render;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-expect-error renderer is not exposed to TS definition
|
||||
Text.render = function render(...args) {
|
||||
const origin = oldRender.call(this, ...args);
|
||||
return cloneElement(origin, {
|
||||
|
||||
10
app/utils/groups.test.ts
Normal file
10
app/utils/groups.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {generateGroupAssociationId} from './groups';
|
||||
|
||||
describe('groups utility', () => {
|
||||
test('generateGroupAssociationId', () => {
|
||||
expect(generateGroupAssociationId('groupId', 'otherId')).toEqual('groupId-otherId');
|
||||
});
|
||||
});
|
||||
@@ -1,48 +1,240 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import moment from 'moment-timezone';
|
||||
import {NativeModules, Platform} from 'react-native';
|
||||
|
||||
import {areBothStringArraysEqual} from '@utils/helpers';
|
||||
import {STATUS_BAR_HEIGHT} from '@constants/view';
|
||||
|
||||
describe('areBothStringArraysEqual', () => {
|
||||
test('Should return false when length of arrays are not equal', () => {
|
||||
const array1 = ['test1', 'test2'];
|
||||
const array2 = ['test1'];
|
||||
import {
|
||||
isMinimumServerVersion,
|
||||
buildQueryString,
|
||||
isEmail,
|
||||
identity,
|
||||
safeParseJSON,
|
||||
getCurrentMomentForTimezone,
|
||||
getUtcOffsetForTimeZone,
|
||||
toTitleCase,
|
||||
getRoundedTime,
|
||||
isTablet,
|
||||
pluckUnique,
|
||||
bottomSheetSnapPoint,
|
||||
hasTrailingSpaces,
|
||||
isMainActivity,
|
||||
areBothStringArraysEqual,
|
||||
} from './helpers';
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(false);
|
||||
jest.mock('@mattermost/rnshare', () => ({
|
||||
default: {
|
||||
getCurrentActivityName: jest.fn().
|
||||
mockReturnValueOnce('MainActivity').
|
||||
mockReturnValue('SomeOtherActivity'),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Helpers', () => {
|
||||
afterAll(() => {
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
test('Should return false when arrays are not equal', () => {
|
||||
const array1 = ['test1', 'test2'];
|
||||
const array2 = ['test1', 'test2', 'test3'];
|
||||
describe('isMinimumServerVersion', () => {
|
||||
test('should return true if server version meets minimum requirements', () => {
|
||||
expect(isMinimumServerVersion('4.6.0', 4, 6, 0)).toBe(true);
|
||||
expect(isMinimumServerVersion('4.6.0', 4, 5, 0)).toBe(true);
|
||||
expect(isMinimumServerVersion('4.6.1', 4, 6, 0)).toBe(true);
|
||||
expect(isMinimumServerVersion('4')).toBe(true);
|
||||
expect(isMinimumServerVersion('4.6', 4, 6)).toBe(true);
|
||||
});
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(false);
|
||||
test('should return false if server version does not meet minimum requirements', () => {
|
||||
expect(isMinimumServerVersion('4.5.0', 4, 6, 0)).toBe(false);
|
||||
expect(isMinimumServerVersion('4.6.0', 4, 6, 1)).toBe(false);
|
||||
});
|
||||
|
||||
test('currentVersion is not set or not a string', () => {
|
||||
expect(isMinimumServerVersion()).toBe(false);
|
||||
|
||||
// @ts-expect-error argument should be a string
|
||||
expect(isMinimumServerVersion(0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('Should return false when either array is empty', () => {
|
||||
const array1 = ['test1', 'test2'];
|
||||
const array2: string[] = [];
|
||||
describe('buildQueryString', () => {
|
||||
test('should build query string from object', () => {
|
||||
const parameters = {key1: 'value1', key2: 'value2'};
|
||||
expect(buildQueryString(parameters)).toBe('?key1=value1&key2=value2');
|
||||
expect(buildQueryString({...parameters, key3: null})).toBe('?key1=value1&key2=value2');
|
||||
expect(buildQueryString({key0: null, ...parameters, key3: null})).toBe('?key1=value1&key2=value2');
|
||||
expect(buildQueryString({key1: 'value1', key0: null, key3: null, key2: 'value2'})).toBe('?key1=value1&key2=value2');
|
||||
});
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(false);
|
||||
test('should handle empty object', () => {
|
||||
expect(buildQueryString({})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
test('Should return true when arrays are equal', () => {
|
||||
const array1 = ['test1', 'test2'];
|
||||
const array2 = ['test1', 'test2'];
|
||||
describe('isEmail', () => {
|
||||
test('should validate correct emails', () => {
|
||||
expect(isEmail('test@example.com')).toBe(true);
|
||||
expect(isEmail('another@test.com')).toBe(true);
|
||||
});
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(true);
|
||||
test('should invalidate incorrect emails', () => {
|
||||
expect(isEmail('invalid')).toBe(false);
|
||||
expect(isEmail('test@')).toBe(false);
|
||||
expect(isEmail('test@localhost')).toBe(true);
|
||||
expect(isEmail('test@example.com')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Should return true when arrays are equal but in different order', () => {
|
||||
const array1 = ['test1', 'test2'];
|
||||
const array2 = ['test2', 'test1'];
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(true);
|
||||
describe('identity', () => {
|
||||
test('should return the same argument', () => {
|
||||
expect(identity('test')).toBe('test');
|
||||
expect(identity(123)).toBe(123);
|
||||
const obj = {};
|
||||
expect(identity(obj)).toBe(obj);
|
||||
});
|
||||
});
|
||||
|
||||
test('Should return true when both arrays are empty', () => {
|
||||
const array1: string[] = [];
|
||||
const array2: string[] = [];
|
||||
describe('safeParseJSON', () => {
|
||||
test('should parse valid JSON string', () => {
|
||||
const jsonString = '{"key": "value"}';
|
||||
expect(safeParseJSON(jsonString)).toEqual({key: 'value'});
|
||||
});
|
||||
|
||||
expect(areBothStringArraysEqual(array1, array2)).toEqual(false);
|
||||
test('should handle invalid JSON', () => {
|
||||
expect(safeParseJSON('invalid-json')).toBe('invalid-json');
|
||||
});
|
||||
|
||||
test('should handle non-string input', () => {
|
||||
expect(safeParseJSON({key: 'value'})).toEqual({key: 'value'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentMomentForTimezone', () => {
|
||||
test('should return current moment in specified timezone', () => {
|
||||
const timezone = 'America/New_York';
|
||||
const result = getCurrentMomentForTimezone(timezone);
|
||||
expect(result.isValid()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return current moment if no timezone specified', () => {
|
||||
const result = getCurrentMomentForTimezone(null);
|
||||
expect(result.isValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTitleCase', () => {
|
||||
test('should convert string to title case', () => {
|
||||
expect(toTitleCase('hello world')).toBe('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoundedTime', () => {
|
||||
test('should round time to nearest interval', () => {
|
||||
const time = moment('2024-06-01T12:34:00Z');
|
||||
const result = getRoundedTime(time);
|
||||
expect(result.minute() % 30).toBe(0); // Assuming CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES is 15
|
||||
|
||||
const time2 = moment('2024-06-01T12:00:00Z');
|
||||
const result2 = getRoundedTime(time2);
|
||||
expect(result2.minute() % 30).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTablet', () => {
|
||||
test('should identify tablet correctly', () => {
|
||||
console.log('split mock', NativeModules.RNUtils.isRunningInSplitView());
|
||||
expect(isTablet()).toBe(false);
|
||||
|
||||
const prevSplitViewModule = NativeModules.RNUtils.isRunningInSplitView;
|
||||
NativeModules.RNUtils.isRunningInSplitView = jest.fn().mockReturnValue({isSplit: false, isTablet: true});
|
||||
console.log('split mock', NativeModules.RNUtils.isRunningInSplitView());
|
||||
expect(isTablet()).toBe(true);
|
||||
NativeModules.RNUtils.isRunningInSplitView = jest.fn().mockReturnValue({isSplit: true, isTablet: true});
|
||||
expect(isTablet()).toBe(false);
|
||||
NativeModules.RNUtils.isRunningInSplitView = prevSplitViewModule; // Restore original value
|
||||
});
|
||||
});
|
||||
|
||||
// Add tests for other functions similarly
|
||||
|
||||
describe('areBothStringArraysEqual', () => {
|
||||
test('should compare two string arrays', () => {
|
||||
const arr1 = ['apple', 'banana', 'cherry'];
|
||||
const arr2 = ['banana', 'cherry', 'apple'];
|
||||
expect(areBothStringArraysEqual(arr1, arr2)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for unequal arrays or empty', () => {
|
||||
const arr1 = ['apple', 'banana', 'cherry'];
|
||||
const arr2 = ['banana', 'cherry', 'orange'];
|
||||
expect(areBothStringArraysEqual(arr1, arr2)).toBe(false);
|
||||
expect(areBothStringArraysEqual(arr1, [...arr1, ...arr2])).toBe(false);
|
||||
expect(areBothStringArraysEqual([], [])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUtcOffsetForTimeZone', () => {
|
||||
test('should return UTC offset for timezone', () => {
|
||||
const timezone = 'America/New_York';
|
||||
const result = getUtcOffsetForTimeZone(timezone);
|
||||
expect(result).toBe(moment.tz(timezone).utcOffset());
|
||||
});
|
||||
});
|
||||
describe('pluckUnique', () => {
|
||||
test('should pluck unique values based on key', () => {
|
||||
const array = [
|
||||
{id: 1, name: 'John'},
|
||||
{id: 2, name: 'Jane'},
|
||||
{id: 1, name: 'John'},
|
||||
];
|
||||
const result = pluckUnique('id')(array);
|
||||
expect(result).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bottomSheetSnapPoint', () => {
|
||||
test('should calculate bottom sheet snap point', () => {
|
||||
const itemsCount = 5;
|
||||
const itemHeight = 50;
|
||||
const bottomInset = 20;
|
||||
const result = bottomSheetSnapPoint(itemsCount, itemHeight, bottomInset);
|
||||
const expected = (itemsCount * itemHeight) + bottomInset + STATUS_BAR_HEIGHT;
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasTrailingSpaces', () => {
|
||||
test('should detect trailing spaces', () => {
|
||||
const term = 'Hello ';
|
||||
const result = hasTrailingSpaces(term);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should not detect trailing spaces', () => {
|
||||
const term = 'Hello';
|
||||
const result = hasTrailingSpaces(term);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMainActivity', () => {
|
||||
test('should return true on iOS', () => {
|
||||
Platform.OS = 'ios';
|
||||
const result = isMainActivity();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true if current activity is MainActivity on Android', () => {
|
||||
Platform.OS = 'android';
|
||||
const result = isMainActivity();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if current activity is not MainActivity on Android', () => {
|
||||
Platform.OS = 'android';
|
||||
const result = isMainActivity();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,12 +8,6 @@ import {Platform} from 'react-native';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
import {STATUS_BAR_HEIGHT} from '@constants/view';
|
||||
|
||||
const {isRunningInSplitView} = RNUtils;
|
||||
const MattermostShare = Platform.select({
|
||||
default: null,
|
||||
android: require('@mattermost/rnshare').default,
|
||||
});
|
||||
|
||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||
// the provided minimum version. A non-equal major version will ignore minor and dot
|
||||
// versions, and a non-equal minor version will ignore dot version.
|
||||
@@ -76,6 +70,10 @@ export function buildQueryString(parameters: Dictionary<any>): string {
|
||||
}
|
||||
}
|
||||
|
||||
if (query.endsWith('&')) {
|
||||
return query.slice(0, -1);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -86,7 +84,10 @@ export function isEmail(email: string): boolean {
|
||||
// - followed by a single @ symbol
|
||||
// - followed by at least one character that is not a space, comma, or @ symbol
|
||||
// this prevents <Outlook Style> outlook.style@domain.com addresses and multiple comma-separated addresses from being accepted
|
||||
return (/^[^ ,@]+@[^ ,@]+$/).test(email);
|
||||
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const regexWithoutTLDN = /^[^\s@]+@[^\s@]+$/;
|
||||
|
||||
return regex.test(email) || regexWithoutTLDN.test(email);
|
||||
}
|
||||
|
||||
export function identity<T>(arg: T): T {
|
||||
@@ -133,7 +134,7 @@ export function getRoundedTime(value: Moment) {
|
||||
}
|
||||
|
||||
export function isTablet() {
|
||||
const result: SplitViewResult = isRunningInSplitView();
|
||||
const result: SplitViewResult = RNUtils.isRunningInSplitView();
|
||||
return result.isTablet && !result.isSplit;
|
||||
}
|
||||
|
||||
@@ -155,10 +156,12 @@ export function hasTrailingSpaces(term: string) {
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isMainActivity() {
|
||||
return Platform.select({
|
||||
default: true,
|
||||
android: MattermostShare?.getCurrentActivityName() === 'MainActivity',
|
||||
});
|
||||
if (Platform.OS === 'android') {
|
||||
const MattermostShare = require('@mattermost/rnshare').default;
|
||||
return MattermostShare?.getCurrentActivityName() === 'MainActivity';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areBothStringArraysEqual(a: string[], b: string[]) {
|
||||
|
||||
309
app/utils/integrations.test.ts
Normal file
309
app/utils/integrations.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {checkDialogElementForError, checkIfErrorsMatchElements, selectKeyboardType} from './integrations';
|
||||
|
||||
describe('checkDialogElementForError', () => {
|
||||
test('returns required error for empty required field', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
optional: false,
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const result = checkDialogElementForError(elem, '');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.required',
|
||||
defaultMessage: 'This field is required.',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns too short error for text shorter than min_length', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
min_length: 5,
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const result = checkDialogElementForError(elem, '123');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.too_short',
|
||||
defaultMessage: 'Minimum input length is {minLength}.',
|
||||
values: {minLength: 5},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns bad email error for invalid email', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
subtype: 'email',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const result = checkDialogElementForError(elem, 'invalidemail');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.bad_email',
|
||||
defaultMessage: 'Must be a valid email address.',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns bad number error for invalid number', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
subtype: 'number',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const result = checkDialogElementForError(elem, 'notanumber');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.bad_number',
|
||||
defaultMessage: 'Must be a number.',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns bad URL error for invalid URL', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
subtype: 'url',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const result = checkDialogElementForError(elem, 'invalidurl');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.bad_url',
|
||||
defaultMessage: 'URL must include http:// or https://.',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns invalid option error for invalid radio option', () => {
|
||||
const elem: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'radio',
|
||||
options: [{
|
||||
value: 'option1',
|
||||
text: '',
|
||||
}, {
|
||||
value: 'option2',
|
||||
text: '',
|
||||
}],
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
};
|
||||
const result = checkDialogElementForError(elem, 'invalidoption');
|
||||
expect(result).toEqual({
|
||||
id: 'interactive_dialog.error.invalid_option',
|
||||
defaultMessage: 'Must be a valid option',
|
||||
});
|
||||
});
|
||||
|
||||
test('returns null for valid inputs', () => {
|
||||
const elemText: DialogElement = {
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
min_length: 3,
|
||||
display_name: '',
|
||||
subtype: 'password',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const elemEmail: DialogElement = {
|
||||
name: 'field2',
|
||||
type: 'text',
|
||||
subtype: 'email',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const elemNumber: DialogElement = {
|
||||
name: 'field3',
|
||||
type: 'text',
|
||||
subtype: 'number',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const elemURL: DialogElement = {
|
||||
name: 'field4',
|
||||
type: 'text',
|
||||
subtype: 'url',
|
||||
display_name: '',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
};
|
||||
const elemRadio: DialogElement = {
|
||||
name: 'field5',
|
||||
type: 'radio',
|
||||
options: [{
|
||||
value: 'option1',
|
||||
text: '',
|
||||
}, {
|
||||
value: 'option2',
|
||||
text: '',
|
||||
}],
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
};
|
||||
|
||||
expect(checkDialogElementForError(elemText, 'valid')).toBeNull();
|
||||
expect(checkDialogElementForError(elemEmail, 'email@example.com')).toBeNull();
|
||||
expect(checkDialogElementForError(elemNumber, '123')).toBeNull();
|
||||
expect(checkDialogElementForError(elemURL, 'http://example.com')).toBeNull();
|
||||
expect(checkDialogElementForError(elemRadio, 'option1')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkIfErrorsMatchElements', () => {
|
||||
test('returns false if no dialog elements', () => {
|
||||
const errors = {field1: 'error'};
|
||||
expect(checkIfErrorsMatchElements(errors)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if no dialog errprs', () => {
|
||||
expect(checkIfErrorsMatchElements()).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true if errors match elements', () => {
|
||||
const elements: DialogElement[] = [{
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
}];
|
||||
const errors = {field1: 'error'};
|
||||
expect(checkIfErrorsMatchElements(errors, elements)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false if errors do not match elements', () => {
|
||||
const elements: DialogElement[] = [{
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
display_name: '',
|
||||
subtype: 'number',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
help_text: '',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [],
|
||||
}];
|
||||
const errors = {field2: 'error'};
|
||||
expect(checkIfErrorsMatchElements(errors, elements)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if errors and elements are empty', () => {
|
||||
expect(checkIfErrorsMatchElements({}, [])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectKeyboardType', () => {
|
||||
test('returns email-address for email subtype', () => {
|
||||
expect(selectKeyboardType('email')).toBe('email-address');
|
||||
});
|
||||
|
||||
test('returns numeric for number subtype', () => {
|
||||
expect(selectKeyboardType('number')).toBe('numeric');
|
||||
});
|
||||
|
||||
test('returns phone-pad for tel subtype', () => {
|
||||
expect(selectKeyboardType('tel')).toBe('phone-pad');
|
||||
});
|
||||
|
||||
test('returns url for url subtype', () => {
|
||||
expect(selectKeyboardType('url')).toBe('url');
|
||||
});
|
||||
|
||||
test('returns default for undefined subtype', () => {
|
||||
expect(selectKeyboardType()).toBe('default');
|
||||
});
|
||||
|
||||
test('returns default for unrecognized subtype', () => {
|
||||
expect(selectKeyboardType('unrecognized')).toBe('default');
|
||||
});
|
||||
});
|
||||
28
app/utils/key_mirror.test.ts
Normal file
28
app/utils/key_mirror.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from './key_mirror';
|
||||
|
||||
describe('keyMirror', () => {
|
||||
test('creates an object with keys mirrored as values', () => {
|
||||
const input = {key1: null, key2: null, key3: null};
|
||||
const expectedOutput = {key1: 'key1', key2: 'key2', key3: 'key3'};
|
||||
expect(keyMirror(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('throws an error if argument is not an object', () => {
|
||||
// @ts-expect-error null will complain by TS
|
||||
expect(() => keyMirror(null)).toThrow('keyMirror(...): Argument must be an object.');
|
||||
expect(() => keyMirror([])).toThrow('keyMirror(...): Argument must be an object.');
|
||||
expect(() => keyMirror('string')).toThrow('keyMirror(...): Argument must be an object.');
|
||||
expect(() => keyMirror(42)).toThrow('keyMirror(...): Argument must be an object.');
|
||||
expect(() => keyMirror(true)).toThrow('keyMirror(...): Argument must be an object.');
|
||||
});
|
||||
|
||||
test('ignores properties from the prototype chain', () => {
|
||||
const input = Object.create({inheritedKey: null});
|
||||
input.ownKey = null;
|
||||
const expectedOutput = {ownKey: 'ownKey'};
|
||||
expect(keyMirror(input)).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
79
app/utils/log.test.ts
Normal file
79
app/utils/log.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from '@utils/key_mirror';
|
||||
|
||||
import {logError, logWarning, logInfo, logDebug} from './log';
|
||||
|
||||
// Mock Sentry
|
||||
jest.mock('@sentry/react-native', () => ({
|
||||
addBreadcrumb: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock console methods
|
||||
const originalConsole = global.console;
|
||||
|
||||
// @ts-expect-error global not in TS def
|
||||
global.console = {
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
log: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Logging functions', () => {
|
||||
const Sentry = require('@sentry/react-native');
|
||||
const SentryLevels = keyMirror({debug: null, info: null, warning: null, error: null});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.console = originalConsole;
|
||||
});
|
||||
|
||||
test('logError logs error and adds breadcrumb', () => {
|
||||
const args = ['Error message'];
|
||||
logError(...args);
|
||||
expect(console.error).toHaveBeenCalledWith(...args);
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
level: SentryLevels.error,
|
||||
message: args.join(','),
|
||||
type: 'console-log',
|
||||
});
|
||||
});
|
||||
|
||||
test('logWarning logs warning and adds breadcrumb', () => {
|
||||
const args = ['Warning message'];
|
||||
logWarning(...args);
|
||||
expect(console.warn).toHaveBeenCalledWith(...args);
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
level: SentryLevels.warning,
|
||||
message: args.join(','),
|
||||
type: 'console-log',
|
||||
});
|
||||
});
|
||||
|
||||
test('logInfo logs info and adds breadcrumb', () => {
|
||||
const args = ['Info message'];
|
||||
logInfo(...args);
|
||||
expect(console.log).toHaveBeenCalledWith(...args);
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
level: SentryLevels.info,
|
||||
message: args.join(','),
|
||||
type: 'console-log',
|
||||
});
|
||||
});
|
||||
|
||||
test('logDebug logs debug and adds breadcrumb', () => {
|
||||
const args = ['Debug message'];
|
||||
logDebug(...args);
|
||||
expect(console.debug).toHaveBeenCalledWith(...args);
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
level: SentryLevels.debug,
|
||||
message: args.join(','),
|
||||
type: 'console-log',
|
||||
});
|
||||
});
|
||||
});
|
||||
102
app/utils/mattermost_managed.test.ts
Normal file
102
app/utils/mattermost_managed.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import RNUtils from '@mattermost/rnutils';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {
|
||||
getIOSAppGroupDetails,
|
||||
deleteIOSDatabase,
|
||||
renameIOSDatabase,
|
||||
deleteEntitiesFile,
|
||||
} from './mattermost_managed';
|
||||
|
||||
jest.mock('@mattermost/rnutils', () => ({
|
||||
getConstants: jest.fn(),
|
||||
deleteDatabaseDirectory: jest.fn(),
|
||||
renameDatabase: jest.fn(),
|
||||
deleteEntitiesFile: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('iOS Utilities', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getIOSAppGroupDetails', () => {
|
||||
test('retrieves iOS AppGroup details correctly', () => {
|
||||
const constantsMock = {
|
||||
appGroupIdentifier: 'group.com.example.app',
|
||||
appGroupSharedDirectory: {sharedDirectory: '/shared', databasePath: '/shared/database'},
|
||||
};
|
||||
|
||||
//@ts-expect-error mockReturnValue
|
||||
RNUtils.getConstants.mockReturnValue(constantsMock);
|
||||
|
||||
const details = getIOSAppGroupDetails();
|
||||
|
||||
expect(details).toEqual({
|
||||
appGroupIdentifier: 'group.com.example.app',
|
||||
appGroupSharedDirectory: '/shared',
|
||||
appGroupDatabase: '/shared/database',
|
||||
});
|
||||
expect(RNUtils.getConstants).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIOSDatabase', () => {
|
||||
test('deletes iOS database with given parameters', async () => {
|
||||
//@ts-expect-error mockReturnValue
|
||||
const deleteMock = RNUtils.deleteDatabaseDirectory.mockResolvedValue(true);
|
||||
|
||||
const result = await deleteIOSDatabase({databaseName: 'test.db', shouldRemoveDirectory: true});
|
||||
|
||||
expect(deleteMock).toHaveBeenCalledWith('test.db', true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('deletes iOS database with default parameters', async () => {
|
||||
//@ts-expect-error mockReturnValue
|
||||
const deleteMock = RNUtils.deleteDatabaseDirectory.mockResolvedValue(true);
|
||||
|
||||
const result = await deleteIOSDatabase({});
|
||||
|
||||
expect(deleteMock).toHaveBeenCalledWith('', false);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameIOSDatabase', () => {
|
||||
test('renames iOS database with given parameters', () => {
|
||||
//@ts-expect-error mockReturnValue
|
||||
const renameMock = RNUtils.renameDatabase.mockResolvedValue(true);
|
||||
|
||||
const result = renameIOSDatabase('old.db', 'new.db');
|
||||
|
||||
expect(renameMock).toHaveBeenCalledWith('old.db', 'new.db');
|
||||
expect(result).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEntitiesFile', () => {
|
||||
test('deletes entities file on iOS', () => {
|
||||
Platform.OS = 'ios';
|
||||
|
||||
//@ts-expect-error mockReturnValue
|
||||
const deleteMock = RNUtils.deleteEntitiesFile.mockResolvedValue(true);
|
||||
|
||||
const result = deleteEntitiesFile();
|
||||
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
expect(result).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('resolves true on non-iOS platforms', () => {
|
||||
Platform.OS = 'android';
|
||||
|
||||
const result = deleteEntitiesFile();
|
||||
|
||||
expect(result).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
app/utils/message_attachment_colors.test.ts
Normal file
44
app/utils/message_attachment_colors.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
|
||||
import {getStatusColors} from './message_attachment_colors';
|
||||
|
||||
describe('getStatusColors', () => {
|
||||
const mockTheme = Preferences.THEMES.denim;
|
||||
|
||||
test('returns correct status colors based on theme', () => {
|
||||
const expectedColors: Dictionary<string> = {
|
||||
good: '#00c100',
|
||||
warning: '#dede01',
|
||||
danger: '#d24b4e',
|
||||
default: '#3f4350',
|
||||
primary: '#1c58d9',
|
||||
success: '#3db887',
|
||||
};
|
||||
|
||||
const statusColors = getStatusColors(mockTheme);
|
||||
expect(statusColors).toEqual(expectedColors);
|
||||
});
|
||||
|
||||
test('returns the correct danger color from the theme', () => {
|
||||
const statusColors = getStatusColors(mockTheme);
|
||||
expect(statusColors.danger).toBe(mockTheme.errorTextColor);
|
||||
});
|
||||
|
||||
test('returns the correct default color from the theme', () => {
|
||||
const statusColors = getStatusColors(mockTheme);
|
||||
expect(statusColors.default).toBe(mockTheme.centerChannelColor);
|
||||
});
|
||||
|
||||
test('returns the correct primary color from the theme', () => {
|
||||
const statusColors = getStatusColors(mockTheme);
|
||||
expect(statusColors.primary).toBe(mockTheme.buttonBg);
|
||||
});
|
||||
|
||||
test('returns the correct success color from the theme', () => {
|
||||
const statusColors = getStatusColors(mockTheme);
|
||||
expect(statusColors.success).toBe(mockTheme.onlineIndicator);
|
||||
});
|
||||
});
|
||||
63
app/utils/mix.test.ts
Normal file
63
app/utils/mix.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import mix from './mix';
|
||||
|
||||
describe('MixinBuilder', () => {
|
||||
class BaseClass {
|
||||
baseMethod() {
|
||||
return 'base method';
|
||||
}
|
||||
}
|
||||
|
||||
const MixinA = (superclass: any) => class extends superclass {
|
||||
mixinAMethod() {
|
||||
return 'mixin A method';
|
||||
}
|
||||
};
|
||||
|
||||
const MixinB = (superclass: any) => class extends superclass {
|
||||
mixinBMethod() {
|
||||
return 'mixin B method';
|
||||
}
|
||||
};
|
||||
|
||||
test('applies mixins correctly', () => {
|
||||
const MixedClass = mix(BaseClass).with(MixinA, MixinB);
|
||||
const instance = new MixedClass();
|
||||
|
||||
expect(instance.baseMethod()).toBe('base method');
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(instance.mixinAMethod()).toBe('mixin A method');
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(instance.mixinBMethod()).toBe('mixin B method');
|
||||
});
|
||||
|
||||
test('applies a single mixin correctly', () => {
|
||||
const MixedClass = mix(BaseClass).with(MixinA);
|
||||
const instance = new MixedClass();
|
||||
|
||||
expect(instance.baseMethod()).toBe('base method');
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(instance.mixinAMethod()).toBe('mixin A method');
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(() => instance.mixinBMethod()).toThrow(TypeError);
|
||||
});
|
||||
|
||||
test('returns the superclass if no mixins are provided', () => {
|
||||
const MixedClass = mix(BaseClass).with();
|
||||
const instance = new MixedClass();
|
||||
|
||||
expect(instance.baseMethod()).toBe('base method');
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(() => instance.mixinAMethod()).toThrow(TypeError);
|
||||
|
||||
// @ts-expect-error mixin method not defined
|
||||
expect(() => instance.mixinBMethod()).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
64
app/utils/opengraph.test.ts
Normal file
64
app/utils/opengraph.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getDistanceBW2Points, getNearestPoint} from './opengraph';
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('getDistanceBW2Points', () => {
|
||||
test('calculates the distance correctly', () => {
|
||||
const point1 = {x: 1, y: 1};
|
||||
const point2 = {x: 4, y: 5};
|
||||
const distance = getDistanceBW2Points(point1, point2);
|
||||
expect(distance).toBeCloseTo(5);
|
||||
});
|
||||
|
||||
test('calculates the distance correctly with custom attributes', () => {
|
||||
const point1 = {a: 1, b: 1};
|
||||
const point2 = {a: 4, b: 5};
|
||||
const distance = getDistanceBW2Points(point1, point2, 'a', 'b');
|
||||
expect(distance).toBeCloseTo(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNearestPoint', () => {
|
||||
test('finds the nearest point correctly', () => {
|
||||
const pivotPoint = {height: 0, width: 0};
|
||||
const points = [
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 2},
|
||||
{x: -1, y: -1},
|
||||
] as never[];
|
||||
const nearestPoint = getNearestPoint(pivotPoint, points);
|
||||
expect(nearestPoint).toEqual({x: 1, y: 1});
|
||||
});
|
||||
|
||||
test('returns an empty object if points array is empty', () => {
|
||||
const pivotPoint = {height: 0, width: 0};
|
||||
const points: never[] = [];
|
||||
const nearestPoint = getNearestPoint(pivotPoint, points);
|
||||
expect(nearestPoint).toEqual({});
|
||||
});
|
||||
|
||||
test('finds the nearest point with custom attributes', () => {
|
||||
const pivotPoint = {height: 0, width: 0};
|
||||
const points = [
|
||||
{a: 1, b: 1},
|
||||
{a: 2, b: 2},
|
||||
{a: -1, b: -1},
|
||||
] as never[];
|
||||
const nearestPoint = getNearestPoint(pivotPoint, points, 'a', 'b');
|
||||
expect(nearestPoint).toEqual({a: 1, b: 1});
|
||||
});
|
||||
|
||||
test('updates nearest point based on distance comparison', () => {
|
||||
const pivotPoint = {height: 0, width: 0};
|
||||
const points = [
|
||||
{x: 5, y: 5},
|
||||
{x: 2, y: 2},
|
||||
{x: 3, y: 3},
|
||||
] as never[];
|
||||
const nearestPoint = getNearestPoint(pivotPoint, points);
|
||||
expect(nearestPoint).toEqual({x: 2, y: 2});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,10 +16,11 @@ export function getDistanceBW2Points(point1: Record<string, any>, point2: Record
|
||||
*/
|
||||
export function getNearestPoint(pivotPoint: {height: number; width: number}, points: never[], xAttr = 'x', yAttr = 'y') {
|
||||
let nearestPoint: Record<string, any> = {};
|
||||
const pivot = {[xAttr]: pivotPoint.width, [yAttr]: pivotPoint.height};
|
||||
for (const point of points) {
|
||||
if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') {
|
||||
nearestPoint = point;
|
||||
} else if (getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPoint, pivotPoint, xAttr, yAttr)) {
|
||||
} else if (getDistanceBW2Points(point, pivot, xAttr, yAttr) < getDistanceBW2Points(nearestPoint, pivot, xAttr, yAttr)) {
|
||||
// Check for bestImage
|
||||
nearestPoint = point;
|
||||
}
|
||||
|
||||
156
app/utils/push_proxy.test.ts
Normal file
156
app/utils/push_proxy.test.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {
|
||||
storePushDisabledInServerAcknowledged,
|
||||
} from '@actions/app/global';
|
||||
import {
|
||||
PUSH_PROXY_RESPONSE_NOT_AVAILABLE,
|
||||
PUSH_PROXY_RESPONSE_UNKNOWN,
|
||||
PUSH_PROXY_STATUS_NOT_AVAILABLE,
|
||||
PUSH_PROXY_STATUS_UNKNOWN,
|
||||
PUSH_PROXY_STATUS_VERIFIED,
|
||||
} from '@constants/push_proxy';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import {
|
||||
pushDisabledInServerAck,
|
||||
canReceiveNotifications,
|
||||
alertPushProxyError,
|
||||
alertPushProxyUnknown,
|
||||
} from './push_proxy';
|
||||
import {urlSafeBase64Encode} from './security';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
jest.mock('react-native', () => ({
|
||||
Alert: {
|
||||
alert: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@actions/app/global', () => ({
|
||||
storePushDisabledInServerAcknowledged: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/app/global', () => ({
|
||||
getPushDisabledInServerAcknowledged: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@store/ephemeral_store', () => ({
|
||||
setPushProxyVerificationState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./security', () => ({
|
||||
urlSafeBase64Encode: jest.fn((url: string) => `encoded-${url}`),
|
||||
}));
|
||||
|
||||
// Mock pushDisabledInServerAck as it's not being mocked by default
|
||||
jest.mock('./push_proxy', () => ({
|
||||
...jest.requireActual('./push_proxy'),
|
||||
pushDisabledInServerAck: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Notification utilities', () => {
|
||||
const intl: IntlShape = {
|
||||
formatMessage: ({defaultMessage}: { defaultMessage: string }) => defaultMessage,
|
||||
} as IntlShape;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('canReceiveNotifications', () => {
|
||||
test('handles PUSH_PROXY_RESPONSE_NOT_AVAILABLE', async () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
(pushDisabledInServerAck as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await canReceiveNotifications(serverUrl, PUSH_PROXY_RESPONSE_NOT_AVAILABLE, intl);
|
||||
|
||||
expect(EphemeralStore.setPushProxyVerificationState).toHaveBeenCalledWith(serverUrl, PUSH_PROXY_STATUS_NOT_AVAILABLE);
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles PUSH_PROXY_RESPONSE_UNKNOWN', async () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
(pushDisabledInServerAck as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await canReceiveNotifications(serverUrl, PUSH_PROXY_RESPONSE_UNKNOWN, intl);
|
||||
|
||||
expect(EphemeralStore.setPushProxyVerificationState).toHaveBeenCalledWith(serverUrl, PUSH_PROXY_STATUS_UNKNOWN);
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles default case', async () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
(pushDisabledInServerAck as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
await canReceiveNotifications(serverUrl, 'some_other_response', intl);
|
||||
|
||||
expect(EphemeralStore.setPushProxyVerificationState).toHaveBeenCalledWith(serverUrl, PUSH_PROXY_STATUS_VERIFIED);
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alertPushProxyError', () => {
|
||||
test('displays alert with correct messages', () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
const alert = jest.spyOn(Alert, 'alert');
|
||||
|
||||
alertPushProxyError(intl, serverUrl);
|
||||
|
||||
expect(Alert.alert).toHaveBeenCalledWith(
|
||||
'Notifications cannot be received from this server',
|
||||
'Due to the configuration of this server, notifications cannot be received in the mobile app. Contact your system admin for more information.',
|
||||
[{
|
||||
text: 'Okay',
|
||||
onPress: expect.any(Function),
|
||||
}],
|
||||
);
|
||||
|
||||
alert?.mock.calls?.[0]?.[2]?.[0]?.onPress?.();
|
||||
expect(storePushDisabledInServerAcknowledged).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alertPushProxyUnknown', () => {
|
||||
test('displays alert with correct messages', () => {
|
||||
alertPushProxyUnknown(intl);
|
||||
|
||||
expect(Alert.alert).toHaveBeenCalledWith(
|
||||
'Notifications could not be received from this server',
|
||||
'This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.',
|
||||
[{
|
||||
text: 'Okay',
|
||||
}],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleAlertResponse', () => {
|
||||
const handleAlertResponse = async (buttonIndex: number, serverUrl?: string) => {
|
||||
if (buttonIndex === 0 && serverUrl) {
|
||||
await storePushDisabledInServerAcknowledged(urlSafeBase64Encode(serverUrl));
|
||||
}
|
||||
};
|
||||
|
||||
test('stores acknowledgment when buttonIndex is 0 and serverUrl is provided', async () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
await handleAlertResponse(0, serverUrl);
|
||||
expect(storePushDisabledInServerAcknowledged).toHaveBeenCalledWith('encoded-https://example.com');
|
||||
});
|
||||
|
||||
test('does not store acknowledgment when buttonIndex is not 0', async () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
await handleAlertResponse(1, serverUrl);
|
||||
expect(storePushDisabledInServerAcknowledged).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not store acknowledgment when serverUrl is not provided', async () => {
|
||||
await handleAlertResponse(0);
|
||||
expect(storePushDisabledInServerAcknowledged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
80
app/utils/reviews.test.ts
Normal file
80
app/utils/reviews.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Import necessary dependencies
|
||||
import {isAvailableAsync} from 'expo-store-review';
|
||||
|
||||
import * as actions from '@actions/app/global';
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Launch} from '@constants';
|
||||
import {getDontAskForReview, getLastAskedForReview} from '@queries/app/global';
|
||||
import {areAllServersSupported} from '@queries/app/servers';
|
||||
import {showReviewOverlay} from '@screens/navigation';
|
||||
|
||||
import {tryRunAppReview} from './reviews';
|
||||
|
||||
// Mocks
|
||||
jest.mock('expo-store-review', () => ({
|
||||
isAvailableAsync: jest.fn(() => Promise.resolve(true)),
|
||||
}));
|
||||
jest.mock('@queries/app/servers', () => ({
|
||||
areAllServersSupported: jest.fn(() => Promise.resolve(true)),
|
||||
}));
|
||||
jest.mock('@queries/app/global', () => ({
|
||||
getDontAskForReview: jest.fn(() => Promise.resolve(false)),
|
||||
getFirstLaunch: jest.fn().mockResolvedValueOnce(0).mockResolvedValue(Date.now()),
|
||||
getLastAskedForReview: jest.fn().mockResolvedValueOnce(0).mockResolvedValue(Date.now() - (91 * 24 * 60 * 60 * 1000)),
|
||||
}));
|
||||
jest.mock('@actions/app/global', () => ({
|
||||
storeFirstLaunch: jest.fn(),
|
||||
}));
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
showReviewOverlay: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('tryRunAppReview function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should do nothing if LocalConfig.ShowReview is false', async () => {
|
||||
LocalConfig.ShowReview = false;
|
||||
await tryRunAppReview(Launch.Normal, true);
|
||||
expect(isAvailableAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if coldStart is false', async () => {
|
||||
LocalConfig.ShowReview = true;
|
||||
await tryRunAppReview(Launch.Normal, false);
|
||||
expect(isAvailableAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if launchType is not Launch.Normal', async () => {
|
||||
LocalConfig.ShowReview = true;
|
||||
await tryRunAppReview('SomeOtherType', true);
|
||||
expect(isAvailableAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show review overlay if conditions are met', async () => {
|
||||
LocalConfig.ShowReview = true;
|
||||
const storeFirstLaunch = jest.spyOn(actions, 'storeFirstLaunch');
|
||||
await tryRunAppReview(Launch.Normal, true);
|
||||
expect(isAvailableAsync).toHaveBeenCalled();
|
||||
expect(areAllServersSupported).toHaveBeenCalled();
|
||||
expect(getDontAskForReview).toHaveBeenCalled();
|
||||
expect(getLastAskedForReview).toHaveBeenCalled();
|
||||
expect(storeFirstLaunch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show review overlay if last review was done more than TIME_TO_NEXT_REVIEW ago', async () => {
|
||||
const storeFirstLaunch = jest.spyOn(actions, 'storeFirstLaunch');
|
||||
LocalConfig.ShowReview = true;
|
||||
await tryRunAppReview(Launch.Normal, true);
|
||||
expect(isAvailableAsync).toHaveBeenCalled();
|
||||
expect(areAllServersSupported).toHaveBeenCalled();
|
||||
expect(getDontAskForReview).toHaveBeenCalled();
|
||||
expect(getLastAskedForReview).toHaveBeenCalled();
|
||||
expect(storeFirstLaunch).not.toHaveBeenCalled();
|
||||
expect(showReviewOverlay).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
72
app/utils/security.test.ts
Normal file
72
app/utils/security.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
|
||||
import {getCSRFFromCookie, urlSafeBase64Encode} from './security';
|
||||
|
||||
// Mock CookieManager
|
||||
jest.mock('@react-native-cookies/cookies', () => ({
|
||||
get: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getCSRFFromCookie function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should return MMCSRF value from cookies', async () => {
|
||||
const url = 'https://example.com';
|
||||
const mockCookies = {
|
||||
MMCSRF: {value: 'mock_CSRF_token'},
|
||||
};
|
||||
|
||||
(CookieManager.get as jest.Mock).mockResolvedValue(mockCookies);
|
||||
|
||||
const result = await getCSRFFromCookie(url);
|
||||
|
||||
expect(CookieManager.get).toHaveBeenCalledWith(url, false);
|
||||
expect(result).toEqual('mock_CSRF_token');
|
||||
});
|
||||
|
||||
it('should return undefined if MMCSRF value is not found', async () => {
|
||||
const url = 'https://example.com';
|
||||
const mockCookies = {};
|
||||
|
||||
(CookieManager.get as jest.Mock).mockResolvedValue(mockCookies);
|
||||
|
||||
const result = await getCSRFFromCookie(url);
|
||||
|
||||
expect(CookieManager.get).toHaveBeenCalledWith(url, false);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlSafeBase64Encode function', () => {
|
||||
it('should encode a string to URL-safe Base64', () => {
|
||||
const input = 'Hello, World!';
|
||||
const expectedOutput = 'SGVsbG8sIFdvcmxkIQ==';
|
||||
|
||||
const result = urlSafeBase64Encode(input);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should handle special characters in the input string', () => {
|
||||
const input = 'a+b/c=d';
|
||||
const expectedOutput = 'YStiL2M9ZA==';
|
||||
|
||||
const result = urlSafeBase64Encode(input);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const input = '';
|
||||
const expectedOutput = '';
|
||||
|
||||
const result = urlSafeBase64Encode(input);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
265
app/utils/sentry.test.ts
Normal file
265
app/utils/sentry.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Import necessary dependencies and functions
|
||||
import * as Sentry from '@sentry/react-native'; // Importing Sentry as module mock
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import Config from '@assets/config.json';
|
||||
import ClientError from '@client/rest/error';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import * as log from './log';
|
||||
import {initializeSentry, captureException, captureJSException, addSentryContext} from './sentry';
|
||||
|
||||
// Mocks
|
||||
jest.mock('@sentry/react-native', () => ({
|
||||
init: jest.fn(),
|
||||
captureException: jest.fn(),
|
||||
addBreadcrumb: jest.fn(),
|
||||
setContext: jest.fn(),
|
||||
ReactNativeTracing: jest.fn(),
|
||||
ReactNativeNavigationInstrumentation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@assets/config.json', () => ({
|
||||
SentryEnabled: true,
|
||||
SentryDsnAndroid: 'YOUR_ANDROID_DSN_HERE',
|
||||
SentryDsnIos: 'YOUR_IOS_DSN_HERE',
|
||||
SentryOptions: {
|
||||
severityLevelFilter: ['error', 'warning'],
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./log', () => ({
|
||||
logWarning: jest.fn(),
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@database/manager', () => ({
|
||||
getServerDatabaseAndOperator: jest.fn().mockReturnValue({
|
||||
database: {} as any, // Mocking database object
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/system', () => ({
|
||||
getConfig: jest.fn().mockResolvedValue({
|
||||
BuildDate: '2024-06-24',
|
||||
BuildEnterpriseReady: true,
|
||||
BuildHash: 'HASH',
|
||||
BuildHashEnterprise: 'HASH_ENTERPRISE',
|
||||
BuildNumber: '1234',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/user', () => ({
|
||||
getCurrentUser: jest.fn().mockResolvedValue({
|
||||
id: 'userId',
|
||||
locale: 'en-US',
|
||||
roles: ['user'],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/errors', () => ({
|
||||
getFullErrorMessage: jest.fn().mockReturnValue('Full error message'),
|
||||
}));
|
||||
|
||||
describe('initializeSentry function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should not initialize Sentry if SentryEnabled is false', () => {
|
||||
Config.SentryEnabled = false;
|
||||
|
||||
initializeSentry();
|
||||
|
||||
expect(Sentry.init).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log a warning if DSN is missing', () => {
|
||||
Config.SentryEnabled = true;
|
||||
Config.SentryDsnAndroid = '';
|
||||
Config.SentryDsnIos = '';
|
||||
|
||||
initializeSentry();
|
||||
|
||||
expect(log.logWarning).toHaveBeenCalledWith('Sentry is enabled, but not configured on this platform');
|
||||
});
|
||||
|
||||
it('should initialize Sentry correctly', () => {
|
||||
Config.SentryEnabled = true;
|
||||
Config.SentryDsnAndroid = 'YOUR_ANDROID_DSN_HERE';
|
||||
Config.SentryDsnIos = 'YOUR_IOS_DSN_HERE';
|
||||
|
||||
initializeSentry();
|
||||
|
||||
expect(Sentry.init).toHaveBeenCalled();
|
||||
Platform.OS = 'ios';
|
||||
expect(Sentry.init).toHaveBeenCalledWith({
|
||||
dsn: 'YOUR_IOS_DSN_HERE',
|
||||
sendDefaultPii: false,
|
||||
environment: 'beta', // Assuming isBetaApp() returns true in this test
|
||||
tracesSampleRate: 1.0,
|
||||
sampleRate: 1.0,
|
||||
attachStacktrace: true, // Adjust based on your actual logic
|
||||
enableCaptureFailedRequests: false,
|
||||
integrations: [
|
||||
expect.any(Sentry.ReactNativeTracing),
|
||||
],
|
||||
beforeSend: expect.any(Function),
|
||||
});
|
||||
|
||||
// @ts-expect-error mock not in definition
|
||||
const beforeSendFn = Sentry.init.mock.calls[0][0].beforeSend;
|
||||
const event = {level: 'error'};
|
||||
const result = beforeSendFn(event as any); // Simulate a call to beforeSend function
|
||||
|
||||
expect(result).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('captureException function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should not capture exception if Sentry is disabled', () => {
|
||||
Config.SentryEnabled = false;
|
||||
|
||||
captureException(new Error('Test error'));
|
||||
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log a warning if error is missing', () => {
|
||||
Config.SentryEnabled = true;
|
||||
|
||||
captureException(undefined);
|
||||
|
||||
expect(log.logWarning).toHaveBeenCalledWith('captureException called with missing arguments', undefined);
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should capture exception correctly', () => {
|
||||
captureException(new Error('Test error'));
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalled();
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Test error'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('captureJSException function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should not capture exception if Sentry is disabled', () => {
|
||||
Config.SentryEnabled = false;
|
||||
|
||||
captureJSException(new Error('Test error'), true);
|
||||
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log a warning if error is missing', () => {
|
||||
Config.SentryEnabled = true;
|
||||
|
||||
captureJSException(undefined, true);
|
||||
|
||||
expect(log.logWarning).toHaveBeenCalledWith('captureJSException called with missing arguments', undefined);
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should capture ClientError as breadcrumb', () => {
|
||||
const errorData: ClientErrorProps = {
|
||||
url: 'https://example.com',
|
||||
status_code: 400,
|
||||
message: 'some error from server',
|
||||
server_error_id: 'server_error_id',
|
||||
};
|
||||
const clientError = new ClientError('Client error', errorData);
|
||||
|
||||
captureJSException(clientError, true);
|
||||
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalled();
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
category: 'uncaught-app-error',
|
||||
data: {
|
||||
isFatal: 'true',
|
||||
server_error_id: 'server_error_id',
|
||||
status_code: 400,
|
||||
},
|
||||
level: 'warning',
|
||||
message: 'Full error message',
|
||||
});
|
||||
});
|
||||
|
||||
it('should capture other exceptions', () => {
|
||||
const error = new Error('Test error');
|
||||
|
||||
captureJSException(error, true);
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalled();
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSentryContext function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should not add context if Sentry is disabled', async () => {
|
||||
Config.SentryEnabled = false;
|
||||
|
||||
await addSentryContext('https://example.com');
|
||||
|
||||
expect(Sentry.setContext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add user, build, and server context', async () => {
|
||||
Config.SentryEnabled = true;
|
||||
|
||||
await addSentryContext('https://example.com');
|
||||
|
||||
expect(DatabaseManager.getServerDatabaseAndOperator).toHaveBeenCalledWith('https://example.com');
|
||||
expect(getCurrentUser).toHaveBeenCalled();
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(Sentry.setContext).toHaveBeenCalledWith('User-Information', {
|
||||
userID: 'userId',
|
||||
email: '',
|
||||
username: '',
|
||||
locale: 'en-US',
|
||||
roles: ['user'],
|
||||
});
|
||||
expect(Sentry.setContext).toHaveBeenCalledWith('App-Build Information', {
|
||||
serverBuildHash: 'HASH',
|
||||
serverBuildNumber: '1234',
|
||||
});
|
||||
expect(Sentry.setContext).toHaveBeenCalledWith('Server-Information', {
|
||||
config: {
|
||||
BuildDate: '2024-06-24',
|
||||
BuildEnterpriseReady: true,
|
||||
BuildHash: 'HASH',
|
||||
BuildHashEnterprise: 'HASH_ENTERPRISE',
|
||||
BuildNumber: '1234',
|
||||
},
|
||||
currentChannel: {},
|
||||
currentTeam: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log an error if an exception occurs', async () => {
|
||||
(DatabaseManager.getServerDatabaseAndOperator as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Database error');
|
||||
});
|
||||
|
||||
await addSentryContext('https://example.com');
|
||||
|
||||
expect(log.logError).toHaveBeenCalledWith('addSentryContext for serverUrl https://example.com', expect.any(Error));
|
||||
expect(Sentry.setContext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
29
app/utils/strings.test.ts
Normal file
29
app/utils/strings.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {nonBreakingString} from './strings';
|
||||
|
||||
describe('nonBreakingString function', () => {
|
||||
it('should replace space with non-breaking space', () => {
|
||||
// Test case 1: Replace single space with non-breaking space
|
||||
const input1 = 'Hello World';
|
||||
const expected1 = 'Hello\xa0World';
|
||||
expect(nonBreakingString(input1)).toEqual(expected1);
|
||||
|
||||
// Test case 2: Replace multiple spaces with non-breaking spaces
|
||||
const input2 = 'This is a test string';
|
||||
const expected2 = 'This\xa0is\xa0a\xa0test\xa0string';
|
||||
expect(nonBreakingString(input2)).toEqual(expected2);
|
||||
|
||||
// Test case 3: No space to replace
|
||||
const input3 = 'NoSpace';
|
||||
const expected3 = 'NoSpace';
|
||||
expect(nonBreakingString(input3)).toEqual(expected3);
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const input = '';
|
||||
const expected = '';
|
||||
expect(nonBreakingString(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -2,5 +2,5 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function nonBreakingString(s: string) {
|
||||
return s.replace(' ', '\xa0');
|
||||
return s.replace(/ /g, '\xa0');
|
||||
}
|
||||
|
||||
32
app/utils/timezone.test.ts
Normal file
32
app/utils/timezone.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTimeZone} from 'react-native-localize';
|
||||
|
||||
import {getDeviceTimezone} from './timezone';
|
||||
|
||||
// Mocking react-native-localize getTimeZone function
|
||||
jest.mock('react-native-localize', () => ({
|
||||
getTimeZone: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getDeviceTimezone function', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks(); // Clear all mock calls after each test
|
||||
});
|
||||
|
||||
it('should return the device timezone', () => {
|
||||
// Mock getTimeZone to return a specific timezone
|
||||
const mockTimeZone = 'America/New_York';
|
||||
(getTimeZone as jest.Mock).mockReturnValue(mockTimeZone);
|
||||
|
||||
// Call the function
|
||||
const result = getDeviceTimezone();
|
||||
|
||||
// Expect getTimeZone to have been called once
|
||||
expect(getTimeZone).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Expect the result to be the mocked timezone
|
||||
expect(result).toEqual(mockTimeZone);
|
||||
});
|
||||
});
|
||||
126
app/utils/typography.test.ts
Normal file
126
app/utils/typography.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {typography, type FontSizes, type FontStyles, type FontTypes} from './typography';
|
||||
|
||||
describe('Typography', () => {
|
||||
const testCases: Array<[FontTypes, FontSizes, FontStyles | undefined, string]> = [
|
||||
['Heading', 1200, 'SemiBold', 'Metropolis-SemiBold'],
|
||||
['Heading', 1000, 'Regular', 'Metropolis'],
|
||||
['Heading', 900, 'Regular', 'Metropolis'],
|
||||
['Heading', 800, 'Light', 'Metropolis-Light'],
|
||||
['Heading', 700, 'Regular', 'Metropolis'],
|
||||
['Heading', 600, 'Regular', 'Metropolis'],
|
||||
['Heading', 600, undefined, 'Metropolis-SemiBold'],
|
||||
['Body', 500, 'Regular', 'OpenSans'],
|
||||
['Body', 400, 'Regular', 'OpenSans'],
|
||||
['Body', 300, 'Light', 'OpenSans-Light'],
|
||||
['Body', 200, 'SemiBold', 'OpenSans-SemiBold'],
|
||||
['Body', 100, 'Light', 'OpenSans-Light'],
|
||||
['Body', 75, 'Regular', 'OpenSans'],
|
||||
['Body', 50, 'Regular', 'OpenSans'],
|
||||
['Body', 25, 'Light', 'OpenSans-Light'],
|
||||
['Body', 25, undefined, 'OpenSans'],
|
||||
];
|
||||
|
||||
testCases.forEach(([type, size, style, expectedFontFamily]) => {
|
||||
it(`returns correct typography for type: ${type}, size: ${size}, style: ${style}`, () => {
|
||||
const result = typography(type, size, style);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.fontFamily).toBe(expectedFontFamily);
|
||||
|
||||
switch (size) {
|
||||
case 1200:
|
||||
expect(result.fontSize).toBe(66);
|
||||
expect(result.lineHeight).toBe(48);
|
||||
expect(result.letterSpacing).toBe(-0.02);
|
||||
break;
|
||||
case 1000:
|
||||
expect(result.fontSize).toBe(40);
|
||||
expect(result.lineHeight).toBe(48);
|
||||
expect(result.letterSpacing).toBe(-0.02);
|
||||
break;
|
||||
case 900:
|
||||
expect(result.fontSize).toBe(36);
|
||||
expect(result.lineHeight).toBe(44);
|
||||
expect(result.letterSpacing).toBe(-0.02);
|
||||
break;
|
||||
case 800:
|
||||
expect(result.fontSize).toBe(32);
|
||||
expect(result.lineHeight).toBe(40);
|
||||
expect(result.letterSpacing).toBe(-0.01);
|
||||
break;
|
||||
case 700:
|
||||
expect(result.fontSize).toBe(28);
|
||||
expect(result.lineHeight).toBe(36);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 600:
|
||||
expect(result.fontSize).toBe(25);
|
||||
expect(result.lineHeight).toBe(30);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 500:
|
||||
expect(result.fontSize).toBe(22);
|
||||
expect(result.lineHeight).toBe(28);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 400:
|
||||
expect(result.fontSize).toBe(20);
|
||||
expect(result.lineHeight).toBe(28);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 300:
|
||||
expect(result.fontSize).toBe(18);
|
||||
expect(result.lineHeight).toBe(24);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 200:
|
||||
expect(result.fontSize).toBe(16);
|
||||
expect(result.lineHeight).toBe(24);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 100:
|
||||
expect(result.fontSize).toBe(14);
|
||||
expect(result.lineHeight).toBe(20);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 75:
|
||||
expect(result.fontSize).toBe(12);
|
||||
expect(result.lineHeight).toBe(16);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 50:
|
||||
expect(result.fontSize).toBe(11);
|
||||
expect(result.lineHeight).toBe(16);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
case 25:
|
||||
expect(result.fontSize).toBe(10);
|
||||
expect(result.lineHeight).toBe(16);
|
||||
expect(result.letterSpacing).toBeUndefined();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected font size: ${size}`);
|
||||
}
|
||||
|
||||
switch (style) {
|
||||
case 'SemiBold':
|
||||
expect(result.fontWeight).toBe('600');
|
||||
break;
|
||||
case 'Regular':
|
||||
expect(result.fontWeight).toBe('400');
|
||||
break;
|
||||
case 'Light':
|
||||
expect(result.fontWeight).toBe('300');
|
||||
break;
|
||||
default:
|
||||
if (type === 'Heading') {
|
||||
expect(result.fontWeight).toBe('600');
|
||||
} else {
|
||||
expect(result.fontWeight).toBe('400');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,9 +4,9 @@
|
||||
import {StyleSheet, type TextStyle} from 'react-native';
|
||||
|
||||
// type FontFamilies = 'OpenSans' | 'Metropolis';
|
||||
type FontTypes = 'Heading' | 'Body';
|
||||
type FontStyles = 'SemiBold' | 'Regular' | 'Light';
|
||||
type FontSizes = 25 | 50 | 75 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 1000 | 1200;
|
||||
export type FontTypes = 'Heading' | 'Body';
|
||||
export type FontStyles = 'SemiBold' | 'Regular' | 'Light';
|
||||
export type FontSizes = 25 | 50 | 75 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 1000 | 1200;
|
||||
|
||||
const fontFamily = StyleSheet.create({
|
||||
OpenSans: {
|
||||
|
||||
@@ -144,7 +144,7 @@ jest.doMock('react-native', () => {
|
||||
}),
|
||||
addListener: jest.fn(),
|
||||
removeListeners: jest.fn(),
|
||||
isRunningInSplitView: jest.fn().mockReturnValue(() => ({isSplit: false, isTablet: false})),
|
||||
isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}),
|
||||
|
||||
getDeliveredNotifications: jest.fn().mockResolvedValue([]),
|
||||
removeChannelNotifications: jest.fn().mockImplementation(),
|
||||
|
||||
Reference in New Issue
Block a user