575 строки
20 KiB
TypeScript
575 строки
20 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import type {UserProfile} from '@mattermost/types/users';
|
|
|
|
import {GeneralTypes} from 'mattermost-redux/action_types';
|
|
|
|
import store from 'stores/redux_store';
|
|
|
|
import * as lineBreakHelpers from 'tests/helpers/line_break_helpers';
|
|
import * as ua from 'tests/helpers/user_agent_mocks';
|
|
import Constants, {ValidationErrors, AdvancedTextEditorTextboxIds} from 'utils/constants';
|
|
import * as Utils from 'utils/utils';
|
|
|
|
describe('Utils.getDisplayNameByUser', () => {
|
|
afterEach(() => {
|
|
store.dispatch({
|
|
type: GeneralTypes.CLIENT_CONFIG_RESET,
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
const userA = {username: 'a_user', nickname: 'a_nickname', first_name: 'a_first_name', last_name: ''};
|
|
const userB = {username: 'b_user', nickname: 'b_nickname', first_name: '', last_name: 'b_last_name'};
|
|
const userC = {username: 'c_user', nickname: '', first_name: 'c_first_name', last_name: 'c_last_name'};
|
|
const userD = {username: 'd_user', nickname: 'd_nickname', first_name: 'd_first_name', last_name: 'd_last_name'};
|
|
const userE = {username: 'e_user', nickname: '', first_name: 'e_first_name', last_name: 'e_last_name'};
|
|
const userF = {username: 'f_user', nickname: 'f_nickname', first_name: 'f_first_name', last_name: 'f_last_name'};
|
|
const userG = {username: 'g_user', nickname: '', first_name: 'g_first_name', last_name: 'g_last_name'};
|
|
const userH = {username: 'h_user', nickname: 'h_nickname', first_name: '', last_name: 'h_last_name'};
|
|
const userI = {username: 'i_user', nickname: 'i_nickname', first_name: 'i_first_name', last_name: ''};
|
|
const userJ = {username: 'j_user', nickname: '', first_name: 'j_first_name', last_name: ''};
|
|
|
|
test('Show display name of user with TeammateNameDisplay set to username', () => {
|
|
store.dispatch({
|
|
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
data: {
|
|
TeammateNameDisplay: 'username',
|
|
},
|
|
});
|
|
|
|
[userA, userB, userC, userD, userE, userF, userG, userH, userI, userJ].forEach((user) => {
|
|
expect(Utils.getDisplayNameByUser(store.getState(), user as UserProfile)).toEqual(user.username);
|
|
});
|
|
});
|
|
|
|
test('Show display name of user with TeammateNameDisplay set to nickname_full_name', () => {
|
|
store.dispatch({
|
|
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
data: {
|
|
TeammateNameDisplay: 'nickname_full_name',
|
|
},
|
|
});
|
|
|
|
for (const data of [
|
|
{user: userA, result: userA.nickname},
|
|
{user: userB, result: userB.nickname},
|
|
{user: userC, result: `${userC.first_name} ${userC.last_name}`},
|
|
{user: userD, result: userD.nickname},
|
|
{user: userE, result: `${userE.first_name} ${userE.last_name}`},
|
|
{user: userF, result: userF.nickname},
|
|
{user: userG, result: `${userG.first_name} ${userG.last_name}`},
|
|
{user: userH, result: userH.nickname},
|
|
{user: userI, result: userI.nickname},
|
|
{user: userJ, result: userJ.first_name},
|
|
]) {
|
|
expect(Utils.getDisplayNameByUser(store.getState(), data.user as UserProfile)).toEqual(data.result);
|
|
}
|
|
});
|
|
|
|
test('Show display name of user with TeammateNameDisplay set to username', () => {
|
|
store.dispatch({
|
|
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
data: {
|
|
TeammateNameDisplay: 'full_name',
|
|
},
|
|
});
|
|
|
|
for (const data of [
|
|
{user: userA, result: userA.first_name},
|
|
{user: userB, result: userB.last_name},
|
|
{user: userC, result: `${userC.first_name} ${userC.last_name}`},
|
|
{user: userD, result: `${userD.first_name} ${userD.last_name}`},
|
|
{user: userE, result: `${userE.first_name} ${userE.last_name}`},
|
|
{user: userF, result: `${userF.first_name} ${userF.last_name}`},
|
|
{user: userG, result: `${userG.first_name} ${userG.last_name}`},
|
|
{user: userH, result: userH.last_name},
|
|
{user: userI, result: userI.first_name},
|
|
{user: userJ, result: userJ.first_name},
|
|
]) {
|
|
expect(Utils.getDisplayNameByUser(store.getState(), data.user as UserProfile)).toEqual(data.result);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Utils.isValidUsername', () => {
|
|
const tests = [
|
|
{
|
|
testUserName: 'sonic.the.hedgehog',
|
|
expectedError: undefined,
|
|
}, {
|
|
testUserName: 'sanic.the.speedy.errored.hedgehog@10_10-10',
|
|
expectedError: ValidationErrors.INVALID_LENGTH,
|
|
}, {
|
|
testUserName: 'sanic⭑',
|
|
expectedError: ValidationErrors.INVALID_CHARACTERS,
|
|
}, {
|
|
testUserName: '.sanic',
|
|
expectedError: ValidationErrors.INVALID_FIRST_CHARACTER,
|
|
}, {
|
|
testUserName: 'valet',
|
|
expectedError: ValidationErrors.RESERVED_NAME,
|
|
},
|
|
];
|
|
test('Validate username', () => {
|
|
for (const test of tests) {
|
|
const testError = Utils.isValidUsername(test.testUserName);
|
|
if (testError) {
|
|
expect(testError.id).toEqual(test.expectedError);
|
|
} else {
|
|
expect(testError).toBe(undefined);
|
|
}
|
|
}
|
|
});
|
|
test('Validate bot username', () => {
|
|
tests.push({
|
|
testUserName: 'sanic.the.hedgehog.',
|
|
expectedError: ValidationErrors.INVALID_LAST_CHARACTER,
|
|
});
|
|
for (const test of tests) {
|
|
const testError = Utils.isValidUsername(test.testUserName);
|
|
if (testError) {
|
|
expect(testError.id).toEqual(test.expectedError);
|
|
} else {
|
|
expect(testError).toBe(undefined);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Utils.localizeMessage', () => {
|
|
const originalGetState = store.getState;
|
|
|
|
afterAll(() => {
|
|
store.getState = originalGetState;
|
|
});
|
|
|
|
const entities = {
|
|
general: {
|
|
config: {},
|
|
},
|
|
users: {
|
|
currentUserId: 'abcd',
|
|
profiles: {
|
|
abcd: {
|
|
locale: 'fr',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
describe('with translations', () => {
|
|
beforeAll(() => {
|
|
store.getState = () => ({
|
|
entities,
|
|
views: {
|
|
i18n: {
|
|
translations: {
|
|
fr: {
|
|
'test.hello_world': 'Bonjour tout le monde!',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as any);
|
|
});
|
|
|
|
test('with translations', () => {
|
|
const messageId = 'test.hello_world';
|
|
expect(Utils.localizeMessage({id: messageId, defaultMessage: 'Hello, World!'})).toEqual('Bonjour tout le monde!');
|
|
});
|
|
|
|
test('with missing string in translations', () => {
|
|
const messageId = 'test.hello_world2';
|
|
expect(Utils.localizeMessage({id: messageId, defaultMessage: 'Hello, World 2!'})).toEqual('Hello, World 2!');
|
|
});
|
|
|
|
test('with missing string in translations and no default', () => {
|
|
const messageId = 'test.hello_world2';
|
|
expect(Utils.localizeMessage({id: messageId})).toEqual('test.hello_world2');
|
|
});
|
|
});
|
|
|
|
describe('without translations', () => {
|
|
beforeAll(() => {
|
|
store.getState = () => ({
|
|
entities,
|
|
views: {
|
|
i18n: {
|
|
translations: {},
|
|
},
|
|
},
|
|
} as any);
|
|
});
|
|
|
|
test('without translations', () => {
|
|
const messageId = 'test.hello_world';
|
|
expect(Utils.localizeMessage({id: messageId, defaultMessage: 'Hello, World!'})).toEqual('Hello, World!');
|
|
});
|
|
|
|
test('without translations and no default', () => {
|
|
const messageId = 'test.hello_world';
|
|
expect(Utils.localizeMessage({id: messageId})).toEqual('test.hello_world');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Utils.imageURLForUser', () => {
|
|
test('should return url when user id and last_picture_update is given', () => {
|
|
const imageUrl = Utils.imageURLForUser('foobar-123', 123456);
|
|
expect(imageUrl).toEqual('/api/v4/users/foobar-123/image?_=123456');
|
|
});
|
|
|
|
test('should return url when user id is given without last_picture_update', () => {
|
|
const imageUrl = Utils.imageURLForUser('foobar-123');
|
|
expect(imageUrl).toEqual('/api/v4/users/foobar-123/image?_=0');
|
|
});
|
|
});
|
|
|
|
describe('Utils.isUnhandledLineBreakKeyCombo', () => {
|
|
test('isUnhandledLineBreakKeyCombo returns true for alt + enter for Chrome UA', () => {
|
|
ua.mockChrome();
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.getAltKeyEvent())).toBe(true);
|
|
});
|
|
|
|
test('isUnhandledLineBreakKeyCombo returns false for alt + enter for Safari UA', () => {
|
|
ua.mockSafari();
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.getAltKeyEvent())).toBe(false);
|
|
});
|
|
|
|
test('isUnhandledLineBreakKeyCombo returns false for shift + enter', () => {
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.getShiftKeyEvent())).toBe(false);
|
|
});
|
|
|
|
test('isUnhandledLineBreakKeyCombo returns false for ctrl/command + enter', () => {
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.getCtrlKeyEvent())).toBe(false);
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.getMetaKeyEvent())).toBe(false);
|
|
});
|
|
|
|
test('isUnhandledLineBreakKeyCombo returns false for just enter', () => {
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(lineBreakHelpers.BASE_EVENT)).toBe(false);
|
|
});
|
|
|
|
test('isUnhandledLineBreakKeyCombo returns false for f (random key)', () => {
|
|
const e = {
|
|
...lineBreakHelpers.BASE_EVENT,
|
|
key: Constants.KeyCodes.F[0],
|
|
keyCode: Constants.KeyCodes.F[1],
|
|
};
|
|
expect(Utils.isUnhandledLineBreakKeyCombo(e)).toBe(false);
|
|
});
|
|
|
|
// restore initial user agent
|
|
afterEach(ua.reset);
|
|
});
|
|
|
|
describe('Utils.insertLineBreakFromKeyEvent', () => {
|
|
test('insertLineBreakFromKeyEvent returns with line break appending (no selection range)', () => {
|
|
expect(Utils.insertLineBreakFromKeyEvent(lineBreakHelpers.getAppendEvent())).toBe(lineBreakHelpers.OUTPUT_APPEND);
|
|
});
|
|
test('insertLineBreakFromKeyEvent returns with line break replacing (with selection range)', () => {
|
|
expect(Utils.insertLineBreakFromKeyEvent(lineBreakHelpers.getReplaceEvent())).toBe(lineBreakHelpers.OUTPUT_REPLACE);
|
|
});
|
|
});
|
|
|
|
describe('Utils.copyTextAreaToDiv', () => {
|
|
const textArea = document.createElement('textarea');
|
|
|
|
test('copyTextAreaToDiv actually creates a div element', () => {
|
|
const copy = Utils.copyTextAreaToDiv(textArea);
|
|
|
|
expect(copy!.nodeName).toEqual('DIV');
|
|
});
|
|
|
|
test('copyTextAreaToDiv copies the content into the div element', () => {
|
|
textArea.value = 'the content';
|
|
|
|
const copy = Utils.copyTextAreaToDiv(textArea);
|
|
|
|
expect(copy!.innerHTML).toEqual('the content');
|
|
});
|
|
|
|
test('copyTextAreaToDiv correctly copies the styles of the textArea element', () => {
|
|
textArea.style.fontFamily = 'Sans-serif';
|
|
|
|
const copy = Utils.copyTextAreaToDiv(textArea);
|
|
|
|
expect(copy!.style.fontFamily).toEqual('Sans-serif');
|
|
});
|
|
});
|
|
|
|
describe('Utils.getCaretXYCoordinate', () => {
|
|
const tmpCreateRange = document.createRange;
|
|
const cleanUp = () => {
|
|
document.createRange = tmpCreateRange;
|
|
};
|
|
|
|
afterAll(cleanUp);
|
|
|
|
const textArea = document.createElement('textarea');
|
|
document.createRange = () => {
|
|
const range = new Range();
|
|
|
|
range.getClientRects = () => {
|
|
return [{
|
|
top: 10,
|
|
left: 15,
|
|
}] as unknown as DOMRectList;
|
|
};
|
|
|
|
return range;
|
|
};
|
|
textArea.value = 'm'.repeat(10);
|
|
|
|
test('getCaretXYCoordinate returns the coordinates of the caret', () => {
|
|
const coordinates = Utils.getCaretXYCoordinate(textArea);
|
|
|
|
expect(coordinates.x).toEqual(15);
|
|
expect(coordinates.y).toEqual(10);
|
|
});
|
|
|
|
test('getCaretXYCoordinate returns the coordinates of the caret with a left scroll', () => {
|
|
textArea.scrollLeft = 5;
|
|
|
|
const coordinates = Utils.getCaretXYCoordinate(textArea);
|
|
|
|
expect(coordinates.x).toEqual(10);
|
|
});
|
|
});
|
|
|
|
describe('Utils.getViewportSize', () => {
|
|
test('getViewportSize returns the right viewport using default jsDom window', () => {
|
|
// the default values of the jsDom window are w: 1024, h: 768
|
|
const viewportDimensions = Utils.getViewportSize();
|
|
|
|
expect(viewportDimensions.w).toEqual(1024);
|
|
expect(viewportDimensions.h).toEqual(768);
|
|
});
|
|
|
|
test('getViewportSize returns the right viewport width with custom parameter', () => {
|
|
const mockWindow = {document: {body: {}, compatMode: undefined}};
|
|
(mockWindow.document.body as any).clientWidth = 1025;
|
|
(mockWindow.document.body as any).clientHeight = 860;
|
|
|
|
const viewportDimensions = Utils.getViewportSize(mockWindow as unknown as Window);
|
|
|
|
expect(viewportDimensions.w).toEqual(1025);
|
|
expect(viewportDimensions.h).toEqual(860);
|
|
});
|
|
|
|
test('getViewportSize returns the right viewport width with custom parameter - innerWidth', () => {
|
|
const mockWindow = {innerWidth: 1027, innerHeight: 767};
|
|
|
|
const viewportDimensions = Utils.getViewportSize(mockWindow as unknown as Window);
|
|
|
|
expect(viewportDimensions.w).toEqual(1027);
|
|
expect(viewportDimensions.h).toEqual(767);
|
|
});
|
|
});
|
|
|
|
describe('Utils.offsetTopLeft', () => {
|
|
test('offsetTopLeft returns the right offset values', () => {
|
|
const textArea = document.createElement('textArea');
|
|
|
|
textArea.getBoundingClientRect = jest.fn(() => ({
|
|
top: 967,
|
|
left: 851,
|
|
} as DOMRect));
|
|
|
|
const offsetTopLeft = Utils.offsetTopLeft(textArea);
|
|
expect(offsetTopLeft.top).toEqual(967);
|
|
expect(offsetTopLeft.left).toEqual(851);
|
|
});
|
|
});
|
|
|
|
describe('Utils.getSuggestionBoxAlgn', () => {
|
|
const tmpCreateRange = document.createRange;
|
|
const cleanUp = () => {
|
|
document.createRange = tmpCreateRange;
|
|
};
|
|
|
|
afterAll(cleanUp);
|
|
|
|
const textArea: HTMLTextAreaElement = document.createElement('textArea') as HTMLTextAreaElement;
|
|
|
|
textArea.value = 'a'.repeat(30);
|
|
|
|
jest.spyOn(textArea, 'offsetWidth', 'get').
|
|
mockImplementation(() => 950);
|
|
|
|
textArea.getBoundingClientRect = jest.fn(() => ({
|
|
left: 50,
|
|
} as DOMRect));
|
|
|
|
const createRange = (size: number) => {
|
|
document.createRange = () => {
|
|
const range = new Range();
|
|
range.getClientRects = () => {
|
|
return [{
|
|
top: 100,
|
|
left: size,
|
|
}] as unknown as DOMRectList;
|
|
};
|
|
return range;
|
|
};
|
|
};
|
|
|
|
const fixedToTheRight = textArea.offsetWidth - Constants.SUGGESTION_LIST_MODAL_WIDTH;
|
|
|
|
test('getSuggestionBoxAlgn returns 0 (box stuck to left) when the length of the text is small', () => {
|
|
const smallSizeText = 15;
|
|
createRange(smallSizeText);
|
|
const suggestionBoxAlgn = Utils.getSuggestionBoxAlgn(textArea, Utils.getPxToSubstract());
|
|
expect(suggestionBoxAlgn.pixelsToMoveX).toEqual(0);
|
|
});
|
|
|
|
test('getSuggestionBoxAlgn returns pixels to move when text is medium size', () => {
|
|
const mediumSizeText = 155;
|
|
createRange(mediumSizeText);
|
|
const suggestionBoxAlgn = Utils.getSuggestionBoxAlgn(textArea, Utils.getPxToSubstract());
|
|
expect(suggestionBoxAlgn.pixelsToMoveX).toBeGreaterThan(0);
|
|
expect(suggestionBoxAlgn.pixelsToMoveX).not.toBe(fixedToTheRight);
|
|
});
|
|
|
|
test('getSuggestionBoxAlgn align box to the righ when text is large size', () => {
|
|
const largeSizeText = 700;
|
|
createRange(largeSizeText);
|
|
const suggestionBoxAlgn = Utils.getSuggestionBoxAlgn(textArea, Utils.getPxToSubstract());
|
|
expect(fixedToTheRight).toEqual(suggestionBoxAlgn.pixelsToMoveX);
|
|
});
|
|
});
|
|
|
|
describe('Utils.numberToFixedDynamic', () => {
|
|
const tests = [
|
|
{
|
|
label: 'Removes period when no decimals needed',
|
|
num: 123.001,
|
|
places: 2,
|
|
expected: '123',
|
|
},
|
|
{
|
|
label: 'Extra places are ignored',
|
|
num: 123.45,
|
|
places: 3,
|
|
expected: '123.45',
|
|
},
|
|
{
|
|
label: 'rounds positives',
|
|
num: 123.45,
|
|
places: 1,
|
|
expected: '123.5',
|
|
},
|
|
{
|
|
label: 'rounds negatives',
|
|
num: -123.45,
|
|
places: 1,
|
|
expected: '-123.5',
|
|
},
|
|
{
|
|
label: 'negative places interpreted as 0 places',
|
|
num: 123,
|
|
places: -1,
|
|
expected: '123',
|
|
},
|
|
{
|
|
label: 'handles integers',
|
|
num: 123,
|
|
places: 4,
|
|
expected: '123',
|
|
},
|
|
{
|
|
label: 'handles integers with 0 places',
|
|
num: 123,
|
|
places: 4,
|
|
expected: '123',
|
|
},
|
|
{
|
|
label: 'correctly excludes decimal when rounding exlcudes number',
|
|
num: 0.004,
|
|
places: 2,
|
|
expected: '0',
|
|
},
|
|
];
|
|
tests.forEach((testCase) => {
|
|
test(testCase.label, () => {
|
|
const actual = Utils.numberToFixedDynamic(testCase.num, testCase.places);
|
|
expect(actual).toBe(testCase.expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('isTextSelectedInPostOrReply', () => {
|
|
function createKeyboardEvent(target: Partial<HTMLTextAreaElement>) {
|
|
return {
|
|
target: {
|
|
selectionStart: 0,
|
|
selectionEnd: 0,
|
|
id: AdvancedTextEditorTextboxIds.Default,
|
|
...target,
|
|
},
|
|
} as unknown as KeyboardEvent;
|
|
}
|
|
|
|
test('returns false when not typing in a textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: 'not_a_textbox',
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(false);
|
|
});
|
|
|
|
test('returns false when no text is selected in center textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InCenter,
|
|
selectionStart: 5,
|
|
selectionEnd: 5,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(false);
|
|
});
|
|
|
|
test('returns true when text is selected in center textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InCenter,
|
|
selectionStart: 0,
|
|
selectionEnd: 5,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(true);
|
|
});
|
|
|
|
test('returns false when no text is selected in RHS comment textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InRHSComment,
|
|
selectionStart: 3,
|
|
selectionEnd: 3,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(false);
|
|
});
|
|
|
|
test('returns true when text is selected in RHS comment textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InRHSComment,
|
|
selectionStart: 0,
|
|
selectionEnd: 3,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(true);
|
|
});
|
|
|
|
test('returns false when no text is selected in edit mode textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InEditMode,
|
|
selectionStart: 7,
|
|
selectionEnd: 7,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(false);
|
|
});
|
|
|
|
test('returns true when text is selected in edit mode textbox', () => {
|
|
const event = createKeyboardEvent({
|
|
id: AdvancedTextEditorTextboxIds.InEditMode,
|
|
selectionStart: 0,
|
|
selectionEnd: 7,
|
|
});
|
|
expect(Utils.isTextSelectedInPostOrReply(event)).toBe(true);
|
|
});
|
|
});
|