mattermost-mobile-upstream/app/screens/invite/selection.test.tsx
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* 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
2026-05-13 17:44:55 +08:00

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);
});
});