From 320bee1ce4373796aad6d75ccfeecc1de2d353be Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Wed, 15 Jul 2026 01:08:47 +0700 Subject: [PATCH] integrating translation support across multiple components --- .../useReportActionAvatars.ts | 2 +- src/components/ReportActionItem/TaskView.tsx | 2 +- .../ListItem/UserSelectionListItem.tsx | 5 +- src/pages/ReportAddApproverPage.tsx | 2 +- src/pages/Search/SearchAddApproverPage.tsx | 2 +- src/pages/ShareCodePage.tsx | 2 +- .../inbox/report/ReportTypingIndicator.tsx | 2 +- .../menuItems/QuickActionMenuItem.tsx | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 8 +- tests/ui/WorkspaceMembersTest.tsx | 4 + tests/unit/QuickActionMenuItemTest.tsx | 70 +++++++++++++ .../ReportAddApproverPageTranslateTest.tsx | 97 +++++++++++++++++++ tests/unit/ReportTypingIndicatorTest.tsx | 63 ++++++++++++ .../SearchAddApproverPageTranslateTest.tsx | 95 ++++++++++++++++++ tests/unit/ShareCodePageTranslateTest.tsx | 80 +++++++++++++++ tests/unit/TaskViewTranslateTest.tsx | 75 ++++++++++++++ tests/unit/UserSelectionListItemTest.tsx | 55 +++++++++++ .../useReportActionAvatarsTranslateTest.tsx | 57 +++++++++++ 18 files changed, 610 insertions(+), 13 deletions(-) create mode 100644 tests/unit/QuickActionMenuItemTest.tsx create mode 100644 tests/unit/ReportAddApproverPageTranslateTest.tsx create mode 100644 tests/unit/ReportTypingIndicatorTest.tsx create mode 100644 tests/unit/SearchAddApproverPageTranslateTest.tsx create mode 100644 tests/unit/ShareCodePageTranslateTest.tsx create mode 100644 tests/unit/TaskViewTranslateTest.tsx create mode 100644 tests/unit/UserSelectionListItemTest.tsx create mode 100644 tests/unit/useReportActionAvatarsTranslateTest.tsx diff --git a/src/components/ReportActionAvatars/useReportActionAvatars.ts b/src/components/ReportActionAvatars/useReportActionAvatars.ts index debfdd0c0563..507cb37e35d5 100644 --- a/src/components/ReportActionAvatars/useReportActionAvatars.ts +++ b/src/components/ReportActionAvatars/useReportActionAvatars.ts @@ -220,7 +220,7 @@ function useReportActionAvatars({ const accountID = reportPreviewSenderID || (actorAccountID ?? CONST.DEFAULT_NUMBER_ID); const {avatar, fallbackIcon, login} = personalDetails?.[delegatePersonalDetails ? delegatePersonalDetails.accountID : accountID] ?? {}; - const defaultDisplayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails, formatPhoneNumber}) ?? ''; + const defaultDisplayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails, formatPhoneNumber, translate}) ?? ''; const invoiceReport = [iouReport, chatReport, reportChatReport].find((susReport) => isInvoiceReport(susReport) || susReport?.chatType === CONST.REPORT.TYPE.INVOICE); const isNestedInInvoiceReport = !!invoiceReport && !isChatThread(report); const isInvoiceReportActor = isAInvoiceReport && (!actorAccountID || displayAllActors || isAReportPreviewAction); diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 0c5a95f47611..5404b8571de4 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -272,7 +272,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { {report?.managerID ? ( ({ }: UserSelectionListItemProps) { const styles = useThemeStyles(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {formatPhoneNumber} = useLocalize(); + const {formatPhoneNumber, translate} = useLocalize(); const userHandle = useMemo(() => { const login = item.login ?? ''; @@ -58,8 +58,9 @@ function UserSelectionListItem({ return getDisplayNameForParticipant({ accountID: item.accountID ?? CONST.DEFAULT_NUMBER_ID, formatPhoneNumber, + translate, }); - }, [formatPhoneNumber, item.accountID]); + }, [formatPhoneNumber, item.accountID, translate]); return ( getDisplayNameForParticipant({accountID, formatPhoneNumber})) + .map((accountID) => getDisplayNameForParticipant({accountID, formatPhoneNumber, translate})) .join(' & '); } diff --git a/src/pages/inbox/report/ReportTypingIndicator.tsx b/src/pages/inbox/report/ReportTypingIndicator.tsx index af0afc085e2b..9d9ad4d59c8e 100755 --- a/src/pages/inbox/report/ReportTypingIndicator.tsx +++ b/src/pages/inbox/report/ReportTypingIndicator.tsx @@ -35,7 +35,7 @@ function ReportTypingIndicator({reportID}: ReportTypingIndicatorProps) { // If the user is typing on OldDot, firstUserTyping will be a string (the user's displayName) const firstUserTypingDisplayName = isUserTypingADisplayName ? firstUserTyping - : getDisplayNameForParticipant({accountID: Number(firstUserTyping), shouldFallbackToHidden: false, formatPhoneNumber}); + : getDisplayNameForParticipant({accountID: Number(firstUserTyping), shouldFallbackToHidden: false, formatPhoneNumber, translate}); if (usersTyping.length === 1) { return ( diff --git a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx index 7651391d8c4c..51d38ba3d304 100644 --- a/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx +++ b/src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx @@ -97,7 +97,7 @@ function QuickActionMenuItem({reportID}: QuickActionMenuItemProps) { if (!isEmptyObject(quickActionReport)) { if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { const accountID = quickActionAvatars.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID; - const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber}) ?? ''; + const name = getDisplayNameForParticipant({accountID: Number(accountID), shouldUseShortForm: true, formatPhoneNumber, translate}) ?? ''; quickActionTitle = translate('quickAction.paySomeone', name); } else { const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 4baf8be44f16..6ca26bb334d2 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -166,8 +166,8 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers const approverAccountID = policyMemberEmailsToAccountIDs[approverEmail]; return translate( 'workspace.people.removeMembersWarningPrompt', - getDisplayNameForParticipant({accountID: approverAccountID, formatPhoneNumber}), - getDisplayNameForParticipant({accountID: policy?.ownerAccountID, formatPhoneNumber}), + getDisplayNameForParticipant({accountID: approverAccountID, formatPhoneNumber, translate}), + getDisplayNameForParticipant({accountID: policy?.ownerAccountID, formatPhoneNumber, translate}), ); } @@ -177,8 +177,8 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers if (userExporter) { const exporterAccountID = policyMemberEmailsToAccountIDs[userExporter]; return translate('workspace.people.removeMemberPromptExporter', { - memberName: getDisplayNameForParticipant({accountID: exporterAccountID, formatPhoneNumber}), - workspaceOwner: getDisplayNameForParticipant({accountID: policy?.ownerAccountID, formatPhoneNumber}), + memberName: getDisplayNameForParticipant({accountID: exporterAccountID, formatPhoneNumber, translate}), + workspaceOwner: getDisplayNameForParticipant({accountID: policy?.ownerAccountID, formatPhoneNumber, translate}), }); } diff --git a/tests/ui/WorkspaceMembersTest.tsx b/tests/ui/WorkspaceMembersTest.tsx index 7d9a6f78fcaf..0b542bc18c65 100644 --- a/tests/ui/WorkspaceMembersTest.tsx +++ b/tests/ui/WorkspaceMembersTest.tsx @@ -459,6 +459,10 @@ describe('WorkspaceMembers', () => { expect(screen.getByLabelText(confirmText)).toBeOnTheScreen(); }); + // The prompt resolves the approver's and workspace owner's display names through the translate-aware pipeline + const warningPrompt = TestHelper.translateLocal('workspace.people.removeMembersWarningPrompt', 'Admin User', 'Owner User'); + expect(screen.getByText(warningPrompt)).toBeOnTheScreen(); + unmount(); }); }); diff --git a/tests/unit/QuickActionMenuItemTest.tsx b/tests/unit/QuickActionMenuItemTest.tsx new file mode 100644 index 000000000000..968b41e34d72 --- /dev/null +++ b/tests/unit/QuickActionMenuItemTest.tsx @@ -0,0 +1,70 @@ +import {render} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import QuickActionMenuItem from '@pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import {createRegularChat} from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: () => new Proxy({}, {get: (_, name) => String(name)}), +})); + +jest.mock('@pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem', () => jest.fn(() => null)); + +const AVATAR_ACCOUNT_ID = 555001; + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + getIcons: jest.fn(() => [{id: 555001, source: '', name: 'Avatar', type: 'avatar'}]), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const QUICK_ACTION_REPORT_ID = '991001'; + +describe('QuickActionMenuItem', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('resolves the pay-someone title name through the translate function from useLocalize', async () => { + const report = {...createRegularChat(Number(QUICK_ACTION_REPORT_ID), [1, AVATAR_ACCOUNT_ID]), reportID: QUICK_ACTION_REPORT_ID}; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${QUICK_ACTION_REPORT_ID}`, report); + await Onyx.merge(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {action: CONST.QUICK_ACTIONS.SEND_MONEY, chatReportID: QUICK_ACTION_REPORT_ID}); + await waitForBatchedUpdates(); + + render( + + + , + ); + await waitForBatchedUpdates(); + + // The pay-someone quick action resolves the payee name via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: AVATAR_ACCOUNT_ID, shouldUseShortForm: true, translate: mockTranslate})); + expect(mockTranslate).toHaveBeenCalledWith('quickAction.paySomeone', 'SPY_NAME'); + }); +}); diff --git a/tests/unit/ReportAddApproverPageTranslateTest.tsx b/tests/unit/ReportAddApproverPageTranslateTest.tsx new file mode 100644 index 000000000000..c6f2c5af13cd --- /dev/null +++ b/tests/unit/ReportAddApproverPageTranslateTest.tsx @@ -0,0 +1,97 @@ +import {render} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import ReportAddApproverPage from '@pages/ReportAddApproverPage'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import createMock from '../utils/createMock'; +import {buildPersonalDetails} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: () => new Proxy({}, {get: (_, name) => String(name)}), +})); + +jest.mock('@components/ApproverSelectionList', () => jest.fn(() => null)); + +jest.mock('@hooks/usePressLoading', () => () => ({isLoading: false, startWithLoading: (fn: () => void) => fn})); + +jest.mock('@react-navigation/native', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actualNav = jest.requireActual('@react-navigation/native'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actualNav, + useIsFocused: () => true, + }; +}); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + isAllowedToApproveExpenseReport: jest.fn(() => true), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const APPROVER_ACCOUNT_ID = 727001; +const APPROVER_EMAIL = 'approver@test.com'; + +describe('ReportAddApproverPage', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('resolves candidate approver names through the translate function from useLocalize', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[APPROVER_ACCOUNT_ID]: buildPersonalDetails(APPROVER_EMAIL, APPROVER_ACCOUNT_ID, 'Approver')}); + await waitForBatchedUpdates(); + + const policy = createMock({ + id: 'approver-policy', + employeeList: { + [APPROVER_EMAIL]: {email: APPROVER_EMAIL, role: CONST.POLICY.ROLE.USER}, + }, + }); + const report = createMock({reportID: '727100', type: CONST.REPORT.TYPE.EXPENSE, policyID: policy.id}); + // The withReportOrNotFound HOC resolves the report and policy from Onyx via route.params.reportID. + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await waitForBatchedUpdates(); + render( + + + , + ); + await waitForBatchedUpdates(); + + // Each candidate approver's name resolves via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: APPROVER_ACCOUNT_ID, translate: mockTranslate})); + }); +}); diff --git a/tests/unit/ReportTypingIndicatorTest.tsx b/tests/unit/ReportTypingIndicatorTest.tsx new file mode 100644 index 000000000000..adf7b3cf6afb --- /dev/null +++ b/tests/unit/ReportTypingIndicatorTest.tsx @@ -0,0 +1,63 @@ +import {render, screen} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const REPORT_ID = '881001'; +const TYPING_ACCOUNT_ID = 777001; + +describe('ReportTypingIndicator', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + it('resolves the typing user display name through the translate function from useLocalize', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${REPORT_ID}`, {[TYPING_ACCOUNT_ID]: true}); + await waitForBatchedUpdates(); + + render( + + + , + ); + await waitForBatchedUpdates(); + + // The typing user's name resolves via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: TYPING_ACCOUNT_ID, shouldFallbackToHidden: false, translate: mockTranslate})); + expect(screen.getByText('SPY_NAME')).toBeOnTheScreen(); + }); +}); diff --git a/tests/unit/SearchAddApproverPageTranslateTest.tsx b/tests/unit/SearchAddApproverPageTranslateTest.tsx new file mode 100644 index 000000000000..134ecbfeabdf --- /dev/null +++ b/tests/unit/SearchAddApproverPageTranslateTest.tsx @@ -0,0 +1,95 @@ +import {render} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import SearchAddApproverPage from '@pages/Search/SearchAddApproverPage'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import createMock from '../utils/createMock'; +import {buildPersonalDetails} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +const SELECTED_REPORT_ID = '737100'; +const POLICY_ID = 'search-approver-policy'; + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: () => new Proxy({}, {get: (_, name) => String(name)}), +})); + +jest.mock('@components/ApproverSelectionList', () => jest.fn(() => null)); + +jest.mock('@hooks/usePressLoading', () => () => ({isLoading: false, startWithLoading: (fn: () => void) => fn})); + +jest.mock('@components/Search/SearchContext', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@components/Search/SearchContext'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + useSearchSelectionActions: () => ({clearSelectedTransactions: jest.fn()}), + useSearchSelectionContext: () => ({selectedReports: [{reportID: '737100', policyID: 'search-approver-policy'}]}), + }; +}); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + isAllowedToApproveExpenseReport: jest.fn(() => true), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const APPROVER_ACCOUNT_ID = 737001; +const APPROVER_EMAIL = 'search-approver@test.com'; +const MANAGER_ACCOUNT_ID = 737002; + +describe('SearchAddApproverPage', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('resolves candidate approver names through the translate function from useLocalize', async () => { + const policy = createMock({ + id: POLICY_ID, + employeeList: { + [APPROVER_EMAIL]: {email: APPROVER_EMAIL, role: CONST.POLICY.ROLE.USER}, + }, + }); + const report = createMock({reportID: SELECTED_REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, policyID: POLICY_ID, managerID: MANAGER_ACCOUNT_ID}); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[APPROVER_ACCOUNT_ID]: buildPersonalDetails(APPROVER_EMAIL, APPROVER_ACCOUNT_ID, 'Approver')}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${SELECTED_REPORT_ID}`, report); + await waitForBatchedUpdates(); + + render( + + + , + ); + await waitForBatchedUpdates(); + + // Each candidate approver's name resolves via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: APPROVER_ACCOUNT_ID, translate: mockTranslate})); + }); +}); diff --git a/tests/unit/ShareCodePageTranslateTest.tsx b/tests/unit/ShareCodePageTranslateTest.tsx new file mode 100644 index 000000000000..61fa0593c1d5 --- /dev/null +++ b/tests/unit/ShareCodePageTranslateTest.tsx @@ -0,0 +1,80 @@ +import {render} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import ShareCodePage from '@pages/ShareCodePage'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: () => new Proxy({}, {get: (_, name) => String(name)}), +})); + +jest.mock('@components/ScreenWrapper', () => jest.fn(({children}: {children: React.ReactNode}) => children)); +jest.mock('@components/HeaderWithBackButton', () => jest.fn(() => null)); +jest.mock('@components/QRShare/QRShareWithDownload', () => jest.fn(() => null)); +jest.mock('@components/ContextMenuItem', () => jest.fn(() => null)); +jest.mock('@components/MenuItem', () => jest.fn(() => null)); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const PARTICIPANT_A = 717001; +const PARTICIPANT_B = 717002; + +describe('ShareCodePage', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('resolves money request participant names through the translate function from useLocalize', async () => { + // A money request (IOU) report renders its subtitle from participant display names. + const iouReport: Report = { + reportID: '717100', + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: PARTICIPANT_A, + managerID: PARTICIPANT_B, + participants: { + [PARTICIPANT_A]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [PARTICIPANT_B]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + await waitForBatchedUpdates(); + + render( + + + , + ); + await waitForBatchedUpdates(); + + // Each participant's name resolves via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({translate: mockTranslate})); + }); +}); diff --git a/tests/unit/TaskViewTranslateTest.tsx b/tests/unit/TaskViewTranslateTest.tsx new file mode 100644 index 000000000000..5656e131128a --- /dev/null +++ b/tests/unit/TaskViewTranslateTest.tsx @@ -0,0 +1,75 @@ +import {render} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import TaskView from '@components/ReportActionItem/TaskView'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import {createRegularTaskReport} from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber, localeCompare: (a: string, b: string) => a.localeCompare(b)})); + +jest.mock('@components/RenderHTML', () => () => null); + +jest.mock('@hooks/useScreenWrapperTransitionStatus', () => ({ + __esModule: true, + default: () => ({didScreenTransitionEnd: true}), +})); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const TASK_MANAGER_ACCOUNT_ID = 616161; + +describe('TaskView', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('resolves the assignee display name through the translate function from useLocalize', async () => { + const taskReport: Report = { + ...createRegularTaskReport(1, TASK_MANAGER_ACCOUNT_ID), + managerID: TASK_MANAGER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${taskReport.reportID}`, taskReport); + await waitForBatchedUpdates(); + + render( + + + , + ); + await waitForBatchedUpdates(); + + // The assignee menu item resolves its title via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: TASK_MANAGER_ACCOUNT_ID, translate: mockTranslate})); + }); +}); diff --git a/tests/unit/UserSelectionListItemTest.tsx b/tests/unit/UserSelectionListItemTest.tsx new file mode 100644 index 000000000000..7446855b8c76 --- /dev/null +++ b/tests/unit/UserSelectionListItemTest.tsx @@ -0,0 +1,55 @@ +import {render} from '@testing-library/react-native'; + +import type {ListItem} from '@components/SelectionList/ListItem/types'; +import UserSelectionListItem from '@components/SelectionList/ListItem/UserSelectionListItem'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import React from 'react'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber})); + +jest.mock('@hooks/useCurrentUserPersonalDetails', () => () => ({accountID: 1, login: 'current@test.com'})); + +jest.mock('@components/SelectionList/ListItem/SelectableListItem', () => jest.fn(() => null)); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const ITEM_ACCOUNT_ID = 424242; + +describe('UserSelectionListItem', () => { + it('resolves the user display name through the translate function from useLocalize', () => { + const item: ListItem = { + keyForList: 'user-item', + accountID: ITEM_ACCOUNT_ID, + login: 'someone@test.com', + }; + + render( + , + ); + + // The row's display name resolves via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: ITEM_ACCOUNT_ID, translate: mockTranslate})); + }); +}); diff --git a/tests/unit/useReportActionAvatarsTranslateTest.tsx b/tests/unit/useReportActionAvatarsTranslateTest.tsx new file mode 100644 index 000000000000..7998f89abb8d --- /dev/null +++ b/tests/unit/useReportActionAvatarsTranslateTest.tsx @@ -0,0 +1,57 @@ +import {renderHook} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import useReportActionAvatars from '@components/ReportActionAvatars/useReportActionAvatars'; + +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +import Onyx from 'react-native-onyx'; + +import createRandomReportAction from '../utils/collections/reportActions'; +import {createRegularChat} from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockTranslate = jest.fn((path: string) => path); +const mockFormatPhoneNumber = jest.fn((value: string) => value); + +jest.mock('@hooks/useLocalize', () => () => ({translate: mockTranslate, formatPhoneNumber: mockFormatPhoneNumber, localeCompare: (a: string, b: string) => a.localeCompare(b)})); + +jest.mock('@libs/ReportUtils', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@libs/ReportUtils'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + __esModule: true, + getDisplayNameForParticipant: jest.fn(() => 'SPY_NAME'), + }; +}); + +const mockGetDisplayNameForParticipant = jest.mocked(getDisplayNameForParticipant); + +const ACTOR_ACCOUNT_ID = 636363; + +const wrapper = ({children}: {children: React.ReactNode}) => { + return {children}; +}; + +describe('useReportActionAvatars translate wiring', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + it('threads the translate function from useLocalize into the default display name resolution', async () => { + const report = createRegularChat(1, [ACTOR_ACCOUNT_ID]); + const action = {...createRandomReportAction(2), actorAccountID: ACTOR_ACCOUNT_ID}; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await waitForBatchedUpdates(); + + renderHook(() => useReportActionAvatars({report, action}), {wrapper}); + + // The hook resolves the default display name via getDisplayNameForParticipant, which must receive the translate from useLocalize. + expect(mockGetDisplayNameForParticipant).toHaveBeenCalledWith(expect.objectContaining({accountID: ACTOR_ACCOUNT_ID, translate: mockTranslate})); + }); +});