// 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 {goToScreen} 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) => 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', () => ({ goToScreen: 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 { 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(); 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(); 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(); 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(); 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(); 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(); 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(); // 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(); 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(); const channelOption = getByTestId('invite.selected_channels'); channelOption.props.action(); expect(goToScreen).toHaveBeenCalledWith( Screens.INTEGRATION_SELECTOR, expect.any(String), 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(); 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(); const optionItem = queryByTestId('invite.guest_magic_link'); expect(optionItem).toBeNull(); }); it('passes correct props to SelectionSearchBar', () => { const props = getBaseProps(); props.term = 'test search'; renderWithIntl(); 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(); 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); }); });