diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 77d2df9ed10a..c911feb78c76 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -374,11 +374,11 @@ function SearchAutocompleteList({ const reportOptions: OptionData[] = [...orderedOptions.recentReports, ...orderedOptions.personalDetails]; if (searchOptions.userToInvite) { - reportOptions.push(searchOptions.userToInvite); + reportOptions.push({...searchOptions.userToInvite, alternateText: translate('common.invite')}); } return reportOptions.slice(0, 20); - }, [autocompleteQueryValue, searchOptions]); + }, [autocompleteQueryValue, searchOptions, translate]); const debounceHandleSearch = useDebounce(() => { if (!handleSearch || !autocompleteQueryWithoutFilters) { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 381a139e70cc..f807853562d8 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1911,13 +1911,20 @@ function canCreateOptimisticPersonalDetailOption({ currentUserOption?: SearchOptionData | null; searchValue: string; }) { - if (recentReportOptions.length + personalDetailsOptions.length > 0) { + const normalizedSearchValue = addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase(); + const rawSearchValue = (searchValue ?? '').toLowerCase(); + const matchesLogin = (login: string | undefined) => { + const normalizedLogin = login?.toLowerCase(); + return normalizedLogin === normalizedSearchValue || normalizedLogin === rawSearchValue; + }; + const hasExactLoginMatch = recentReportOptions.some((o) => matchesLogin(o.login)) || personalDetailsOptions.some((o) => matchesLogin(o.login)); + if (hasExactLoginMatch) { return false; } if (!currentUserOption) { return true; } - return currentUserOption.login !== addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() && currentUserOption.login !== searchValue?.toLowerCase(); + return currentUserOption.login !== normalizedSearchValue && currentUserOption.login !== rawSearchValue; } /** diff --git a/tests/ui/components/SearchAutocompleteListTest.tsx b/tests/ui/components/SearchAutocompleteListTest.tsx index 95a8d4cf7042..32702f361f66 100644 --- a/tests/ui/components/SearchAutocompleteListTest.tsx +++ b/tests/ui/components/SearchAutocompleteListTest.tsx @@ -1,4 +1,4 @@ -import {act, render} from '@testing-library/react-native'; +import {act, render, screen} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; @@ -128,6 +128,64 @@ describe('SearchAutocompleteList', () => { expect(mockHtmlToText).not.toHaveBeenCalled(); }); + it('should set alternateText to "Invite" on the userToInvite row when autocompleteQueryValue is non-empty', async () => { + // Regression test for #88730: when userToInvite is present and the query is non-empty, + // recentReportsOptions spreads it with alternateText: translate('common.invite'). + type OptionsListUtilsMock = {getSearchOptions: jest.Mock; combineOrderingOfReportsAndPersonalDetails: jest.Mock}; + const OptionsListUtils: OptionsListUtilsMock = jest.requireMock('@libs/OptionsListUtils'); + + const inviteOption = { + reportID: undefined, + keyForList: 'unknown@example.com', + login: 'unknown@example.com', + text: 'unknown@example.com', + alternateText: '', + }; + + OptionsListUtils.getSearchOptions.mockImplementation(() => ({ + recentReports: [], + personalDetails: [], + currentUserOption: null, + userToInvite: inviteOption, + categoryOptions: [], + })); + OptionsListUtils.combineOrderingOfReportsAndPersonalDetails.mockImplementation(() => ({recentReports: [], personalDetails: []})); + + render( + + + + + , + ); + + await waitForBatchedUpdatesWithAct(); + + expect(screen.getByText('Invite')).toBeTruthy(); + + // Restore default mock so other tests are not affected + OptionsListUtils.getSearchOptions.mockImplementation(() => ({ + recentReports: [ + { + reportID: '10', + keyForList: '10', + text: 'Test Report', + alternateText: 'alternate text', + lastMessageText: 'last message', + }, + ], + personalDetails: [], + currentUserOption: null, + userToInvite: null, + categoryOptions: [], + })); + OptionsListUtils.combineOrderingOfReportsAndPersonalDetails.mockImplementation(() => ({recentReports: [], personalDetails: []})); + }); + it('should call Parser.htmlToText when parentReportAction is not ADD_COMMENT', async () => { const reportID = '10'; const parentReportID = '20'; diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index a642dc212c3d..2228aab96f1f 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -149,9 +149,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '1', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 5: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 5: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Iron Man, Mister Fantastic, Invisible Woman', type: CONST.REPORT.TYPE.CHAT, @@ -162,8 +168,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '2', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Spider-Man', type: CONST.REPORT.TYPE.CHAT, @@ -176,8 +186,12 @@ describe('OptionsListUtils', () => { isPinned: true, reportID: '3', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Mister Fantastic', type: CONST.REPORT.TYPE.CHAT, @@ -188,8 +202,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '4', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Black Panther', type: CONST.REPORT.TYPE.CHAT, @@ -200,8 +218,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '5', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 5: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 5: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Invisible Woman', type: CONST.REPORT.TYPE.CHAT, @@ -212,8 +234,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '6', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 6: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 6: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Thor', type: CONST.REPORT.TYPE.CHAT, @@ -226,8 +252,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '7', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 7: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 7: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Captain America', type: CONST.REPORT.TYPE.CHAT, @@ -240,8 +270,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '8', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 12: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 12: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Silver Surfer', type: CONST.REPORT.TYPE.CHAT, @@ -254,8 +288,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '9', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 8: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 8: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Mister Sinister', iouReportID: '100', @@ -269,8 +307,12 @@ describe('OptionsListUtils', () => { reportID: '10', isPinned: false, participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 7: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 7: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: '', oldPolicyName: "SHIELD's workspace", @@ -285,7 +327,9 @@ describe('OptionsListUtils', () => { reportID: '11', isPinned: false, participants: { - 10: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}, + 10: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, }, reportName: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, @@ -302,7 +346,9 @@ describe('OptionsListUtils', () => { reportID: '11', isPinned: false, participants: { - 10: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}, + 10: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, }, reportName: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, @@ -324,8 +370,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '11', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 999: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 999: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Concierge', type: CONST.REPORT.TYPE.CHAT, @@ -340,8 +390,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '12', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1000: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1000: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Chronos', type: CONST.REPORT.TYPE.CHAT, @@ -356,8 +410,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '13', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1001: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1001: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Receipts', type: CONST.REPORT.TYPE.CHAT, @@ -372,10 +430,18 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '14', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 10: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 10: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: '', oldPolicyName: 'Avengers Room', @@ -393,9 +459,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '15', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Spider-Man, Black Panther', type: CONST.REPORT.TYPE.CHAT, @@ -410,7 +482,9 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '16', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Expense Report', type: CONST.REPORT.TYPE.EXPENSE, @@ -421,7 +495,9 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '17', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: '', type: CONST.REPORT.TYPE.CHAT, @@ -437,8 +513,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1003: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1003: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Manager McTest', type: CONST.REPORT.TYPE.CHAT, @@ -1148,7 +1228,10 @@ describe('OptionsListUtils', () => { // Given a set of reports and personalDetails that includes Concierge // When we call getValidOptions() const results = getValidOptions( - {reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}, + { + reports: OPTIONS_WITH_CONCIERGE.reports, + personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, + }, allPolicies, {}, loginList, @@ -1194,7 +1277,10 @@ describe('OptionsListUtils', () => { const conciergeReportID = '11'; // When we call getValidOptions() with a conciergeReportID const results = getValidOptions( - {reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}, + { + reports: OPTIONS_WITH_CONCIERGE.reports, + personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, + }, allPolicies, {}, loginList, @@ -1214,7 +1300,10 @@ describe('OptionsListUtils', () => { // Given a set of reports and personalDetails that includes Chronos and a config object that excludes Chronos // When we call getValidOptions() const results = getValidOptions( - {reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, + { + reports: OPTIONS_WITH_CHRONOS.reports, + personalDetails: OPTIONS_WITH_CHRONOS.personalDetails, + }, allPolicies, {}, loginList, @@ -1263,7 +1352,10 @@ describe('OptionsListUtils', () => { // Given a set of reports and personalDetails that includes Manager McTest // When we call getValidOptions() const result = getValidOptions( - {reports: OPTIONS_WITH_MANAGER_MCTEST.reports, personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails}, + { + reports: OPTIONS_WITH_MANAGER_MCTEST.reports, + personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails, + }, allPolicies, {}, loginList, @@ -1288,7 +1380,10 @@ describe('OptionsListUtils', () => { // Given a set of reports and personalDetails that includes Manager McTest and a config object that excludes Manager McTest // When we call getValidOptions() const result = getValidOptions( - {reports: OPTIONS_WITH_MANAGER_MCTEST.reports, personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails}, + { + reports: OPTIONS_WITH_MANAGER_MCTEST.reports, + personalDetails: OPTIONS_WITH_MANAGER_MCTEST.personalDetails, + }, allPolicies, {}, loginList, @@ -1686,7 +1781,10 @@ describe('OptionsListUtils', () => { // Given a set of report and personalDetails that include Concierge // When we call getValidOptions() const results = getValidOptions( - {reports: OPTIONS_WITH_CONCIERGE.reports, personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails}, + { + reports: OPTIONS_WITH_CONCIERGE.reports, + personalDetails: OPTIONS_WITH_CONCIERGE.personalDetails, + }, allPolicies, {}, loginList, @@ -1732,7 +1830,10 @@ describe('OptionsListUtils', () => { // given a set of reports and personalDetails that includes Chronos // When we call getValidOptions() with excludeLogins param const results = getValidOptions( - {reports: OPTIONS_WITH_CHRONOS.reports, personalDetails: OPTIONS_WITH_CHRONOS.personalDetails}, + { + reports: OPTIONS_WITH_CHRONOS.reports, + personalDetails: OPTIONS_WITH_CHRONOS.personalDetails, + }, allPolicies, {}, loginList, @@ -1944,7 +2045,10 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with an empty search value const results = getValidOptions( - {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + { + reports: filteredReportsWithWorkspaceRooms, + personalDetails: OPTIONS.personalDetails, + }, allPolicies, {}, loginList, @@ -2052,8 +2156,12 @@ describe('OptionsListUtils', () => { const report = { ...REPORTS['1'], participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - [CONST.ACCOUNT_ID.MANAGER_MCTEST]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + [CONST.ACCOUNT_ID.MANAGER_MCTEST]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, } as Report; const lastActorDetails = PERSONAL_DETAILS['2']; @@ -2585,7 +2693,10 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with the filteredReports const options = getValidOptions( - {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + { + reports: filteredReportsWithWorkspaceRooms, + personalDetails: OPTIONS.personalDetails, + }, allPolicies, {}, loginList, @@ -2628,7 +2739,10 @@ describe('OptionsListUtils', () => { // When we call getValidOptions for share destination with the filteredReports const options = getValidOptions( - {reports: filteredReportsWithWorkspaceRooms, personalDetails: OPTIONS.personalDetails}, + { + reports: filteredReportsWithWorkspaceRooms, + personalDetails: OPTIONS.personalDetails, + }, allPolicies, {}, loginList, @@ -2689,9 +2803,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Team Chat', type: CONST.REPORT.TYPE.CHAT, @@ -2731,9 +2851,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Team Chat', type: CONST.REPORT.TYPE.CHAT, @@ -2773,9 +2899,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Team Chat', type: CONST.REPORT.TYPE.CHAT, @@ -2815,9 +2947,15 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 4: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 4: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Team Chat', type: CONST.REPORT.TYPE.CHAT, @@ -2855,8 +2993,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '19', participants: { - 9999: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 9998: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 9999: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 9998: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Unknown Group', type: CONST.REPORT.TYPE.CHAT, @@ -3319,6 +3461,54 @@ describe('OptionsListUtils', () => { expect(canCreate).toBe(true); }); + it('should allow to create optimistic option when partial-match contacts exist but none has an exact login match', () => { + // Regression test for #88730: the old gate returned false whenever any option was present, + // suppressing userToInvite even when no existing contact matched the typed email exactly. + const canCreate = canCreateOptimisticPersonalDetailOption({ + searchValue: 'unknown@example.com', + currentUserOption: {login: 'currentuser@expensify.com'} as OptionData, + personalDetailsOptions: [{login: 'unknownuser@other.com'} as OptionData], + recentReportOptions: [{login: 'unknown@other.org'} as OptionData], + }); + + expect(canCreate).toBe(true); + }); + + it('should not allow to create option when a contact with an exact login match exists', () => { + const canCreate = canCreateOptimisticPersonalDetailOption({ + searchValue: 'known@example.com', + currentUserOption: {login: 'currentuser@expensify.com'} as OptionData, + personalDetailsOptions: [{login: 'known@example.com'} as OptionData], + recentReportOptions: [], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option when phone-number contact is stored as raw E.164 without SMS domain', () => { + // Regression test for the Codex P1 review on PR #89405: searching a phone number must also match + // existing contacts whose login is stored as raw E.164 (no @expensify.sms suffix). + const canCreate = canCreateOptimisticPersonalDetailOption({ + searchValue: '+15005550006', + currentUserOption: {login: 'currentuser@expensify.com'} as OptionData, + personalDetailsOptions: [{login: '+15005550006'} as OptionData], + recentReportOptions: [], + }); + + expect(canCreate).toBe(false); + }); + + it('should not allow to create option when phone-number contact is stored with SMS domain', () => { + const canCreate = canCreateOptimisticPersonalDetailOption({ + searchValue: '+15005550006', + currentUserOption: {login: 'currentuser@expensify.com'} as OptionData, + personalDetailsOptions: [{login: '+15005550006@expensify.sms'} as OptionData], + recentReportOptions: [], + }); + + expect(canCreate).toBe(false); + }); + it('should not allow to create option if email is an email of current user', () => { // Given a set of arguments with currentUserOption object // When we call canCreateOptimisticPersonalDetailOption @@ -3711,7 +3901,15 @@ describe('OptionsListUtils', () => { it('should not filter report by subtitle if it is not an expense chat nor a chat room', () => { // Given a report object that is not an expense chat nor a chat room // When we call filterSelfDMChat with the report and a search term that matches the report's subtitle - const result = filterSelfDMChat({...REPORT, subtitle: SUBTITLE, isPolicyExpenseChat: false, isChatRoom: false}, ['Software']); + const result = filterSelfDMChat( + { + ...REPORT, + subtitle: SUBTITLE, + isPolicyExpenseChat: false, + isChatRoom: false, + }, + ['Software'], + ); // Then the returned value should be undefined expect(result).toBeUndefined(); @@ -3720,7 +3918,15 @@ describe('OptionsListUtils', () => { it('should filter report by subtitle if it is a chat room', () => { // Given a report object that is not an expense chat but is a chat room // When we call filterSelfDMChat with the report and a search term that matches the report's subtitle - const result = filterSelfDMChat({...REPORT, subtitle: SUBTITLE, isPolicyExpenseChat: false, isChatRoom: true}, ['Software']); + const result = filterSelfDMChat( + { + ...REPORT, + subtitle: SUBTITLE, + isPolicyExpenseChat: false, + isChatRoom: true, + }, + ['Software'], + ); // Then the returned value should be the same as the input expect(result?.reportID).toEqual(REPORT.reportID); @@ -3746,10 +3952,22 @@ describe('OptionsListUtils', () => { describe('getMostRecentOptions()', () => { it('returns the most recent options up to the specified limit', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, - {reportID: '3', lastVisibleActionCreated: '2022-01-01T09:00:00Z'} as OptionData, - {reportID: '4', lastVisibleActionCreated: '2022-01-01T13:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, + { + reportID: '3', + lastVisibleActionCreated: '2022-01-01T09:00:00Z', + } as OptionData, + { + reportID: '4', + lastVisibleActionCreated: '2022-01-01T13:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, 2); @@ -3762,8 +3980,14 @@ describe('OptionsListUtils', () => { it('returns all options if limit is greater than options length', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, 5); @@ -3781,9 +4005,21 @@ describe('OptionsListUtils', () => { it('applies filter function if provided', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z', isPinned: true} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z', isPinned: false} as OptionData, - {reportID: '3', lastVisibleActionCreated: '2022-01-01T09:00:00Z', isPinned: true} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + isPinned: true, + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + isPinned: false, + } as OptionData, + { + reportID: '3', + lastVisibleActionCreated: '2022-01-01T09:00:00Z', + isPinned: true, + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, 2, (option) => option.isPinned); @@ -3796,9 +4032,18 @@ describe('OptionsListUtils', () => { it('handles negative limit by returning empty array', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, - {reportID: '3', lastVisibleActionCreated: '2022-01-01T09:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, + { + reportID: '3', + lastVisibleActionCreated: '2022-01-01T09:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, -1); @@ -3807,8 +4052,14 @@ describe('OptionsListUtils', () => { it('handles negative limit with large absolute value', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, -100); @@ -3817,8 +4068,14 @@ describe('OptionsListUtils', () => { it('handles limit equal to zero', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; const result = optionsOrderBy(options, comparator, 0); @@ -3827,10 +4084,22 @@ describe('OptionsListUtils', () => { it('returns the older options up to the specified limit', () => { const options: OptionData[] = [ - {reportID: '1', lastVisibleActionCreated: '2022-01-01T10:00:00Z'} as OptionData, - {reportID: '2', lastVisibleActionCreated: '2022-01-01T12:00:00Z'} as OptionData, - {reportID: '3', lastVisibleActionCreated: '2022-01-01T09:00:00Z'} as OptionData, - {reportID: '4', lastVisibleActionCreated: '2022-01-01T13:00:00Z'} as OptionData, + { + reportID: '1', + lastVisibleActionCreated: '2022-01-01T10:00:00Z', + } as OptionData, + { + reportID: '2', + lastVisibleActionCreated: '2022-01-01T12:00:00Z', + } as OptionData, + { + reportID: '3', + lastVisibleActionCreated: '2022-01-01T09:00:00Z', + } as OptionData, + { + reportID: '4', + lastVisibleActionCreated: '2022-01-01T13:00:00Z', + } as OptionData, ]; const comparator = (option: OptionData) => option.lastVisibleActionCreated ?? ''; // We will pass reversed === true to sort the list in ascending order @@ -4024,8 +4293,12 @@ describe('OptionsListUtils', () => { reportID, chatReportID, participants: { - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -4038,7 +4311,12 @@ describe('OptionsListUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport); await waitForBatchedUpdates(); - const result = createOption({accountIDs: [1, 2], personalDetails: PERSONAL_DETAILS, report, privateIsArchived: undefined}); + const result = createOption({ + accountIDs: [1, 2], + personalDetails: PERSONAL_DETAILS, + report, + privateIsArchived: undefined, + }); expect(result.reportID).toBe(reportID); expect(typeof result.text).toBe('string'); @@ -4048,8 +4326,12 @@ describe('OptionsListUtils', () => { const report: Report = { ...createRandomReport(0, undefined), participants: { - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -4057,7 +4339,12 @@ describe('OptionsListUtils', () => { await waitForBatchedUpdates(); // Should not throw when reports is undefined - const result = createOption({accountIDs: [1, 2], personalDetails: PERSONAL_DETAILS, report, privateIsArchived: undefined}); + const result = createOption({ + accountIDs: [1, 2], + personalDetails: PERSONAL_DETAILS, + report, + privateIsArchived: undefined, + }); expect(result.reportID).toBe(report.reportID); }); @@ -4310,7 +4597,10 @@ describe('OptionsListUtils', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FOREIGN_CURRENCY_DEFAULT_TAX, message: [{type: 'COMMENT', text: ''}], - originalMessage: {oldName: 'Foreign Tax (15%)', newName: 'Foreign Tax (10%)'}, + originalMessage: { + oldName: 'Foreign Tax (15%)', + newName: 'Foreign Tax (10%)', + }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [action.reportActionID]: action, @@ -4417,7 +4707,11 @@ describe('OptionsListUtils', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ASSIGN_COMPANY_CARD, message: [{type: 'COMMENT', text: ''}], - originalMessage: {email: 'user@example.com', feedName: 'US Bank', cardLastFour: '1234'}, + originalMessage: { + email: 'user@example.com', + feedName: 'US Bank', + cardLastFour: '1234', + }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [action.reportActionID]: action, @@ -4438,7 +4732,11 @@ describe('OptionsListUtils', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UNASSIGN_COMPANY_CARD, message: [{type: 'COMMENT', text: ''}], - originalMessage: {email: 'user@example.com', feedName: 'US Bank', cardLastFour: '5678'}, + originalMessage: { + email: 'user@example.com', + feedName: 'US Bank', + cardLastFour: '5678', + }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [action.reportActionID]: action, @@ -4459,7 +4757,10 @@ describe('OptionsListUtils', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CARD_FEED_LIABILITY, message: [{type: 'COMMENT', text: ''}], - originalMessage: {feedName: 'Visa Commercial', liabilityType: CONST.TRANSACTION.LIABILITY_TYPE.ALLOW}, + originalMessage: { + feedName: 'Visa Commercial', + liabilityType: CONST.TRANSACTION.LIABILITY_TYPE.ALLOW, + }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [action.reportActionID]: action, @@ -4480,7 +4781,11 @@ describe('OptionsListUtils', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CARD_FEED_STATEMENT_PERIOD, message: [{type: 'COMMENT', text: ''}], - originalMessage: {feedName: 'Visa Commercial', statementPeriodEndDay: '15', previousStatementPeriodEndDay: '20'}, + originalMessage: { + feedName: 'Visa Commercial', + statementPeriodEndDay: '15', + previousStatementPeriodEndDay: '20', + }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [action.reportActionID]: action, @@ -4665,7 +4970,11 @@ describe('OptionsListUtils', () => { }); const transactions = getReportTransactions(report.reportID); const scanningTransactions = transactions.filter((transaction) => isScanning(transaction)); - expect(result).toBe(translateLocal('iou.receiptScanning', {count: scanningTransactions.length})); + expect(result).toBe( + translateLocal('iou.receiptScanning', { + count: scanningTransactions.length, + }), + ); }); it('should NOT leak fraud alert text when user cannot perform write actions', async () => { const report: Report = { @@ -4976,7 +5285,16 @@ describe('OptionsListUtils', () => { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, actorAccountID, created: DateUtils.getDBTime(), - message: [{type: 'COMMENT', text: 'Test message', html: 'Test message', isEdited: false, isDeletedParentAction: false, whisperedTo: []}], + message: [ + { + type: 'COMMENT', + text: 'Test message', + html: 'Test message', + isEdited: false, + isDeletedParentAction: false, + whisperedTo: [], + }, + ], shouldShow: true, pendingAction: null, }; @@ -5016,7 +5334,16 @@ describe('OptionsListUtils', () => { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, actorAccountID, created: DateUtils.getDBTime(), - message: [{type: 'COMMENT', text: 'Test message', html: 'Test message', isEdited: false, isDeletedParentAction: false, whisperedTo: []}], + message: [ + { + type: 'COMMENT', + text: 'Test message', + html: 'Test message', + isEdited: false, + isDeletedParentAction: false, + whisperedTo: [], + }, + ], shouldShow: true, pendingAction: null, person: [{text: 'Unknown User', type: 'TEXT'}], @@ -5058,7 +5385,16 @@ describe('OptionsListUtils', () => { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, actorAccountID: currentUserAccountID, created: DateUtils.getDBTime(), - message: [{type: 'COMMENT', text: 'Test message', html: 'Test message', isEdited: false, isDeletedParentAction: false, whisperedTo: []}], + message: [ + { + type: 'COMMENT', + text: 'Test message', + html: 'Test message', + isEdited: false, + isDeletedParentAction: false, + whisperedTo: [], + }, + ], shouldShow: true, pendingAction: null, }; @@ -5096,7 +5432,16 @@ describe('OptionsListUtils', () => { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, actorAccountID: undefined, created: DateUtils.getDBTime(), - message: [{type: 'COMMENT', text: 'Test message', html: 'Test message', isEdited: false, isDeletedParentAction: false, whisperedTo: []}], + message: [ + { + type: 'COMMENT', + text: 'Test message', + html: 'Test message', + isEdited: false, + isDeletedParentAction: false, + whisperedTo: [], + }, + ], shouldShow: true, pendingAction: null, person: [], // Ensure person array is empty so it doesn't create actorDetails from person @@ -5151,7 +5496,9 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; const personalDetails: PersonalDetailsList = PERSONAL_DETAILS; @@ -5234,7 +5581,9 @@ describe('OptionsListUtils', () => { ...createRandomReport(0, undefined), reportID: 'test-personal-details-1', participants: { - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; // Use a modified personalDetails that differs from what's in Onyx @@ -5298,7 +5647,9 @@ describe('OptionsListUtils', () => { areCategoriesEnabled: true, }; - const policies = {[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy}; + const policies = { + [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy, + }; // Test that getValidOptions accepts policies collection as second parameter const results = getValidOptions({reports: [], personalDetails: []}, policies, undefined, loginList, CURRENT_USER_ACCOUNT_ID, CURRENT_USER_EMAIL, undefined); @@ -5340,7 +5691,9 @@ describe('OptionsListUtils', () => { areCategoriesEnabled: true, }; - const policies = {[`${ONYXKEYS.COLLECTION.POLICY}${testPolicyID}`]: policy}; + const policies = { + [`${ONYXKEYS.COLLECTION.POLICY}${testPolicyID}`]: policy, + }; // Verify function works with policies parameter const results = getValidOptions( @@ -5494,7 +5847,9 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, participants: { - [currentUserAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [currentUserAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -5506,7 +5861,10 @@ describe('OptionsListUtils', () => { }, }; - await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: 'currentuser@test.com'}); + await Onyx.merge(ONYXKEYS.SESSION, { + accountID: currentUserAccountID, + email: 'currentuser@test.com', + }); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); await waitForBatchedUpdates(); @@ -5564,7 +5922,9 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: '', policyID, @@ -5606,8 +5966,12 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '19', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 3: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 3: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: 'Draft Report', type: CONST.REPORT.TYPE.CHAT, @@ -5828,7 +6192,9 @@ describe('OptionsListUtils', () => { reportName: 'Test Chat', type: CONST.REPORT.TYPE.CHAT, participants: { - [participantAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [participantAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -5871,7 +6237,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID: submitterAccountID, participants: { - [submitterAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [submitterAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -5977,8 +6345,12 @@ describe('OptionsListUtils', () => { accountID: receiverAccountID, }, participants: { - [senderAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - [receiverAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [senderAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + [receiverAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6059,7 +6431,9 @@ describe('OptionsListUtils', () => { reportName: 'Test Chat', type: CONST.REPORT.TYPE.CHAT, participants: { - [participantAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [participantAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6122,7 +6496,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6178,8 +6554,12 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - [memberAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + [memberAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6236,7 +6616,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6280,7 +6662,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6324,7 +6708,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6382,7 +6768,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6436,7 +6824,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6506,7 +6896,9 @@ describe('OptionsListUtils', () => { policyID: formatTestPolicyID, ownerAccountID: formatOwnerAccountID, participants: { - [formatOwnerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [formatOwnerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6518,7 +6910,9 @@ describe('OptionsListUtils', () => { policyID: formatTestPolicyID, ownerAccountID: formatMemberAccountID, participants: { - [formatMemberAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [formatMemberAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6861,8 +7255,12 @@ describe('OptionsListUtils', () => { chatReportID, type: CONST.REPORT.TYPE.EXPENSE, participants: { - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6899,7 +7297,9 @@ describe('OptionsListUtils', () => { reportID, chatReportID, participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6969,8 +7369,12 @@ describe('OptionsListUtils', () => { reportName: 'Test Report', type: CONST.REPORT.TYPE.CHAT, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6987,8 +7391,12 @@ describe('OptionsListUtils', () => { reportName: 'Archived Report', type: CONST.REPORT.TYPE.CHAT, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7004,8 +7412,12 @@ describe('OptionsListUtils', () => { reportName: 'Non-Archived Report', type: CONST.REPORT.TYPE.CHAT, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7021,8 +7433,12 @@ describe('OptionsListUtils', () => { reportName: 'Report with Attributes', type: CONST.REPORT.TYPE.CHAT, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7039,8 +7455,12 @@ describe('OptionsListUtils', () => { reportName: 'Report with Config', type: CONST.REPORT.TYPE.CHAT, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7068,8 +7488,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, lastVisibleActionCreated: '2022-01-01 00:00:00', participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }, '102': { @@ -7078,8 +7502,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, lastVisibleActionCreated: '2024-01-01 00:00:00', participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }, '103': { @@ -7088,8 +7516,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, lastVisibleActionCreated: '2023-01-01 00:00:00', participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }, }; @@ -7135,8 +7567,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, lastVisibleActionCreated: '2024-01-01 00:00:00', participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7188,10 +7624,18 @@ describe('OptionsListUtils', () => { describe('getFilteredRecentAttendees', () => { it('should deduplicate recent attendees by email', () => { const personalDetails = {}; - const attendees: Array<{email: string; displayName: string; avatarUrl: string}> = []; + const attendees: Array<{ + email: string; + displayName: string; + avatarUrl: string; + }> = []; const recentAttendees = [ {email: 'user1@example.com', displayName: 'User One', avatarUrl: ''}, - {email: 'user1@example.com', displayName: 'User One Duplicate', avatarUrl: ''}, // Duplicate by email + { + email: 'user1@example.com', + displayName: 'User One Duplicate', + avatarUrl: '', + }, // Duplicate by email {email: 'user2@example.com', displayName: 'User Two', avatarUrl: ''}, ]; @@ -7205,7 +7649,11 @@ describe('OptionsListUtils', () => { it('should deduplicate name-only attendees by displayName', () => { const personalDetails = {}; - const attendees: Array<{email: string; displayName: string; avatarUrl: string}> = []; + const attendees: Array<{ + email: string; + displayName: string; + avatarUrl: string; + }> = []; const recentAttendees = [ {email: '', displayName: 'Name Only', avatarUrl: ''}, {email: '', displayName: 'Name Only', avatarUrl: ''}, // Duplicate by displayName (name-only attendee) @@ -7222,7 +7670,11 @@ describe('OptionsListUtils', () => { it('should use displayName as login for name-only attendees', () => { const personalDetails = {}; - const attendees: Array<{email: string; displayName: string; avatarUrl: string}> = []; + const attendees: Array<{ + email: string; + displayName: string; + avatarUrl: string; + }> = []; const recentAttendees = [{email: '', displayName: 'John Smith', avatarUrl: ''}]; const result = getFilteredRecentAttendees(personalDetails, attendees, recentAttendees, CURRENT_USER_EMAIL, CURRENT_USER_ACCOUNT_ID); @@ -7234,7 +7686,11 @@ describe('OptionsListUtils', () => { it('should preserve displayName for recent attendees with undefined email', () => { const personalDetails = {}; - const attendees: Array<{email: string; displayName: string; avatarUrl: string}> = []; + const attendees: Array<{ + email: string; + displayName: string; + avatarUrl: string; + }> = []; const recentAttendees: Attendee[] = [{displayName: 'Login Only User', avatarUrl: ''}]; const result = getFilteredRecentAttendees(personalDetails, attendees, recentAttendees, CURRENT_USER_EMAIL, CURRENT_USER_ACCOUNT_ID); @@ -7265,8 +7721,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, policyID, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7282,8 +7742,12 @@ describe('OptionsListUtils', () => { type: CONST.REPORT.TYPE.CHAT, policyID, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -7300,8 +7764,12 @@ describe('OptionsListUtils', () => { policyID, ownerAccountID: 1, participants: { - [CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - 1: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CURRENT_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + 1: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; diff --git a/tests/unit/SearchAutocompleteListTest.tsx b/tests/unit/SearchAutocompleteListTest.tsx index 08874a3583e4..8542289cadb9 100644 --- a/tests/unit/SearchAutocompleteListTest.tsx +++ b/tests/unit/SearchAutocompleteListTest.tsx @@ -1,5 +1,5 @@ import type * as NativeNavigation from '@react-navigation/native'; -import {act, render, screen, waitFor} from '@testing-library/react-native'; +import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import Onyx from 'react-native-onyx'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; @@ -193,4 +193,27 @@ describe('SearchAutocompleteList', () => { expect(screen.getByText('type:expense status:approved')).toBeTruthy(); expect(screen.getByText('type:chat')).toBeTruthy(); }); + + it('should display invite row with "Invite" alternateText when typing an unknown email', async () => { + // Regression test for #88730: userToInvite row must carry alternateText: translate('common.invite') + // so the UI shows "Invite" beneath the unknown email address. + await waitForBatchedUpdates(); + await Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + }); + + render(); + await flushAllUpdates(); + + const input = screen.getByTestId('search-autocomplete-text-input'); + fireEvent.changeText(input, 'unknown@example.com'); + + await flushAllUpdates(); + + await waitFor(() => { + expect(screen.getByText('Invite')).toBeTruthy(); + }); + }); });