Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ReportActionItem/TaskView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) {
{report?.managerID ? (
<MenuItem
label={translate('task.assignee')}
title={getDisplayNameForParticipant({accountID: report.managerID, formatPhoneNumber})}
title={getDisplayNameForParticipant({accountID: report.managerID, formatPhoneNumber, translate})}
iconAccountID={report.managerID}
iconType={CONST.ICON_TYPE_AVATAR}
avatarSize={CONST.AVATAR_SIZE.SMALLER}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function UserSelectionListItem<TItem extends ListItem>({
}: UserSelectionListItemProps<TItem>) {
const styles = useThemeStyles();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {formatPhoneNumber} = useLocalize();
const {formatPhoneNumber, translate} = useLocalize();

const userHandle = useMemo(() => {
const login = item.login ?? '';
Expand All @@ -58,8 +58,9 @@ function UserSelectionListItem<TItem extends ListItem>({
return getDisplayNameForParticipant({
accountID: item.accountID ?? CONST.DEFAULT_NUMBER_ID,
formatPhoneNumber,
translate,
});
}, [formatPhoneNumber, item.accountID]);
}, [formatPhoneNumber, item.accountID, translate]);

return (
<SelectableListItem
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ReportAddApproverPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA
}

const {avatar} = personalDetails?.[accountID] ?? {};
const displayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails, formatPhoneNumber});
const displayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails, formatPhoneNumber, translate});
return {
text: displayName,
alternateText: email,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/SearchAddApproverPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function SearchAddApproverPage() {
}

const {avatar} = personalDetails?.[accountID] ?? {};
const displayName = getDisplayNameForParticipant({accountID, formatPhoneNumber, personalDetailsData: personalDetails});
const displayName = getDisplayNameForParticipant({accountID, formatPhoneNumber, personalDetailsData: personalDetails, translate});
return {
text: displayName,
alternateText: email,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ShareCodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) {
if (isMoneyRequestReport(report)) {
// generate subtitle from participants
return getParticipantsAccountIDsForDisplay(report, true)
.map((accountID) => getDisplayNameForParticipant({accountID, formatPhoneNumber}))
.map((accountID) => getDisplayNameForParticipant({accountID, formatPhoneNumber, translate}))
.join(' & ');
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/inbox/report/ReportTypingIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 4 additions & 4 deletions src/pages/workspace/WorkspaceMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
);
}

Expand All @@ -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}),
});
}

Expand Down
4 changes: 4 additions & 0 deletions tests/ui/WorkspaceMembersTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/QuickActionMenuItemTest.tsx
Original file line number Diff line number Diff line change
@@ -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(
<OnyxListItemProvider>
<QuickActionMenuItem reportID={QUICK_ACTION_REPORT_ID} />
</OnyxListItemProvider>,
);
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');
});
});
97 changes: 97 additions & 0 deletions tests/unit/ReportAddApproverPageTranslateTest.tsx
Original file line number Diff line number Diff line change
@@ -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<Policy>({
id: 'approver-policy',
employeeList: {
[APPROVER_EMAIL]: {email: APPROVER_EMAIL, role: CONST.POLICY.ROLE.USER},
},
});
const report = createMock<Report>({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(
<OnyxListItemProvider>
<ReportAddApproverPage
// @ts-expect-error — the withReportOrNotFound HOC prop intersection collapses the route type; only params.reportID is read at runtime
route={{params: {reportID: report.reportID}}}
report={report}
policy={policy}
isLoadingReportData={false}
/>
</OnyxListItemProvider>,
);
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}));
});
});
63 changes: 63 additions & 0 deletions tests/unit/ReportTypingIndicatorTest.tsx
Original file line number Diff line number Diff line change
@@ -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(
<OnyxListItemProvider>
<ReportTypingIndicator reportID={REPORT_ID} />
</OnyxListItemProvider>,
);
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();
});
});
Loading
Loading