[MM-47181] Migrate "components/suggestion/at_mention_provider" and associated tests to Typescript (#25342)

This commit is contained in:
Sudhanva-Nadiger 2024-01-25 10:58:09 +05:30 committed by GitHub
parent 8f0abc1316
commit 53298a2d60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 306 additions and 141 deletions

View File

@ -8,7 +8,7 @@ exports[`components/TextBox should match snapshot with additional, optional prop
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
>
<Connect(PostMarkdown)
@ -52,10 +52,10 @@ exports[`components/TextBox should match snapshot with additional, optional prop
listPosition="top"
onBlur={[Function]}
onChange={[Function]}
onComposition={[Function]}
onHeightChange={[Function]}
onComposition={[MockFunction]}
onHeightChange={[MockFunction]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
openWhenEmpty={true}
@ -66,10 +66,34 @@ exports[`components/TextBox should match snapshot with additional, optional prop
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
@ -136,7 +160,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
>
<Connect(PostMarkdown)
@ -179,7 +203,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
@ -189,10 +213,34 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
@ -259,7 +307,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
>
<Connect(PostMarkdown)
@ -302,7 +350,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
@ -312,10 +360,34 @@ exports[`components/TextBox should throw error when new property is too long 1`]
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],
@ -382,7 +454,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
tabIndex={0}
>
<Connect(PostMarkdown)
@ -425,7 +497,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyPress={[MockFunction]}
onKeyUp={[Function]}
onMouseUp={[Function]}
placeholder="placeholder text"
@ -435,10 +507,34 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
"addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
},
],
"autocompleteUsersInChannel": [Function],

View File

@ -1,35 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx';
import AtMentionProvider, {type Props} from 'components/suggestion/at_mention_provider/at_mention_provider';
import AtMentionSuggestion from 'components/suggestion/at_mention_provider/at_mention_suggestion';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
jest.useFakeTimers();
describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
const userid10 = {id: 'userid10', username: 'nicknamer', first_name: '', last_name: '', nickname: 'Z'};
const userid3 = {id: 'userid3', username: 'other', first_name: 'X', last_name: 'Y', nickname: 'Z'};
const userid1 = {id: 'userid1', username: 'user', first_name: 'a', last_name: 'b', nickname: 'c', isCurrentUser: true};
const userid2 = {id: 'userid2', username: 'user2', first_name: 'd', last_name: 'e', nickname: 'f'};
const userid4 = {id: 'userid4', username: 'user4', first_name: 'X', last_name: 'Y', nickname: 'Z'};
const userid5 = {id: 'userid5', username: 'user5', first_name: 'out', last_name: 'out', nickname: 'out'};
const userid6 = {id: 'userid6', username: 'user6.six-split', first_name: 'out Junior', last_name: 'out', nickname: 'out'};
const userid7 = {id: 'userid7', username: 'xuser7', first_name: '', last_name: '', nickname: 'x'};
const userid8 = {id: 'userid8', username: 'xuser8', first_name: 'Robert', last_name: 'Ward', nickname: 'nickname'};
const userid10 = TestHelper.getUserMock({id: 'userid10', username: 'nicknamer', first_name: '', last_name: '', nickname: 'Z'});
const userid3 = TestHelper.getUserMock({id: 'userid3', username: 'other', first_name: 'X', last_name: 'Y', nickname: 'Z'});
const userid1 = {...TestHelper.getUserMock({id: 'userid1', username: 'user', first_name: 'a', last_name: 'b', nickname: 'c'}), isCurrentUser: true};
const userid2 = TestHelper.getUserMock({id: 'userid2', username: 'user2', first_name: 'd', last_name: 'e', nickname: 'f'});
const userid4 = TestHelper.getUserMock({id: 'userid4', username: 'user4', first_name: 'X', last_name: 'Y', nickname: 'Z'});
const userid5 = TestHelper.getUserMock({id: 'userid5', username: 'user5', first_name: 'out', last_name: 'out', nickname: 'out'});
const userid6 = TestHelper.getUserMock({id: 'userid6', username: 'user6.six-split', first_name: 'out Junior', last_name: 'out', nickname: 'out'});
const userid7 = TestHelper.getUserMock({id: 'userid7', username: 'xuser7', first_name: '', last_name: '', nickname: 'x'});
const userid8 = TestHelper.getUserMock({id: 'userid8', username: 'xuser8', first_name: 'Robert', last_name: 'Ward', nickname: 'nickname'});
const groupid1 = {id: 'groupid1', name: 'board', display_name: 'board'};
const groupid2 = {id: 'groupid2', name: 'developers', display_name: 'developers'};
const groupid3 = {id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'};
const groupid1 = TestHelper.getGroupMock({id: 'groupid1', name: 'board', display_name: 'board'});
const groupid2 = TestHelper.getGroupMock({id: 'groupid2', name: 'developers', display_name: 'developers'});
const groupid3 = TestHelper.getGroupMock({id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'});
const baseParams = {
const baseParams: Props = {
currentUserId: 'userid1',
channelId: 'channelid1',
autocompleteUsersInChannel: jest.fn().mockResolvedValue(false),
autocompleteGroups: [groupid1, groupid2, groupid3],
useChannelMentions: true,
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue(false),
priorityProfiles: [],
};
it('should ignore pretexts that are not at-mentions', () => {
@ -169,8 +171,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
});
it('should have priorityProfiles at the top', async () => {
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
const userid11 = TestHelper.getUserMock({id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'});
const userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@';
const matchedPretext = '@';
@ -315,8 +317,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
});
it('should remove duplicates from results', async () => {
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
const userid11 = TestHelper.getUserMock({id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'});
const userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@';
const matchedPretext = '@';
@ -461,8 +463,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
});
it('should sort results based on last_viewed_at', async () => {
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
const userid11 = TestHelper.getUserMock({id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'});
const userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@';
const matchedPretext = '@';

View File

@ -1,14 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Group} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import type {Filters} from 'mattermost-redux/selectors/entities/users';
import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {makeAddLastViewAtToProfiles} from 'mattermost-redux/selectors/entities/utils';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {getSuggestionsSplitBy, getSuggestionsSplitByMultiple} from 'mattermost-redux/utils/user_utils';
import store from 'stores/redux_store';
import {Constants} from 'utils/constants';
import type {GlobalState} from 'types/store';
import AtMentionSuggestion from './at_mention_suggestion';
import Provider from '../provider';
@ -16,14 +23,65 @@ import Provider from '../provider';
const profilesInChannelOptions = {active: true};
const regexForAtMention = /(?:^|\W)@([\p{L}\d\-_. ]*)$/iu;
type UserProfileWithLastViewAt = UserProfile & {last_viewed_at?: number};
type CreatedProfile = UserProfile & {
type: string;
isCurrentUser?: boolean;
last_viewed_at?: number;
};
type CreatedGroup = Group & {type: string};
type Results = {
matchedPretext: string;
terms: string[];
items: any;
component: React.ElementType;
};
type ResultsCallback = (results: Results) => void;
export type Props = {
currentUserId: string;
channelId: string;
autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
useChannelMentions: boolean;
autocompleteGroups: Group[] | null;
searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
priorityProfiles: UserProfile[] | undefined;
}
// The AtMentionProvider provides matches for at mentions, including @here, @channel, @all,
// users in the channel and users not in the channel. It mixes together results from the local
// store with results fetched from the server.
export default class AtMentionProvider extends Provider {
constructor(props) {
public currentUserId: string;
public channelId: string;
public autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
public useChannelMentions: boolean;
public autocompleteGroups: Group[] | null;
public searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
public priorityProfiles: UserProfile[] | undefined;
public data: any;
public lastCompletedWord: string;
public lastPrefixWithNoResults: string;
public getProfilesInChannel: (state: GlobalState, channelId: string, filters?: Filters | undefined) => UserProfile[];
public addLastViewAtToProfiles: (state: GlobalState, profiles: UserProfile[]) => UserProfileWithLastViewAt[];
constructor(props: Props) {
super();
this.setProps(props);
const {currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles} = props;
this.currentUserId = currentUserId;
this.channelId = channelId;
this.autocompleteUsersInChannel = autocompleteUsersInChannel;
this.useChannelMentions = useChannelMentions;
this.autocompleteGroups = autocompleteGroups;
this.searchAssociatedGroupsForReference = searchAssociatedGroupsForReference;
this.priorityProfiles = priorityProfiles;
this.data = null;
this.lastCompletedWord = '';
@ -33,9 +91,7 @@ export default class AtMentionProvider extends Provider {
this.addLastViewAtToProfiles = makeAddLastViewAtToProfiles();
}
// setProps gives the provider additional context for matching pretexts. Ideally this would
// just be something akin to a connected component with access to the store itself.
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}) {
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}: Props) {
this.currentUserId = currentUserId;
this.channelId = channelId;
this.autocompleteUsersInChannel = autocompleteUsersInChannel;
@ -61,8 +117,8 @@ export default class AtMentionProvider extends Provider {
// retrieves the parts of the profile that should be checked
// against the term
getProfileSuggestions(profile) {
const profileSuggestions = [];
getProfileSuggestions(profile: UserProfile) {
const profileSuggestions: string[] = [];
if (!profile) {
return profileSuggestions;
}
@ -82,8 +138,8 @@ export default class AtMentionProvider extends Provider {
// retrieves the parts of the group mention that should be checked
// against the term
getGroupSuggestions(group) {
const groupSuggestions = [];
getGroupSuggestions(group: Group) {
const groupSuggestions: string[] = [];
if (!group) {
return groupSuggestions;
}
@ -101,12 +157,12 @@ export default class AtMentionProvider extends Provider {
}
// normalizeString performs a unicode normalization to a string
normalizeString(name) {
normalizeString(name: string) {
return name.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
// filterProfile constrains profiles to those matching the latest prefix.
filterProfile(profile) {
filterProfile(profile: UserProfile | UserProfileWithLastViewAt) {
if (!profile) {
return false;
}
@ -119,7 +175,7 @@ export default class AtMentionProvider extends Provider {
}
// filterGroup constrains group mentions to those matching the latest prefix.
filterGroup(group) {
filterGroup(group: Group) {
if (!group) {
return false;
}
@ -182,8 +238,8 @@ export default class AtMentionProvider extends Provider {
}
const remoteMembers = (this.data.users || []).
filter((profile) => this.filterProfile(profile)).
map((profile) => this.createFromProfile(profile, Constants.MENTION_MEMBERS));
filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
map((profile: UserProfileWithLastViewAt) => this.createFromProfile(profile, Constants.MENTION_MEMBERS));
return remoteMembers;
}
@ -193,30 +249,30 @@ export default class AtMentionProvider extends Provider {
if (!this.data) {
return [];
}
const remoteGroups = (this.data.groups || []).
filter((group) => this.filterGroup(group)).
map((group) => this.createFromGroup(group, Constants.MENTION_GROUPS));
const remoteGroups = ((this.data.groups || []) as Group[]).
filter((group: Group) => this.filterGroup(group)).
map((group: Group) => this.createFromGroup(group, Constants.MENTION_GROUPS));
return remoteGroups;
}
// remoteNonMembers matches users listed as not in the channel by the server.
// listed in the channel from local results.
remoteNonMembers() {
remoteNonMembers(): CreatedProfile[] {
if (!this.data) {
return [];
}
return (this.data.out_of_channel || []).
filter((profile) => this.filterProfile(profile)).
map((profile) => ({
filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
map((profile: UserProfileWithLastViewAt) => ({
type: Constants.MENTION_NONMEMBERS,
...profile,
}));
}
items() {
const priorityProfilesIds = {};
const priorityProfilesIds: Record<string, boolean> = {};
const priorityProfiles = this.filterPriorityProfiles();
priorityProfiles.forEach((member) => {
@ -226,16 +282,16 @@ export default class AtMentionProvider extends Provider {
const specialMentions = this.specialMentions();
const localMembers = this.localMembers().filter((member) => !priorityProfilesIds[member.id]);
const localUserIds = {};
const localUserIds: Record<string, boolean> = {};
localMembers.forEach((member) => {
localUserIds[member.id] = true;
});
const remoteMembers = this.remoteMembers().filter((member) => !localUserIds[member.id] && !priorityProfilesIds[member.id]);
const remoteMembers = this.remoteMembers().filter((member: CreatedProfile) => !localUserIds[member.id] && !priorityProfilesIds[member.id]);
// comparator which prioritises users with usernames starting with search term
const orderUsers = (a, b) => {
const orderUsers = (a: CreatedProfile, b: CreatedProfile) => {
const aStartsWith = a.username.startsWith(this.latestPrefix);
const bStartsWith = b.username.startsWith(this.latestPrefix);
@ -263,7 +319,7 @@ export default class AtMentionProvider extends Provider {
// handle groups
const localGroups = this.localGroups();
const localGroupIds = {};
const localGroupIds: Record<string, boolean> = {};
localGroups.forEach((group) => {
localGroupIds[group.id] = true;
});
@ -271,7 +327,7 @@ export default class AtMentionProvider extends Provider {
const remoteGroups = this.remoteGroups().filter((group) => !localGroupIds[group.id]);
// comparator which prioritises users with usernames starting with search term
const orderGroups = (a, b) => {
const orderGroups = (a: CreatedGroup, b: CreatedGroup) => {
const aStartsWith = a.name.startsWith(this.latestPrefix);
const bStartsWith = b.name.startsWith(this.latestPrefix);
@ -294,11 +350,11 @@ export default class AtMentionProvider extends Provider {
filter((member) => !localUserIds[member.id]).
sort(orderUsers);
return priorityProfiles.concat(localAndRemoteMembers).concat(localAndRemoteGroups).concat(specialMentions).concat(remoteNonMembers);
return [...priorityProfiles, ...localAndRemoteMembers, ...localAndRemoteGroups, ...specialMentions, ...remoteNonMembers];
}
// updateMatches invokes the resultCallback with the metadata for rendering at mentions
updateMatches(resultCallback, items) {
updateMatches(resultCallback: ResultsCallback, items: any[]) {
if (items.length === 0) {
this.lastPrefixWithNoResults = this.latestPrefix;
} else if (this.lastPrefixWithNoResults === this.latestPrefix) {
@ -321,7 +377,7 @@ export default class AtMentionProvider extends Provider {
});
}
handlePretextChanged(pretext, resultCallback) {
handlePretextChanged(pretext: string, resultCallback: ResultsCallback) {
const captured = regexForAtMention.exec(pretext.toLowerCase());
if (!captured) {
return false;
@ -342,15 +398,15 @@ export default class AtMentionProvider extends Provider {
this.updateMatches(resultCallback, this.items());
// If we haven't gotten server-side results in 500 ms, add the loading indicator.
let showLoadingIndicator = setTimeout(() => {
let showLoadingIndicator: NodeJS.Timeout | null = setTimeout(() => {
if (this.shouldCancelDispatch(prefix)) {
return;
}
this.updateMatches(resultCallback, this.items().concat([{
this.updateMatches(resultCallback, [...this.items(), ...[{
type: Constants.MENTION_MORE_MEMBERS,
loading: true,
}]));
}]]);
showLoadingIndicator = null;
}, 500);
@ -375,12 +431,12 @@ export default class AtMentionProvider extends Provider {
return true;
}
handleCompleteWord(term) {
handleCompleteWord(term: string) {
this.lastCompletedWord = term;
this.lastPrefixWithNoResults = '';
}
createFromProfile(profile, type) {
createFromProfile(profile: UserProfile | UserProfileWithLastViewAt, type: string): CreatedProfile {
if (profile.id === this.currentUserId) {
return {
type,
@ -395,7 +451,7 @@ export default class AtMentionProvider extends Provider {
};
}
createFromGroup(group, type) {
createFromGroup(group: Group, type: string): CreatedGroup {
return {
type,
...group,

View File

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AtMentionProvider from './at_mention_provider.jsx';
import AtMentionProvider from './at_mention_provider';
export default AtMentionProvider;

View File

@ -4,7 +4,7 @@
import {shallow, mount} from 'enzyme';
import React from 'react';
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx';
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider';
import CommandProvider from 'components/suggestion/command_provider/command_provider';
import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box';
import SuggestionList from 'components/suggestion/suggestion_list';

View File

@ -4,22 +4,20 @@
import {shallow} from 'enzyme';
import React from 'react';
import Textbox from 'components/textbox/textbox';
import Textbox, {type Props} from 'components/textbox/textbox';
import {TestHelper} from 'utils/test_helper';
describe('components/TextBox', () => {
const baseProps = {
const baseProps: Props = {
channelId: 'channelId',
rootId: 'rootId',
currentUserId: 'currentUserId',
currentTeamId: 'currentTeamId',
profilesInChannel: [
{id: 'id1'},
{id: 'id2'},
],
delayChannelAutocomplete: false,
autocompleteGroups: [
{id: 'gid1'},
{id: 'gid2'},
TestHelper.getGroupMock({id: 'gid1'}),
TestHelper.getGroupMock({id: 'gid2'}),
],
actions: {
autocompleteUsersInChannel: jest.fn(),
@ -28,79 +26,89 @@ describe('components/TextBox', () => {
},
useChannelMentions: true,
tabIndex: 0,
id: '',
value: '',
onChange: jest.fn(),
onKeyPress: jest.fn(),
createMessage: '',
characterLimit: 0,
};
test('should match snapshot with required props', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const emptyFunction = jest.fn();
const props = {
...baseProps,
id: 'someid',
value: 'some test text',
onChange: emptyFunction,
onKeyPress: emptyFunction,
characterLimit: 400,
createMessage: 'placeholder text',
supportsCommands: false,
};
const wrapper = shallow(
<Textbox
id='someid'
value='some test text'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={4000}
createMessage='placeholder text'
supportsCommands={false}
{...baseProps}
/>,
<Textbox {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with additional, optional props', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const emptyFunction = jest.fn();
const props = {
...baseProps,
id: 'someid',
value: 'some test text',
onChange: emptyFunction,
onKeyPress: emptyFunction,
characterLimit: 4000,
createMessage: 'placeholder text',
supportsCommands: false,
rootId: 'root_id',
onComposition: jest.fn().mockReturnValue({}),
onHeightChange: jest.fn().mockReturnValue({}),
onKeyDown: jest.fn().mockReturnValue({}),
onMouseUp: jest.fn().mockReturnValue({}),
onKeyUp: jest.fn().mockReturnValue({}),
onBlur: jest.fn().mockReturnValue({}),
handlePostError: jest.fn().mockReturnValue({}),
suggestionListPosition: 'top' as const,
emojiEnabled: true,
disabled: true,
badConnection: true,
preview: true,
openWhenEmpty: true,
};
const wrapper = shallow(
<Textbox
id='someid'
value='some test text'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={4000}
createMessage='placeholder text'
supportsCommands={false}
{...baseProps}
rootId='root_id'
onComposition={() => {}}
onHeightChange={() => {}}
onKeyDown={() => {}}
onMouseUp={() => {}}
onKeyUp={() => {}}
onBlur={() => {}}
handlePostError={() => {}}
suggestionListPosition='top'
emojiEnabled={true}
disabled={true}
badConnection={true}
preview={true}
openWhenEmpty={true}
/>,
<Textbox {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should throw error when value is too long', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const emptyFunction = jest.fn();
// this mock function should be called when the textbox value is too long
let gotError = false;
function handlePostError(msg: React.ReactNode) {
const handlePostError = jest.fn((msg: React.ReactNode) => {
gotError = msg !== null;
}
});
const props = {
...baseProps,
id: 'someid',
value: 'some test text that exceeds char limit',
onChange: emptyFunction,
onKeyPress: emptyFunction,
characterLimit: 14,
createMessage: 'placeholder text',
supportsCommands: false,
handlePostError,
};
const wrapper = shallow(
<Textbox
id='someid'
value='some test text that exceeds char limit'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={14}
createMessage='placeholder text'
supportsCommands={false}
handlePostError={handlePostError}
{...baseProps}
/>,
<Textbox {...props}/>,
);
expect(gotError).toEqual(true);
@ -108,26 +116,28 @@ describe('components/TextBox', () => {
});
test('should throw error when new property is too long', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const emptyFunction = jest.fn();
// this mock function should be called when the textbox value is too long
let gotError = false;
function handlePostError(msg: React.ReactNode) {
const handlePostError = jest.fn((msg: React.ReactNode) => {
gotError = msg !== null;
}
});
const props = {
...baseProps,
id: 'someid',
value: 'some test text',
onChange: emptyFunction,
onKeyPress: emptyFunction,
characterLimit: 14,
createMessage: 'placeholder text',
supportsCommands: false,
handlePostError,
};
const wrapper = shallow(
<Textbox
id='someid'
value='some test text'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={14}
createMessage='placeholder text'
supportsCommands={false}
handlePostError={handlePostError}
{...baseProps}
/>,
<Textbox {...props}/>,
);
wrapper.setProps({value: 'some test text that exceeds char limit'});

View File

@ -7,6 +7,7 @@ import type {ChangeEvent, ElementType, FocusEvent, KeyboardEvent, MouseEvent} fr
import {FormattedMessage} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import type {Group} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
@ -59,7 +60,7 @@ export type Props = {
currentUserId: string;
currentTeamId: string;
preview?: boolean;
autocompleteGroups: Array<{ id: string }> | null;
autocompleteGroups: Group[] | null;
delayChannelAutocomplete: boolean;
actions: {
autocompleteUsersInChannel: (prefix: string, channelId: string) => Promise<ActionResult>;

View File

@ -41,7 +41,7 @@ import {
export {getCurrentUser, getCurrentUserId, getUsers};
type Filters = {
export type Filters = {
role?: string;
inactive?: boolean;
active?: boolean;