From 2369a7dd2d4ebbca67e16497564a14a581997aa6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 2 Aug 2025 16:45:47 +0700 Subject: [PATCH 1/6] remove onyx connect --- .../LHNOptionsList/OptionRowLHNData.tsx | 25 +++++++++++ .../ReportActionItem/IssueCardMessage.tsx | 4 +- .../useGetExpensifyCardFromReportAction.ts | 25 +++++++++++ src/libs/CardMessageUtils.ts | 45 ------------------- src/libs/SidebarUtils.ts | 6 +-- .../BaseReportActionContextMenu.tsx | 4 +- 6 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 src/hooks/useGetExpensifyCardFromReportAction.ts delete mode 100644 src/libs/CardMessageUtils.ts diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 41c3dbb45da8..de2def4dcb3a 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,9 +1,14 @@ import {deepEqual} from 'fast-equals'; import React, {useMemo, useRef} from 'react'; import useCurrentReportID from '@hooks/useCurrentReportID'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; +import useOnyx from '@hooks/useOnyx'; +import {getSortedReportActions, shouldReportActionBeVisibleAsLastAction} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -38,6 +43,25 @@ function OptionRowLHNData({ const isReportFocused = isOptionFocused && currentReportIDValue?.currentReportID === reportID; const optionItemRef = useRef(undefined); + + const [reportActionsData] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canBeMissing: true}); + + const lastAction = useMemo(() => { + if (!reportActionsData || !fullReport) { + return undefined; + } + + const canUserPerformWriteAction = canUserPerformWriteActionUtil(fullReport); + const actionsArray = getSortedReportActions(Object.values(reportActionsData)); + + const reportActionsForDisplay = actionsArray.filter( + (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + + return reportActionsForDisplay.at(-1); + }, [reportActionsData, fullReport]); + + const card = useGetExpensifyCardFromReportAction({reportAction: lastAction, policyID: fullReport?.policyID}); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! const item = SidebarUtils.getOptionData({ @@ -50,6 +74,7 @@ function OptionRowLHNData({ parentReportAction, lastMessageTextFromReport, invoiceReceiverPolicy, + card, }); // eslint-disable-next-line react-compiler/react-compiler if (deepEqual(item, optionItemRef.current)) { diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index 3a68dc4900d8..1c76d2260fd2 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -3,10 +3,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import {useSession} from '@components/OnyxListItemProvider'; import RenderHTML from '@components/RenderHTML'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getExpensifyCardFromReportAction} from '@libs/CardMessageUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getCardIssuedMessage, getOriginalMessage, shouldShowAddMissingDetails} from '@libs/ReportActionsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -25,7 +25,7 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) { const styles = useThemeStyles(); const session = useSession(); const assigneeAccountID = (getOriginalMessage(action) as IssueNewCardOriginalMessage)?.assigneeAccountID; - const expensifyCard = getExpensifyCardFromReportAction({reportAction: action, policyID}); + const expensifyCard = useGetExpensifyCardFromReportAction({reportAction: action, policyID}); const isAssigneeCurrentUser = !isEmptyObject(session) && session.accountID === assigneeAccountID; const shouldShowAddMissingDetailsButton = isAssigneeCurrentUser && shouldShowAddMissingDetails(action?.actionName, expensifyCard); const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); diff --git a/src/hooks/useGetExpensifyCardFromReportAction.ts b/src/hooks/useGetExpensifyCardFromReportAction.ts new file mode 100644 index 000000000000..9fe853cc983e --- /dev/null +++ b/src/hooks/useGetExpensifyCardFromReportAction.ts @@ -0,0 +1,25 @@ +import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card, ReportAction} from '@src/types/onyx'; +import useOnyx from './useOnyx'; + +function useGetExpensifyCardFromReportAction({reportAction, policyID}: {reportAction?: ReportAction; policyID?: string}): Card | undefined { + const [allUserCards] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); + const [allExpensifyCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, { + selector: (val) => { + const workspaceAccountID = getWorkspaceAccountID(policyID); + return val?.[`cards_${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; + }, + canBeMissing: true, + }); + const cardIssuedActionOriginalMessage = isCardIssuedAction(reportAction) ? getOriginalMessage(reportAction) : undefined; + + const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID; + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line deprecation/deprecation + return isPolicyAdmin(getPolicy(policyID)) ? allExpensifyCards?.[cardID] : allUserCards?.[cardID]; +} + +export default useGetExpensifyCardFromReportAction; diff --git a/src/libs/CardMessageUtils.ts b/src/libs/CardMessageUtils.ts deleted file mode 100644 index d1931062ba00..000000000000 --- a/src/libs/CardMessageUtils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import CONST from '@src/CONST'; -import type {OnyxValues} from '@src/ONYXKEYS'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, ReportAction, WorkspaceCardsList} from '@src/types/onyx'; -import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from './PolicyUtils'; -import {getOriginalMessage, isCardIssuedAction} from './ReportActionsUtils'; - -let allUserCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; -Onyx.connect({ - key: ONYXKEYS.CARD_LIST, - callback: (val) => { - if (!val || Object.keys(val).length === 0) { - return; - } - - allUserCards = val; - }, -}); - -let allWorkspaceCards: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, - waitForCollectionCallback: true, - callback: (value) => { - allWorkspaceCards = value; - }, -}); - -function getExpensifyCardFromReportAction({reportAction, policyID}: {reportAction?: ReportAction; policyID?: string}): Card | undefined { - const cardIssuedActionOriginalMessage = isCardIssuedAction(reportAction) ? getOriginalMessage(reportAction) : undefined; - - const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID; - const workspaceAccountID = getWorkspaceAccountID(policyID); - const allExpensifyCards = allWorkspaceCards?.[`cards_${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation - return isPolicyAdmin(getPolicy(policyID)) ? allExpensifyCards?.[cardID] : allUserCards[cardID]; -} - -export { - // eslint-disable-next-line import/prefer-default-export - getExpensifyCardFromReportAction, -}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index e49100a5ca38..1699e6cc1b89 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,7 +6,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -14,7 +14,6 @@ import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import {getExpensifyCardFromReportAction} from './CardMessageUtils'; import {extractCollectionItemID} from './CollectionUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import {translateLocal} from './Localize'; @@ -510,6 +509,7 @@ function getOptionData({ parentReportAction, lastMessageTextFromReport: lastMessageTextFromReportProp, invoiceReceiverPolicy, + card, }: { report: OnyxEntry; oneTransactionThreadReport: OnyxEntry; @@ -520,6 +520,7 @@ function getOptionData({ lastMessageTextFromReport?: string; invoiceReceiverPolicy?: OnyxEntry; reportAttributes: OnyxEntry; + card: Card | undefined; }): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -744,7 +745,6 @@ function getOptionData({ } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) { result.alternateText = getPolicyChangeLogEmployeeLeftMessage(lastAction, true); } else if (isCardIssuedAction(lastAction)) { - const card = getExpensifyCardFromReportAction({reportAction: lastAction, policyID: report.policyID}); result.alternateText = getCardIssuedMessage({reportAction: lastAction, expensifyCard: card}); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { result.alternateText = formatReportLastMessageText(Parser.htmlToText(`${lastActorDisplayName}: ${lastMessageText}`)); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index a3c49fddf1e9..408c42874c3b 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -11,6 +11,7 @@ import ContextMenuItem from '@components/ContextMenuItem'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useEnvironment from '@hooks/useEnvironment'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -20,7 +21,6 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; import useStyleUtils from '@hooks/useStyleUtils'; -import {getExpensifyCardFromReportAction} from '@libs/CardMessageUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getLinkedTransactionID, getOneTransactionThreadReportID, getOriginalMessage, getReportAction} from '@libs/ReportActionsUtils'; import { @@ -321,7 +321,7 @@ function BaseReportActionContextMenu({ }; // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const card = getExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); return ( (isVisible || shouldKeepOpen || !isMini) && ( From f10a19a90be97127453ee01e4acf16414154a829 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 2 Aug 2025 16:55:07 +0700 Subject: [PATCH 2/6] fix Uts --- tests/unit/SidebarUtilsTest.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 2e42cdbd1a80..72b6954520ea 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -327,6 +327,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); const optionDataUnpinned = SidebarUtils.getOptionData({ report: MOCK_REPORT_UNPINNED, @@ -336,6 +337,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); expect(optionDataPinned?.isPinned).toBe(true); @@ -829,6 +831,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); // Then the alternate text should be equal to the message of the last action prepended with the last actor display name. @@ -886,6 +889,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); // Then the alternate text should show @Hidden. @@ -926,6 +930,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, }); expect(optionData?.alternateText).toBe(`test message`); @@ -959,6 +964,8 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, + card: undefined, }); expect(optionData?.alternateText).toBe(`test message`); @@ -989,6 +996,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, }); expect(optionData?.alternateText).toBe(`test message`); @@ -1108,6 +1116,7 @@ describe('SidebarUtils', () => { policy, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(iouReport); const formattedAmount = convertToDisplayString(totalDisplaySpend, iouReport.currency); @@ -1149,6 +1158,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, }); expect(optionData?.alternateText).toBe(`${policy.name} ${CONST.DOT_SEPARATOR} test message`); @@ -1213,6 +1223,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); // Then the alternate text should be equal to the message of the last action prepended with the last actor display name. @@ -1254,6 +1265,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); expect(result?.alternateText).toBe(`You: ${getReportActionMessageText(lastAction)}`); @@ -1318,6 +1330,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); expect(result?.alternateText).toBe(`You: ${getReportActionMessageText(lastAction)}`); @@ -1428,6 +1441,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, }); expect(result?.alternateText).toContain(`${getReportActionMessageText(lastAction)}`); From 3a931a65058c240171f298800a5f71235d8ec5b2 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 2 Aug 2025 17:12:32 +0700 Subject: [PATCH 3/6] fixx UTs --- tests/perf-test/SidebarUtils.perf-test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 0973bbc0c9d9..9c3c8053a42b 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -83,6 +83,7 @@ describe('SidebarUtils', () => { policy, parentReportAction, oneTransactionThreadReport: undefined, + card: undefined, }), ); }); From 2b6d4ff5a1950e0573951fd6e5c552c470fcfbd2 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 2 Aug 2025 17:16:33 +0700 Subject: [PATCH 4/6] fix UTs --- tests/unit/SidebarUtilsTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 72b6954520ea..b803b0dc7db3 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -965,7 +965,6 @@ describe('SidebarUtils', () => { lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, card: undefined, - card: undefined, }); expect(optionData?.alternateText).toBe(`test message`); From 5ec7045cebdc0b07166c0f15f76310dac849a5ec Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 4 Aug 2025 14:47:04 +0700 Subject: [PATCH 5/6] add UTs --- .../LHNOptionsList/OptionRowLHNData.tsx | 1 + .../useGetExpensifyCardFromReportAction.ts | 2 +- ...useGetExpensifyCardFromReportActionTest.ts | 250 ++++++++++++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 tests/unit/useGetExpensifyCardFromReportActionTest.ts diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index de2def4dcb3a..ced099526566 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -107,6 +107,7 @@ function OptionRowLHNData({ invoiceReceiverPolicy, lastMessageTextFromReport, reportAttributes, + card, ]); return ( diff --git a/src/hooks/useGetExpensifyCardFromReportAction.ts b/src/hooks/useGetExpensifyCardFromReportAction.ts index 9fe853cc983e..653a772044c9 100644 --- a/src/hooks/useGetExpensifyCardFromReportAction.ts +++ b/src/hooks/useGetExpensifyCardFromReportAction.ts @@ -10,7 +10,7 @@ function useGetExpensifyCardFromReportAction({reportAction, policyID}: {reportAc const [allExpensifyCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, { selector: (val) => { const workspaceAccountID = getWorkspaceAccountID(policyID); - return val?.[`cards_${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; + return val?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; }, canBeMissing: true, }); diff --git a/tests/unit/useGetExpensifyCardFromReportActionTest.ts b/tests/unit/useGetExpensifyCardFromReportActionTest.ts new file mode 100644 index 000000000000..b72df7fc1d9e --- /dev/null +++ b/tests/unit/useGetExpensifyCardFromReportActionTest.ts @@ -0,0 +1,250 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import useGetExpensifyCardFromReportAction from '@src/hooks/useGetExpensifyCardFromReportAction'; +import type {OnyxKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card, ReportAction} from '@src/types/onyx'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +// Mock the dependencies +jest.mock('@libs/PolicyUtils'); +jest.mock('@libs/ReportActionsUtils'); + +const mockGetPolicy = getPolicy as jest.MockedFunction; +const mockGetWorkspaceAccountID = getWorkspaceAccountID as jest.MockedFunction; +const mockIsPolicyAdmin = isPolicyAdmin as jest.MockedFunction; +const mockGetOriginalMessage = getOriginalMessage as jest.MockedFunction; +const mockIsCardIssuedAction = isCardIssuedAction as jest.MockedFunction; + +describe('useGetExpensifyCardFromReportAction', () => { + const mockCard: Card = { + cardID: 123, + cardName: 'Test Card', + cardNumber: '1234567890123456', + domainName: 'test.com', + lastUpdated: '2023-01-01T00:00:00.000Z', + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE, + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + bank: 'Test Bank', + }; + + const createMockReportAction = (cardID = 123): ReportAction => ({ + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD, + originalMessage: {cardID, assigneeAccountID: 1}, + created: '2023-01-01T00:00:00.000Z', + actorAccountID: 1, + person: [], + shouldShow: true, + isAttachmentOnly: false, + isFirstItem: false, + pendingAction: null, + errors: undefined, + message: [], + reportID: '1', + }); + + const mockPolicy = { + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }; + + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(async () => { + await Onyx.clear(); + jest.clearAllMocks(); + + mockGetPolicy.mockReturnValue(mockPolicy); + mockGetWorkspaceAccountID.mockReturnValue(123); + mockIsPolicyAdmin.mockReturnValue(false); + mockGetOriginalMessage.mockReturnValue({cardID: 123, assigneeAccountID: 1}); + mockIsCardIssuedAction.mockReturnValue(true); + }); + + describe('when reportAction is not a card issued action', () => { + it('returns undefined', async () => { + mockIsCardIssuedAction.mockReturnValue(false); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('when reportAction is a card issued action', () => { + beforeEach(() => { + mockIsCardIssuedAction.mockReturnValue(true); + mockGetOriginalMessage.mockReturnValue({cardID: 123, assigneeAccountID: 1}); + }); + + describe('when user is not a policy admin', () => { + beforeEach(() => { + mockIsPolicyAdmin.mockReturnValue(false); + }); + + it('returns card from allUserCards when card exists', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('returns undefined when card does not exist in allUserCards', async () => { + Onyx.set(ONYXKEYS.CARD_LIST, {}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('when user is a policy admin', () => { + beforeEach(() => { + mockIsPolicyAdmin.mockReturnValue(true); + mockGetWorkspaceAccountID.mockReturnValue(123); + // Override the default policy for admin tests + mockGetPolicy.mockReturnValue({ + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }); + }); + + it('returns card from allExpensifyCards when card exists', async () => { + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(workspaceCardsKey, {123: mockCard}) + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('returns undefined when card does not exist in allExpensifyCards', async () => { + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {[workspaceCardsKey]: {}}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + }); + + describe('reactivity to Onyx changes', () => { + it('updates when allUserCards changes', async () => { + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('updates when allExpensifyCards changes for policy admin', async () => { + mockIsPolicyAdmin.mockReturnValue(true); + mockGetPolicy.mockReturnValue({ + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }); + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(workspaceCardsKey, {123: mockCard}) + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + }); + + describe('workspace account ID generation', () => { + it('calls getWorkspaceAccountID with correct policyID', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'test-policy-123'})); + await waitForBatchedUpdatesWithAct(); + + expect(mockGetWorkspaceAccountID).toHaveBeenCalledWith('test-policy-123'); + expect(result.current).toEqual(mockCard); + }); + }); + + describe('policy admin check', () => { + it('calls isPolicyAdmin with correct policy', async () => { + const testPolicy = { + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }; + mockGetPolicy.mockReturnValue(testPolicy); + + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(mockGetPolicy).toHaveBeenCalledWith('policy123'); + expect(mockIsPolicyAdmin).toHaveBeenCalledWith(testPolicy); + expect(result.current).toEqual(mockCard); + }); + }); +}); From 3d3b42d78c258b3c37f80ee9453fc64c37c70fac Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 4 Aug 2025 14:53:06 +0700 Subject: [PATCH 6/6] eslint --- tests/unit/useGetExpensifyCardFromReportActionTest.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/useGetExpensifyCardFromReportActionTest.ts b/tests/unit/useGetExpensifyCardFromReportActionTest.ts index b72df7fc1d9e..1aa4496b939a 100644 --- a/tests/unit/useGetExpensifyCardFromReportActionTest.ts +++ b/tests/unit/useGetExpensifyCardFromReportActionTest.ts @@ -13,6 +13,8 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' jest.mock('@libs/PolicyUtils'); jest.mock('@libs/ReportActionsUtils'); +// This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 +// eslint-disable-next-line deprecation/deprecation const mockGetPolicy = getPolicy as jest.MockedFunction; const mockGetWorkspaceAccountID = getWorkspaceAccountID as jest.MockedFunction; const mockIsPolicyAdmin = isPolicyAdmin as jest.MockedFunction; @@ -141,7 +143,7 @@ describe('useGetExpensifyCardFromReportAction', () => { it('returns card from allExpensifyCards when card exists', async () => { const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(workspaceCardsKey, {123: mockCard}) + Onyx.set(workspaceCardsKey, {123: mockCard}); await waitForBatchedUpdatesWithAct(); const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); @@ -199,7 +201,7 @@ describe('useGetExpensifyCardFromReportAction', () => { // eslint-disable-next-line @typescript-eslint/naming-convention const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; // eslint-disable-next-line @typescript-eslint/naming-convention - Onyx.set(workspaceCardsKey, {123: mockCard}) + Onyx.set(workspaceCardsKey, {123: mockCard}); await waitForBatchedUpdatesWithAct(); expect(result.current).toEqual(mockCard);