mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 02:40:15 +00:00
* Expo-Router partial migration * Full migration to ExpoRouter * i18n * add tests * Merge branch 'main' into expo-router * align package-lock.json * i18n * feedback review * feedback review * android 16kb patch * Refactored emoji picker keyboard using a state machine * set android launch_screen in drawables instead of layout * no conditional hooks * remove duplicate call in test * address feedback * fix tests and address more feedback * small corrections * small corrections * small corrections * fix draft test * fix draft test 2 * remove navigateToScreen in draff test * fix nit in draft test * remove doc file * Feedback review * fix snapshots * improve test coverage * fix i18n * Upgrade t o RN 0.83, other dependencies and new architecture (#9737) * library hardware-keyboard android new arch support fix * library rnshare android new arch support fix * library rnutils android new arch support fix * library secure-pdf-viewer add new arch support * Enable new arch on iOS and Android * update emm and network client with new-arch * update intune to use new-arch * performance improvements * fix tests * fixes post input on new-arch * fix android emoji picker * remove doc * run tests in parallel * upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture Bumps core dependencies to their latest versions: - React Native 0.77.3 → 0.83.9 (New Architecture enabled) - React 18.3.1 → 19.2.5 - Expo SDK 52 → 55 - TypeScript 5.8.3 → 6.0.3 - Node.js 22.16.0 → 24.15.0 - react-native-reanimated 3.17.3 → 4.3.0 - Various other dependency upgrades Migrates iOS AppDelegate from Objective-C to Swift, updates Android build tooling and Gradle wrapper, refreshes all patches and font assets, and adapts app code, tests, and CI workflows throughout for API changes introduced by these upgrades. * remove unecessary dismissKeyboard in notifications * feedback review * update intune to use new-arch * update @mattermost deps * missing i18n string * additional fixes * update fastlane
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {type ComponentProps} from 'react';
|
|
|
|
import SelectedChip from '@components/chips/selected_chip';
|
|
import SelectedUserChip from '@components/chips/selected_user_chip';
|
|
import FloatingTextInput from '@components/floating_input/floating_text_input_label';
|
|
import OptionItem from '@components/option_item';
|
|
import UserItem from '@components/user_item';
|
|
import {Screens} from '@constants';
|
|
import {navigateToSettingsScreen} from '@screens/navigation';
|
|
import {fireEvent, renderWithIntl} from '@test/intl-test-helper';
|
|
import TestHelper from '@test/test_helper';
|
|
|
|
import Selection from './selection';
|
|
import SelectionSearchBar from './selection_search_bar';
|
|
import SelectionTeamBar from './selection_team_bar';
|
|
import TextItem from './text_item';
|
|
import {TextItemType} from './types';
|
|
|
|
jest.mock('./selection_search_bar');
|
|
jest.mocked(SelectionSearchBar).mockImplementation(
|
|
(props) => React.createElement('SelectionSearchBar', {testID: 'selection-search-bar', ...props}),
|
|
);
|
|
|
|
jest.mock('./selection_team_bar');
|
|
jest.mocked(SelectionTeamBar).mockImplementation(
|
|
(props) => React.createElement('SelectionTeamBar', {testID: 'selection-team-bar', ...props}),
|
|
);
|
|
|
|
jest.mock('./text_item');
|
|
jest.mocked(TextItem).mockImplementation(
|
|
(props) => React.createElement('TextItem', {...props}),
|
|
);
|
|
|
|
jest.mock('@components/chips/selected_chip');
|
|
jest.mocked(SelectedChip).mockImplementation(
|
|
(props) => React.createElement('SelectedChip', {testID: 'selected-chip', ...props}),
|
|
);
|
|
|
|
jest.mock('@components/chips/selected_user_chip');
|
|
jest.mocked(SelectedUserChip).mockImplementation(
|
|
(props) => React.createElement('SelectedUserChip', {testID: 'selected-user-chip', ...props}),
|
|
);
|
|
|
|
jest.mock('@components/user_item');
|
|
jest.mocked(UserItem).mockImplementation(
|
|
(props: ComponentProps<typeof UserItem>) => React.createElement('UserItem', {testID: 'user-item', ...props}),
|
|
);
|
|
|
|
jest.mock('@components/option_item');
|
|
jest.mocked(OptionItem).mockImplementation(
|
|
(props) => React.createElement('OptionItem', {testID: 'option-item', ...props}),
|
|
);
|
|
|
|
jest.mock('@components/floating_input/floating_text_input_label', () => ({
|
|
__esModule: true,
|
|
default: jest.fn(),
|
|
}));
|
|
jest.mocked(FloatingTextInput).mockImplementation(
|
|
(props) => React.createElement('FloatingTextInput', {testID: 'floating-text-input', ...props}),
|
|
);
|
|
|
|
jest.mock('@screens/navigation', () => ({
|
|
navigateToSettingsScreen: jest.fn(),
|
|
}));
|
|
|
|
describe('Selection', () => {
|
|
const mockOnSearchChange = jest.fn();
|
|
const mockOnSelectItem = jest.fn();
|
|
const mockOnRemoveItem = jest.fn();
|
|
const mockOnClose = jest.fn().mockResolvedValue(undefined);
|
|
const mockOnSendOptionsChange = jest.fn();
|
|
|
|
function getBaseProps(): ComponentProps<typeof Selection> {
|
|
return {
|
|
teamId: 'team-1',
|
|
teamDisplayName: 'Test Team',
|
|
teamLastIconUpdate: 1234567890,
|
|
teamInviteId: 'invite-id-1',
|
|
teammateNameDisplay: 'username',
|
|
serverUrl: 'https://test.server.com',
|
|
term: '',
|
|
searchResults: [],
|
|
selectedIds: {},
|
|
keyboardOverlap: 0,
|
|
wrapperHeight: 800,
|
|
loading: false,
|
|
testID: 'invite.selection',
|
|
sendOptions: {
|
|
inviteAsGuest: false,
|
|
includeCustomMessage: false,
|
|
customMessage: '',
|
|
selectedChannels: [],
|
|
guestMagicLink: false,
|
|
},
|
|
onSendOptionsChange: mockOnSendOptionsChange,
|
|
onSearchChange: mockOnSearchChange,
|
|
onSelectItem: mockOnSelectItem,
|
|
onRemoveItem: mockOnRemoveItem,
|
|
onClose: mockOnClose,
|
|
canInviteGuests: true,
|
|
allowGuestMagicLink: true,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('renders correctly', () => {
|
|
const props = getBaseProps();
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
expect(getByTestId('selection-search-bar')).toBeTruthy();
|
|
expect(getByTestId('selection-team-bar')).toBeTruthy();
|
|
});
|
|
|
|
it('renders selected items correctly', () => {
|
|
const props = getBaseProps();
|
|
const user = TestHelper.fakeUser({id: 'user-1', username: 'user1'});
|
|
props.selectedIds = {
|
|
'user-1': user,
|
|
'email-1': 'test@example.com',
|
|
};
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
expect(getByTestId('invite.selected_items')).toBeTruthy();
|
|
const userChip = getByTestId('invite.selected_item');
|
|
expect(userChip).toHaveProp('user', user);
|
|
expect(userChip).toHaveProp('teammateNameDisplay', 'username');
|
|
userChip.props.onPress('user-1');
|
|
expect(mockOnRemoveItem).toHaveBeenCalledTimes(1);
|
|
expect(mockOnRemoveItem).toHaveBeenCalledWith('user-1');
|
|
const chip = getByTestId('invite.selected_item.test@example.com');
|
|
expect(chip).toHaveProp('id', 'email-1');
|
|
expect(chip).toHaveProp('text', 'test@example.com');
|
|
chip.props.onRemove('email-1');
|
|
expect(mockOnRemoveItem).toHaveBeenCalledTimes(2);
|
|
expect(mockOnRemoveItem).toHaveBeenCalledWith('email-1');
|
|
});
|
|
|
|
it('does not render selected items when empty', () => {
|
|
const props = getBaseProps();
|
|
const {queryByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
expect(queryByTestId('invite.selected_items')).toBeNull();
|
|
});
|
|
|
|
it('renders search results correctly', () => {
|
|
const props = getBaseProps();
|
|
const user = TestHelper.fakeUser({id: 'user-1', username: 'user1'});
|
|
props.searchResults = [user, 'test@example.com'];
|
|
props.term = 'test';
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
expect(getByTestId('invite.search_list')).toBeVisible();
|
|
const userItem = getByTestId('invite.search_list_user_item');
|
|
expect(userItem).toHaveProp('user', user);
|
|
expect(userItem).toHaveProp('onUserPress', mockOnSelectItem);
|
|
userItem.props.onUserPress(user);
|
|
expect(mockOnSelectItem).toHaveBeenCalledTimes(1);
|
|
expect(mockOnSelectItem).toHaveBeenCalledWith(user);
|
|
const textItem = getByTestId('invite.search_list_text_item');
|
|
expect(textItem).toHaveProp('text', 'test@example.com');
|
|
expect(textItem).toHaveProp('type', TextItemType.SEARCH_INVITE);
|
|
fireEvent.press(textItem);
|
|
expect(mockOnSelectItem).toHaveBeenCalledTimes(2);
|
|
expect(mockOnSelectItem).toHaveBeenCalledWith('test@example.com');
|
|
});
|
|
|
|
it('renders no results message when term exists and no results', () => {
|
|
const props = getBaseProps();
|
|
props.term = 'nonexistent';
|
|
props.searchResults = [];
|
|
props.loading = false;
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
const textItem = getByTestId('invite.search_list_no_results');
|
|
expect(textItem).toHaveProp('text', 'nonexistent');
|
|
expect(textItem).toHaveProp('type', TextItemType.SEARCH_NO_RESULTS);
|
|
});
|
|
|
|
it('renders invite as guest option when canInviteGuests is true', () => {
|
|
const props = getBaseProps();
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
const optionItem = getByTestId('invite.invite_as_guest');
|
|
expect(optionItem).toHaveProp('label', 'Invite as guest');
|
|
expect(optionItem).toHaveProp('type', 'toggle');
|
|
expect(optionItem).toHaveProp('selected', false);
|
|
expect(optionItem).toHaveProp('action', expect.any(Function));
|
|
expect(optionItem).toHaveProp('testID', 'invite.invite_as_guest');
|
|
optionItem.props.action();
|
|
expect(mockOnSendOptionsChange).toHaveBeenCalledTimes(1);
|
|
const setStateFunction = mockOnSendOptionsChange.mock.calls[0][0];
|
|
const result = setStateFunction(props.sendOptions);
|
|
expect(result.inviteAsGuest).toBe(true);
|
|
});
|
|
|
|
it('does not render invite as guest option when canInviteGuests is false', () => {
|
|
const props = getBaseProps();
|
|
props.canInviteGuests = false;
|
|
|
|
const {queryByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
// The invite as guest option should not be rendered
|
|
const optionItem = queryByTestId('invite.invite_as_guest');
|
|
expect(optionItem).toBeNull();
|
|
});
|
|
|
|
it('renders custom message input when includeCustomMessage is true', () => {
|
|
const props = getBaseProps();
|
|
props.sendOptions = {
|
|
inviteAsGuest: true,
|
|
includeCustomMessage: true,
|
|
customMessage: 'Test message',
|
|
selectedChannels: [],
|
|
guestMagicLink: false,
|
|
};
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
expect(getByTestId('invite.custom_message')).toBeTruthy();
|
|
});
|
|
|
|
it('handles channel selection', () => {
|
|
const props = getBaseProps();
|
|
props.sendOptions = {
|
|
inviteAsGuest: true,
|
|
includeCustomMessage: false,
|
|
customMessage: '',
|
|
selectedChannels: [],
|
|
guestMagicLink: false,
|
|
};
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
const channelOption = getByTestId('invite.selected_channels');
|
|
channelOption.props.action();
|
|
expect(navigateToSettingsScreen).toHaveBeenCalledWith(
|
|
Screens.INTEGRATION_SELECTOR,
|
|
expect.objectContaining({
|
|
dataSource: 'channels',
|
|
isMultiselect: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('renders guest magic link option when guestMagicLink is true', () => {
|
|
const props = getBaseProps();
|
|
props.allowGuestMagicLink = true;
|
|
props.sendOptions = {
|
|
inviteAsGuest: true,
|
|
includeCustomMessage: false,
|
|
customMessage: '',
|
|
selectedChannels: [],
|
|
guestMagicLink: false,
|
|
};
|
|
|
|
const {getByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
const optionItem = getByTestId('invite.guest_magic_link');
|
|
expect(optionItem).toHaveProp('label', 'Use magic link');
|
|
expect(optionItem).toHaveProp('description', 'Newly created guests will join and log in without a password, using a magic link sent to their email address');
|
|
expect(optionItem).toHaveProp('type', 'toggle');
|
|
expect(optionItem).toHaveProp('selected', false);
|
|
|
|
optionItem.props.action();
|
|
expect(mockOnSendOptionsChange).toHaveBeenCalledTimes(1);
|
|
const setStateFunction = mockOnSendOptionsChange.mock.calls[0][0];
|
|
const result = setStateFunction(props.sendOptions);
|
|
expect(result.guestMagicLink).toBe(true);
|
|
});
|
|
|
|
it('does not render guest magic link option when allowGuestMagicLink is false', () => {
|
|
const props = getBaseProps();
|
|
props.allowGuestMagicLink = false;
|
|
props.sendOptions = {
|
|
inviteAsGuest: true,
|
|
includeCustomMessage: false,
|
|
customMessage: '',
|
|
selectedChannels: [],
|
|
guestMagicLink: false,
|
|
};
|
|
const {queryByTestId} = renderWithIntl(<Selection {...props}/>);
|
|
|
|
const optionItem = queryByTestId('invite.guest_magic_link');
|
|
expect(optionItem).toBeNull();
|
|
});
|
|
|
|
it('passes correct props to SelectionSearchBar', () => {
|
|
const props = getBaseProps();
|
|
props.term = 'test search';
|
|
|
|
renderWithIntl(<Selection {...props}/>);
|
|
|
|
const searchBar = jest.mocked(SelectionSearchBar).mock.calls[0][0];
|
|
expect(searchBar.term).toBe('test search');
|
|
expect(searchBar.onSearchChange).toBe(mockOnSearchChange);
|
|
});
|
|
|
|
it('passes correct props to SelectionTeamBar', () => {
|
|
const props = getBaseProps();
|
|
|
|
renderWithIntl(<Selection {...props}/>);
|
|
|
|
const teamBar = jest.mocked(SelectionTeamBar).mock.calls[0][0];
|
|
expect(teamBar.teamId).toBe('team-1');
|
|
expect(teamBar.teamDisplayName).toBe('Test Team');
|
|
expect(teamBar.onClose).toBe(mockOnClose);
|
|
});
|
|
});
|
|
|