From 0f927a6b7a7873364791075c5ee5e6173b23f4ae Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:21:27 -0400 Subject: [PATCH 1/7] Fix #88730: show Invite label for unknown-email results in SearchRouter canCreateOptimisticPersonalDetailOption used a zero-results gate that suppressed userToInvite whenever any partial-match contact existed. Replace with an exact-login-match check so the invite row appears even when other contacts are present. Label it with translate('common.invite') in SearchAutocompleteList so it renders as "Invite" rather than the raw email, matching every other userToInvite surface in the app. --- src/components/Search/SearchAutocompleteList.tsx | 4 ++-- src/libs/OptionsListUtils/index.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 19358db6171a..ed52b761f3c4 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -367,11 +367,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 a3c097019317..19baebdbee46 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1922,13 +1922,16 @@ function canCreateOptimisticPersonalDetailOption({ currentUserOption?: SearchOptionData | null; searchValue: string; }) { - if (recentReportOptions.length + personalDetailsOptions.length > 0) { + const normalizedSearchValue = addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase(); + const hasExactLoginMatch = + recentReportOptions.some((o) => o.login?.toLowerCase() === normalizedSearchValue) || personalDetailsOptions.some((o) => o.login?.toLowerCase() === normalizedSearchValue); + if (hasExactLoginMatch) { return false; } if (!currentUserOption) { return true; } - return currentUserOption.login !== addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() && currentUserOption.login !== searchValue?.toLowerCase(); + return currentUserOption.login !== normalizedSearchValue && currentUserOption.login !== searchValue?.toLowerCase(); } /** From 57361654b6cfd38d988589fcfc66d376cba36330 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Fri, 1 May 2026 14:44:34 -0400 Subject: [PATCH 2/7] Add regression tests for canCreateOptimisticPersonalDetailOption #88730 Verify that the invite row appears when partial-match contacts exist but none has an exact login match, and that it is suppressed when an exact match is found. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tests/unit/OptionsListUtilsTest.tsx | 764 ++++++++++++++++++++++------ 1 file changed, 604 insertions(+), 160 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index ab4d9d4bf4ff..7511665bd3ad 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -147,9 +147,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, @@ -160,8 +166,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, @@ -174,8 +184,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, @@ -186,8 +200,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, @@ -198,8 +216,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, @@ -210,8 +232,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, @@ -224,8 +250,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, @@ -238,8 +268,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, @@ -252,8 +286,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', @@ -267,8 +305,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", @@ -283,7 +325,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, @@ -300,7 +344,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, @@ -322,8 +368,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, @@ -338,8 +388,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, @@ -354,8 +408,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, @@ -370,10 +428,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', @@ -391,9 +457,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, @@ -408,7 +480,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, @@ -419,7 +493,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, @@ -435,8 +511,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, @@ -1146,7 +1226,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, @@ -1192,7 +1275,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, @@ -1212,7 +1298,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, @@ -1261,7 +1350,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, @@ -1286,7 +1378,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, @@ -1684,7 +1779,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, @@ -1730,7 +1828,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, @@ -1942,7 +2043,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, @@ -2050,8 +2154,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']; @@ -2583,7 +2691,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, @@ -2626,7 +2737,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, @@ -2687,9 +2801,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, @@ -2729,9 +2849,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, @@ -2771,9 +2897,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, @@ -2813,9 +2945,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, @@ -2853,8 +2991,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, @@ -3317,6 +3459,30 @@ 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 if email is an email of current user', () => { // Given a set of arguments with currentUserOption object // When we call canCreateOptimisticPersonalDetailOption @@ -3709,7 +3875,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(); @@ -3718,7 +3892,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); @@ -3744,10 +3926,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); @@ -3760,8 +3954,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); @@ -3779,9 +3979,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); @@ -3794,9 +4006,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); @@ -3805,8 +4026,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); @@ -3815,8 +4042,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); @@ -3825,10 +4058,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 @@ -4022,8 +4267,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, + }, }, }; @@ -4036,7 +4285,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'); @@ -4046,8 +4300,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, + }, }, }; @@ -4055,7 +4313,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); }); @@ -4308,7 +4571,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, @@ -4393,7 +4659,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, @@ -4414,7 +4684,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, @@ -4435,7 +4709,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, @@ -4456,7 +4733,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, @@ -4641,7 +4922,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 = { @@ -4952,7 +5237,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, }; @@ -4992,7 +5286,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'}], @@ -5034,7 +5337,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, }; @@ -5072,7 +5384,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 @@ -5127,7 +5448,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; @@ -5210,7 +5533,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 @@ -5274,7 +5599,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); @@ -5316,7 +5643,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( @@ -5470,7 +5799,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, + }, }, }; @@ -5482,7 +5813,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(); @@ -5540,7 +5874,9 @@ describe('OptionsListUtils', () => { isPinned: false, reportID: '18', participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, reportName: '', policyID, @@ -5582,8 +5918,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, @@ -5804,7 +6144,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, + }, }, }; @@ -5847,7 +6189,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID: submitterAccountID, participants: { - [submitterAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [submitterAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -5953,8 +6297,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, + }, }, }; @@ -6035,7 +6383,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, + }, }, }; @@ -6098,7 +6448,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6154,8 +6506,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, + }, }, }; @@ -6212,7 +6568,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6256,7 +6614,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6300,7 +6660,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6358,7 +6720,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6412,7 +6776,9 @@ describe('OptionsListUtils', () => { policyID: testPolicyID, ownerAccountID, participants: { - [ownerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [ownerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6482,7 +6848,9 @@ describe('OptionsListUtils', () => { policyID: formatTestPolicyID, ownerAccountID: formatOwnerAccountID, participants: { - [formatOwnerAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [formatOwnerAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6494,7 +6862,9 @@ describe('OptionsListUtils', () => { policyID: formatTestPolicyID, ownerAccountID: formatMemberAccountID, participants: { - [formatMemberAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [formatMemberAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6837,8 +7207,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, + }, }, }; @@ -6875,7 +7249,9 @@ describe('OptionsListUtils', () => { reportID, chatReportID, participants: { - 2: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + 2: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, }, }; @@ -6945,8 +7321,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, + }, }, }; @@ -6963,8 +7343,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, + }, }, }; @@ -6980,8 +7364,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, + }, }, }; @@ -6997,8 +7385,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, + }, }, }; @@ -7015,8 +7407,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, + }, }, }; @@ -7044,8 +7440,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': { @@ -7054,8 +7454,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': { @@ -7064,8 +7468,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, + }, }, }, }; @@ -7111,8 +7519,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, + }, }, }; @@ -7164,10 +7576,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: ''}, ]; @@ -7181,7 +7601,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) @@ -7198,7 +7622,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); @@ -7210,7 +7638,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); @@ -7241,8 +7673,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, + }, }, }; @@ -7258,8 +7694,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, + }, }, }; @@ -7276,8 +7716,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, + }, }, }; From d1756655b0eda80f970e4005c46d569aa2f58520 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Wed, 6 May 2026 16:37:49 -0400 Subject: [PATCH 3/7] Address Codex P1: also match raw E.164 logins in canCreateOptimisticPersonalDetailOption #88730 --- src/libs/OptionsListUtils/index.ts | 10 +++++++--- tests/unit/OptionsListUtilsTest.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 19baebdbee46..7f4f2dc649ec 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1923,15 +1923,19 @@ function canCreateOptimisticPersonalDetailOption({ searchValue: string; }) { const normalizedSearchValue = addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase(); - const hasExactLoginMatch = - recentReportOptions.some((o) => o.login?.toLowerCase() === normalizedSearchValue) || personalDetailsOptions.some((o) => o.login?.toLowerCase() === normalizedSearchValue); + 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 !== normalizedSearchValue && currentUserOption.login !== searchValue?.toLowerCase(); + return currentUserOption.login !== normalizedSearchValue && currentUserOption.login !== rawSearchValue; } /** diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 7511665bd3ad..dfc4ca4c708a 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -3483,6 +3483,30 @@ describe('OptionsListUtils', () => { 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 From d236d83000f7214da09a51174d0e2c64c3c8e794 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Tue, 19 May 2026 16:35:46 -0400 Subject: [PATCH 4/7] Add test: invite row shows "Invite" alternateText for unknown email in SearchAutocompleteList Covers the branch added in #88730 where userToInvite is spread with alternateText: translate('common.invite') before being pushed to recentReportsOptions. Closes the Codecov coverage gap on line 377 of SearchAutocompleteList.tsx. Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/SearchAutocompleteListTest.tsx | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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(); + }); + }); }); From f0db2588e85d752f708b4882d31a71570b54e823 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Tue, 19 May 2026 16:42:13 -0400 Subject: [PATCH 5/7] Add UI test: invite row alternateText covered in SearchAutocompleteList The ui/ test suite is what Codecov instruments. Adds a case that passes userToInvite via the mocked getSearchOptions and asserts the "Invite" alternateText is rendered, covering line 377 of SearchAutocompleteList.tsx. Co-Authored-By: Claude Sonnet 4.6 --- .../components/SearchAutocompleteListTest.tsx | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tests/ui/components/SearchAutocompleteListTest.tsx b/tests/ui/components/SearchAutocompleteListTest.tsx index 95a8d4cf7042..047a779d37a5 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,67 @@ 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'). + // eslint-disable-next-line @typescript-eslint/no-var-requires + const OptionsListUtils = jest.requireMock('@libs/OptionsListUtils') as { + getSearchOptions: jest.Mock; + combineOrderingOfReportsAndPersonalDetails: jest.Mock; + }; + + 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'; From 126cb5e422a0f23b22381ac3b87c88743a0dd298 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Wed, 20 May 2026 16:13:06 -0400 Subject: [PATCH 6/7] Fix ESLint: explicit props on SearchQueryListItem, remove stale disable directive --- src/components/Search/SearchAutocompleteList.tsx | 13 ++++++++++++- tests/ui/components/SearchAutocompleteListTest.tsx | 7 ++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index c911feb78c76..1bd4acae9318 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -108,7 +108,18 @@ function SearchRouterItem(props: UserListItemProps | Searc const styles = useThemeStyles(); if (isSearchQueryListItem(props)) { - return ; + const {item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus, shouldDisableHoverStyle} = props; + return ( + + ); } const {item, isFocused, showTooltip, isDisabled, onSelectRow, onDismissError, shouldPreventEnterKeySubmit, rightHandSideComponent, onFocus, shouldSyncFocus, wrapperStyle} = props; diff --git a/tests/ui/components/SearchAutocompleteListTest.tsx b/tests/ui/components/SearchAutocompleteListTest.tsx index 047a779d37a5..32702f361f66 100644 --- a/tests/ui/components/SearchAutocompleteListTest.tsx +++ b/tests/ui/components/SearchAutocompleteListTest.tsx @@ -131,11 +131,8 @@ describe('SearchAutocompleteList', () => { 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'). - // eslint-disable-next-line @typescript-eslint/no-var-requires - const OptionsListUtils = jest.requireMock('@libs/OptionsListUtils') as { - getSearchOptions: jest.Mock; - combineOrderingOfReportsAndPersonalDetails: jest.Mock; - }; + type OptionsListUtilsMock = {getSearchOptions: jest.Mock; combineOrderingOfReportsAndPersonalDetails: jest.Mock}; + const OptionsListUtils: OptionsListUtilsMock = jest.requireMock('@libs/OptionsListUtils'); const inviteOption = { reportID: undefined, From 27abac1a4e8720f05dae96d1ffd7c9ad1a809e43 Mon Sep 17 00:00:00 2001 From: Antonio Antonian <44030600+15antonian@users.noreply.github.com> Date: Wed, 20 May 2026 17:06:26 -0400 Subject: [PATCH 7/7] Revert SearchAutocompleteList to spread props; keep test file ESLint fix --- src/components/Search/SearchAutocompleteList.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 1bd4acae9318..c911feb78c76 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -108,18 +108,7 @@ function SearchRouterItem(props: UserListItemProps | Searc const styles = useThemeStyles(); if (isSearchQueryListItem(props)) { - const {item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus, shouldDisableHoverStyle} = props; - return ( - - ); + return ; } const {item, isFocused, showTooltip, isDisabled, onSelectRow, onDismissError, shouldPreventEnterKeySubmit, rightHandSideComponent, onFocus, shouldSyncFocus, wrapperStyle} = props;