From ac701cc405dd40c35cb8e7b47fdf32ba0892c140 Mon Sep 17 00:00:00 2001 From: Amy Evans Date: Fri, 16 Jan 2026 16:58:44 -0500 Subject: [PATCH 01/17] Add Mock Bank OAuth connection --- src/CONST/index.ts | 2 + src/libs/CardUtils.ts | 1 + .../companyCards/addNew/SelectBankStep.tsx | 37 ++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3bc77c33513e0..3b7684bdd9069 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3580,6 +3580,7 @@ const CONST = { CITI_BANK: 'Citibank', STRIPE: 'Stripe', WELLS_FARGO: 'Wells Fargo', + MOCK_BANK: 'Mock Bank (Local Testing)', OTHER: 'Other', }, BANK_CONNECTIONS: { @@ -3590,6 +3591,7 @@ const CONST = { CAPITAL_ONE: 'capitalone', CITI_BANK: 'citibank', AMEX: 'americanexpressfdx', + MOCK_BANK: 'mockbank', }, AMEX_CUSTOM_FEED: { CORPORATE: 'American Express Corporate Cards', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1f6663d948cb5..55568e2f6440c 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -462,6 +462,7 @@ const getBankCardDetailsImage = (bank: ValueOf [CONST.COMPANY_CARDS.BANKS.WELLS_FARGO]: companyCardIllustrations.WellsFargoCompanyCardDetail, [CONST.COMPANY_CARDS.BANKS.BREX]: companyCardIllustrations.BrexCompanyCardDetail, [CONST.COMPANY_CARDS.BANKS.STRIPE]: companyCardIllustrations.StripeCompanyCardDetail, + [CONST.COMPANY_CARDS.BANKS.MOCK_BANK]: illustrations.GenericCompanyCard, [CONST.COMPANY_CARDS.BANKS.OTHER]: illustrations.GenericCompanyCard, }; return iconMap[bank]; diff --git a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx index 559e4bcdf2680..96bd9191a9a98 100644 --- a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx +++ b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx @@ -21,6 +21,7 @@ import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/t import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -66,20 +67,28 @@ function SelectBankStep() { setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE, data: {selectedBank: null}}); }; - const data = Object.values(CONST.COMPANY_CARDS.BANKS).map((bank) => ({ - value: bank, - text: bank === CONST.COMPANY_CARDS.BANKS.OTHER ? translate('workspace.companyCards.addNewCard.other') : bank, - keyForList: bank, - isSelected: bankSelected === bank, - leftElement: ( - - ), - })); + const data = Object.values(CONST.COMPANY_CARDS.BANKS) + .filter((bank) => { + // Only show Mock Bank in local development + if (bank === CONST.COMPANY_CARDS.BANKS.MOCK_BANK) { + return CONFIG.IS_USING_LOCAL_WEB; + } + return true; + }) + .map((bank) => ({ + value: bank, + text: bank === CONST.COMPANY_CARDS.BANKS.OTHER ? translate('workspace.companyCards.addNewCard.other') : bank, + keyForList: bank, + isSelected: bankSelected === bank, + leftElement: ( + + ), + })); const confirmButtonOptions = useMemo( () => ({ From a0245f7f649b4e5dd80558d549a7c46ff05fad94 Mon Sep 17 00:00:00 2001 From: Amy Evans Date: Fri, 16 Jan 2026 17:19:47 -0500 Subject: [PATCH 02/17] Add ability to break card connection for testing --- src/libs/API/parameters/UpdateCompanyCard.ts | 3 +++ src/libs/actions/CompanyCards.ts | 10 ++++++---- .../WorkspaceCompanyCardDetailsPage.tsx | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/libs/API/parameters/UpdateCompanyCard.ts b/src/libs/API/parameters/UpdateCompanyCard.ts index 71bf8fd89adfd..cc4d467733f4c 100644 --- a/src/libs/API/parameters/UpdateCompanyCard.ts +++ b/src/libs/API/parameters/UpdateCompanyCard.ts @@ -1,5 +1,8 @@ type UpdateCompanyCard = { cardID: number; + + /** JSONCode error to simulate (e.g., 434 for broken connection). Only works in staging/dev. */ + breakConnection?: number; }; export default UpdateCompanyCard; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 5300d7625f4a0..afdc7a8118213 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -495,7 +495,7 @@ function resetFailedWorkspaceCompanyCardAssignment(domainOrWorkspaceAccountID: n }); } -function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: string, bankName: CompanyCardFeed, lastScrapeResult?: number) { +function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: string, bankName: CompanyCardFeed, lastScrapeResult?: number, breakConnection?: boolean) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -537,7 +537,6 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${bankName}`, value: { [cardID]: { - lastScrapeResult: CONST.JSON_CODE.SUCCESS, isLoadingLastUpdated: false, pendingFields: { lastScrape: null, @@ -550,7 +549,6 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: key: ONYXKEYS.CARD_LIST, value: { [cardID]: { - lastScrapeResult: CONST.JSON_CODE.SUCCESS, isLoadingLastUpdated: false, pendingFields: { lastScrape: null, @@ -595,10 +593,14 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: }, ]; - const parameters = { + const parameters: {cardID: number; breakConnection?: number} = { cardID: Number(cardID), }; + if (breakConnection) { + parameters.breakConnection = 434; + } + API.write(WRITE_COMMANDS.UPDATE_COMPANY_CARD, parameters, {optimisticData, finallyData, failureData}); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index 6f1d9be70cf3d..5a05a80404210 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -37,6 +37,7 @@ import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import {clearCompanyCardErrorField, unassignWorkspaceCompanyCard, updateWorkspaceCompanyCard} from '@userActions/CompanyCards'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -104,6 +105,13 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult); }; + const breakConnection = () => { + updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult, true); + }; + + // Show "Break connection" option only in non-production environments (staging/dev) + const shouldShowBreakConnection = CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.PRODUCTION; + const lastScrape = useMemo(() => { if (!card?.lastScrape) { return translate('workspace.moreFeatures.companyCards.neverUpdated'); @@ -255,6 +263,14 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag onPress={updateCard} /> + {shouldShowBreakConnection && ( + + )} Date: Fri, 16 Jan 2026 17:41:02 -0500 Subject: [PATCH 03/17] Refine conditions under which we show test options --- .../companyCards/WorkspaceCompanyCardDetailsPage.tsx | 5 +++-- src/pages/workspace/companyCards/addNew/SelectBankStep.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index 5a05a80404210..b3173db647ce1 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -109,8 +109,9 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult, true); }; - // Show "Break connection" option only in non-production environments (staging/dev) - const shouldShowBreakConnection = CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.PRODUCTION; + // Show "Break connection" option only for Mock Bank cards in non-production environments + const isMockBank = bank?.includes(CONST.COMPANY_CARDS.BANK_CONNECTIONS.MOCK_BANK); + const shouldShowBreakConnection = isMockBank && CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.PRODUCTION; const lastScrape = useMemo(() => { if (!card?.lastScrape) { diff --git a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx index 96bd9191a9a98..5820d3bbf7e30 100644 --- a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx +++ b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx @@ -35,6 +35,7 @@ function SelectBankStep() { const {isOffline} = useNetwork(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); + const [isDebugModeEnabled = false] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true}); const [bankSelected, setBankSelected] = useState | null>(); const [hasError, setHasError] = useState(false); const isOtherBankSelected = bankSelected === CONST.COMPANY_CARDS.BANKS.OTHER; @@ -69,9 +70,9 @@ function SelectBankStep() { const data = Object.values(CONST.COMPANY_CARDS.BANKS) .filter((bank) => { - // Only show Mock Bank in local development + // Only show Mock Bank when Debug Mode is active and not in production if (bank === CONST.COMPANY_CARDS.BANKS.MOCK_BANK) { - return CONFIG.IS_USING_LOCAL_WEB; + return isDebugModeEnabled && CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.PRODUCTION; } return true; }) From fb2f0c1e93565ba1c59c9eff5dc6e0b61b3129b7 Mon Sep 17 00:00:00 2001 From: Amy Evans Date: Mon, 19 Jan 2026 11:38:22 -0500 Subject: [PATCH 04/17] Add Mock Bank in additional lists --- src/CONST/index.ts | 1 + src/libs/CardUtils.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3b7684bdd9069..07c2f59e5a15c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3468,6 +3468,7 @@ const CONST = { AMEX_DIRECT: 'oauth.americanexpressfdx.com', AMEX_FILE_DOWNLOAD: 'americanexpressfd.us', CSV: 'ccupload', + MOCK_BANK: 'oauth.mockbank.com', }, FEED_KEY_SEPARATOR: '#', CARD_NUMBER_MASK_CHAR: 'X', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 55568e2f6440c..2ff6c47d60a95 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -358,6 +358,7 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD [CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: companyCardIllustrations.StripeCompanyCardDetailLarge, [CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]: illustrations.GenericCSVCompanyCardLarge, [CONST.COMPANY_CARD.FEED_BANK_NAME.PEX]: illustrations.GenericCompanyCardLarge, + [CONST.COMPANY_CARD.FEED_BANK_NAME.MOCK_BANK]: illustrations.GenericCompanyCardLarge, [CONST.EXPENSIFY_CARD.BANK]: Illustrations.ExpensifyCardImage, }; @@ -436,6 +437,7 @@ function getBankName(feedType: CompanyCardFeed): string { [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_1205]: 'American Express', [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_FILE_DOWNLOAD]: 'American Express', [CONST.COMPANY_CARD.FEED_BANK_NAME.PEX]: 'PEX', + [CONST.COMPANY_CARD.FEED_BANK_NAME.MOCK_BANK]: CONST.COMPANY_CARDS.BANKS.MOCK_BANK, }; // In existing OldDot setups other variations of feeds could exist, ex: vcf2, vcf3, oauth.americanexpressfdx.com 2003 From d9a4546998101738fcc9163a0139333035263d6a Mon Sep 17 00:00:00 2001 From: Amy Evans Date: Tue, 20 Jan 2026 09:39:15 -0500 Subject: [PATCH 05/17] Rename --- src/CONST/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 07c2f59e5a15c..0b0587f2036f7 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3581,7 +3581,7 @@ const CONST = { CITI_BANK: 'Citibank', STRIPE: 'Stripe', WELLS_FARGO: 'Wells Fargo', - MOCK_BANK: 'Mock Bank (Local Testing)', + MOCK_BANK: 'Mock Bank', OTHER: 'Other', }, BANK_CONNECTIONS: { From b4e6a3d5f3e47d82b2260dcd6ca4cdf7981d7a72 Mon Sep 17 00:00:00 2001 From: Amy Evans Date: Tue, 27 Jan 2026 18:36:37 -0800 Subject: [PATCH 06/17] Add comment --- src/libs/actions/CompanyCards.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index afdc7a8118213..2d7ad86c42002 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -598,6 +598,7 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID: }; if (breakConnection) { + // Simulate "Account not found" error code for testing parameters.breakConnection = 434; } From 3556e9fbb1e724eb48d9499196e1f8ea4b041d39 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 3 Feb 2026 07:23:17 +0800 Subject: [PATCH 07/17] Fix: Blank distance field is shown when submitting expense with zero distance --- src/components/MoneyRequestConfirmationList.tsx | 12 +++++++++++- .../MoneyRequestConfirmationListFooter.tsx | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/DistanceRequestUtils.ts | 8 +++++--- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index cd65ed3993010..c27a82c7243a5 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -884,7 +884,17 @@ function MoneyRequestConfirmationList({ */ setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit, getCurrencySymbol); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant( + hasRoute, + distance, + unit, + rate ?? 0, + currency ?? CONST.CURRENCY.USD, + translate, + toLocaleDigit, + getCurrencySymbol, + isManualDistanceRequest, + ); setMoneyRequestMerchant(transactionID, distanceMerchant, true); }, [ isDistanceRequestWithPendingRoute, diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index b1b7c6597e414..7a48a93b8e41c 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -515,7 +515,7 @@ function MoneyRequestConfirmationListFooter({ Date: Tue, 3 Feb 2026 07:44:06 +0800 Subject: [PATCH 08/17] lint --- src/components/MoneyRequestConfirmationList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index c27a82c7243a5..b1dd9e2f17dd1 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -913,6 +913,7 @@ function MoneyRequestConfirmationList({ isReadOnly, isMovingTransactionFromTrackExpense, getCurrencySymbol, + isManualDistanceRequest, ]); // Auto select the category if there is only one enabled category and it is required From 7528435ca2b368629113aa540290f3cb6198ace8 Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Tue, 3 Feb 2026 08:19:44 +0800 Subject: [PATCH 09/17] lint fix --- src/CONST/index.ts | 2 ++ src/components/MoneyRequestConfirmationList.tsx | 1 + src/components/MoneyRequestConfirmationListFooter.tsx | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b4db81c116559..cdecc0a1aeed2 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8105,6 +8105,8 @@ const CONST = { PURE_REPORT_ACTION_ITEM: 'Report-PureReportActionItem', MODERATION_BUTTON: 'Report-ModerationButton', MONEY_REQUEST_REPORT_ACTIONS_LIST_SELECT_ALL: 'MoneyRequestReportActionsList-SelectAll', + MONEY_REQUEST_CONFIRMATION_RESET_SPLIT: 'MoneyRequestConfirmation-ResetSplit', + MONEY_REQUEST_CONFIRMATION_RECEIPT_THUMBNAIL: 'MoneyRequestConfirmation-ReceiptThumbnail', }, SIDEBAR: { SIGN_IN_BUTTON: 'Sidebar-SignInButton', diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b1dd9e2f17dd1..5ed431d289626 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -802,6 +802,7 @@ function MoneyRequestConfirmationList({ accessibilityLabel={CONST.ROLE.BUTTON} role={CONST.ROLE.BUTTON} shouldUseAutoHitSlop + sentryLabel={CONST.SENTRY_LABEL.REPORT.MONEY_REQUEST_CONFIRMATION_RESET_SPLIT} > {translate('common.reset')} diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 7a48a93b8e41c..35d0b59654636 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -963,6 +963,7 @@ function MoneyRequestConfirmationListFooter({ disabled={!shouldDisplayReceipt} disabledStyle={styles.cursorDefault} style={styles.h100} + sentryLabel={CONST.SENTRY_LABEL.REPORT.MONEY_REQUEST_CONFIRMATION_RECEIPT_THUMBNAIL} > Date: Thu, 5 Feb 2026 22:15:21 +0800 Subject: [PATCH 10/17] lint fix --- src/CONST/index.ts | 1 - src/components/MoneyRequestConfirmationListFooter.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ba63c901db05c..dfa2ef7ab86b0 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8097,7 +8097,6 @@ const CONST = { PURE_REPORT_ACTION_ITEM: 'Report-PureReportActionItem', MODERATION_BUTTON: 'Report-ModerationButton', MONEY_REQUEST_REPORT_ACTIONS_LIST_SELECT_ALL: 'MoneyRequestReportActionsList-SelectAll', - MONEY_REQUEST_CONFIRMATION_RECEIPT_THUMBNAIL: 'MoneyRequestConfirmation-ReceiptThumbnail', }, SIDEBAR: { SIGN_IN_BUTTON: 'Sidebar-SignInButton', diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 8dcf8bc22056b..a84ee387b2926 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -966,7 +966,6 @@ function MoneyRequestConfirmationListFooter({ disabled={!shouldDisplayReceipt} disabledStyle={styles.cursorDefault} style={styles.h100} - sentryLabel={CONST.SENTRY_LABEL.REPORT.MONEY_REQUEST_CONFIRMATION_RECEIPT_THUMBNAIL} > Date: Fri, 6 Feb 2026 19:30:07 +0800 Subject: [PATCH 11/17] Add unit test for getDistanceForDisplay and getDistanceMerchant --- tests/unit/DistanceRequestUtilsTest.ts | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/unit/DistanceRequestUtilsTest.ts b/tests/unit/DistanceRequestUtilsTest.ts index d2ec4c6037d92..8d5bde2e93642 100644 --- a/tests/unit/DistanceRequestUtilsTest.ts +++ b/tests/unit/DistanceRequestUtilsTest.ts @@ -2,6 +2,7 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import CONST from '@src/CONST'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; +import {translateLocal} from '../utils/TestHelper'; const FAKE_POLICY: Policy = { id: 'CEEEDB0EC660F71A', @@ -140,4 +141,39 @@ describe('DistanceRequestUtils', () => { expect(result).toBe('B593F3FBBB0BD'); }); }); + + describe('getDistanceForDisplay', () => { + it('returns empty string when distance is 0 and isManualDistanceRequest is false', () => { + const result = DistanceRequestUtils.getDistanceForDisplay(true, 0, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, 67, translateLocal, false, false); + expect(result).toBe(''); + }); + + it('formats zero distance when isManualDistanceRequest is true', () => { + const result = DistanceRequestUtils.getDistanceForDisplay(true, 0, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, 67, translateLocal, false, true); + expect(result).toBe(`0.00 ${translateLocal('common.miles')}`); + }); + }); + + describe('getDistanceMerchant', () => { + const toLocaleDigitMock = (dot: string): string => dot; + const getCurrencySymbolMock = (currency: string): string | undefined => { + if (currency === 'USD') return '$'; + return undefined; + }; + + it('formats zero distance when isManualDistanceRequest is true', () => { + const result = DistanceRequestUtils.getDistanceMerchant( + true, + 0, + CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + 67, + 'USD', + translateLocal, + toLocaleDigitMock, + getCurrencySymbolMock, + true, + ); + expect(result).toBe('0.00 mi @ $0.67 / mi'); + }); + }); }); From 4b5fa1dc281aafa14d31b7f62546a5257eb0062d Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 6 Feb 2026 19:49:14 +0800 Subject: [PATCH 12/17] lint fix --- tests/unit/DistanceRequestUtilsTest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/DistanceRequestUtilsTest.ts b/tests/unit/DistanceRequestUtilsTest.ts index 8d5bde2e93642..786365a15ecc4 100644 --- a/tests/unit/DistanceRequestUtilsTest.ts +++ b/tests/unit/DistanceRequestUtilsTest.ts @@ -157,7 +157,9 @@ describe('DistanceRequestUtils', () => { describe('getDistanceMerchant', () => { const toLocaleDigitMock = (dot: string): string => dot; const getCurrencySymbolMock = (currency: string): string | undefined => { - if (currency === 'USD') return '$'; + if (currency === 'USD') { + return '$'; + } return undefined; }; From 8fbee8b42f8dec1577c7dfbc87726852b8c78b0e Mon Sep 17 00:00:00 2001 From: I Nyoman Jyotisa Date: Fri, 6 Feb 2026 23:41:21 +0800 Subject: [PATCH 13/17] Pass isManualDistanceRequest to getDistanceMerchant calls --- src/libs/TransactionUtils/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index ad1c65337f444..ffa0471c21860 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -732,6 +732,7 @@ function getUpdatedTransaction({ translateLocal, (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), getCurrencySymbol, + isManualDistanceRequest(transaction), ); updatedTransaction.amount = updatedAmount; @@ -780,6 +781,7 @@ function getUpdatedTransaction({ translateLocal, (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), getCurrencySymbol, + isManualDistanceRequest(transaction), ); updatedTransaction.amount = updatedAmount; @@ -864,6 +866,7 @@ function getUpdatedTransaction({ translateLocal, (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), getCurrencySymbol, + isManualDistanceRequest(transaction), ); updatedTransaction.modifiedAmount = amount; @@ -1133,6 +1136,7 @@ function getMerchant(transaction: OnyxInputOrEntry, policyParam: On translateLocal, (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), getCurrencySymbol, + isManualDistanceRequest(transaction), ); } } From db6a23d13ca3bd0ec1869655955e0857e13e36de Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 6 Feb 2026 09:57:40 -0700 Subject: [PATCH 14/17] First round of memory fixes --- tests/actions/IOUTest.ts | 45 ++++++--- tests/actions/OnyxUpdateManagerTest.ts | 11 ++- tests/actions/PolicyTest.ts | 7 +- tests/actions/ReportTest.ts | 57 ++++++++---- tests/actions/SessionTest.ts | 89 +++++++++--------- tests/ui/SessionTest.tsx | 7 +- tests/ui/UnreadIndicatorsTest.tsx | 70 +++++++------- tests/unit/DistanceRateTest.ts | 10 +- tests/unit/NetworkTest.tsx | 123 +++++++++++++------------ tests/unit/OnyxUpdateManagerTest.ts | 11 ++- tests/unit/OptionsListUtilsTest.tsx | 28 ++++-- tests/unit/ReportUtilsGetIconsTest.ts | 11 ++- tests/unit/ReportUtilsTest.ts | 14 ++- tests/utils/getOnyxValue.ts | 7 +- 14 files changed, 303 insertions(+), 187 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 5950a415ff4a6..3e3847ae64c21 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -203,6 +203,11 @@ const VIT_ACCOUNT_ID = 4; const VIT_PARTICIPANT: Participant = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, role: 'member'}; OnyxUpdateManager(); +let trackedOnyxConnections: Array> = []; +const trackOnyxConnection = (connection: ReturnType) => { + trackedOnyxConnections.push(connection); + return connection; +}; describe('actions/IOU', () => { const currentUserPersonalDetails: CurrentUserPersonalDetails = { ...createPersonalDetails(RORY_ACCOUNT_ID), @@ -235,6 +240,10 @@ describe('actions/IOU', () => { }); afterEach(() => { + for (const connection of trackedOnyxConnections) { + Onyx.disconnect(connection); + } + trackedOnyxConnections = []; jest.clearAllMocks(); }); @@ -7289,10 +7298,12 @@ describe('actions/IOU', () => { expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), - }); + }), + ); await waitForBatchedUpdates(); @@ -7382,10 +7393,12 @@ describe('actions/IOU', () => { // Given a transaction thread thread = buildTransactionThread(createIOUAction, iouReport); - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), - }); + }), + ); await waitForBatchedUpdates(); @@ -7515,10 +7528,12 @@ describe('actions/IOU', () => { openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); await waitForBatchedUpdates(); - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), - }); + }), + ); await waitForBatchedUpdates(); await new Promise((resolve) => { @@ -7606,10 +7621,12 @@ describe('actions/IOU', () => { expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, callback: (val) => (reportActions = val), - }); + }), + ); await waitForBatchedUpdates(); jest.advanceTimersByTime(10); @@ -7682,10 +7699,12 @@ describe('actions/IOU', () => { // Given an added comment to the IOU report - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, callback: (val) => (reportActions = val), - }); + }), + ); await waitForBatchedUpdates(); jest.advanceTimersByTime(10); @@ -7769,10 +7788,12 @@ describe('actions/IOU', () => { it('update IOU report and reportPreview with new totals and messages if the IOU report is not deleted', async () => { await waitForBatchedUpdates(); - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, callback: (val) => (iouReport = val), - }); + }), + ); await waitForBatchedUpdates(); // Given a second expense in addition to the first one diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index 73ef3b799b84b..c75adcf7ab08d 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -130,14 +130,23 @@ OnyxUpdateManager(); describe('actions/OnyxUpdateManager', () => { let reportActions: OnyxEntry; + let reportActionsConnection: ReturnType | undefined; beforeAll(() => { Onyx.init({keys: ONYXKEYS}); - Onyx.connect({ + reportActionsConnection = Onyx.connect({ key: ONYX_KEY, callback: (val) => (reportActions = val), }); }); + afterAll(() => { + if (!reportActionsConnection) { + return; + } + Onyx.disconnect(reportActionsConnection); + reportActionsConnection = undefined; + }); + beforeEach(async () => { jest.clearAllMocks(); await Onyx.clear(); diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 9ce5e11b466e8..0a4bc9ac3d918 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1370,9 +1370,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); const violations = await new Promise>((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - callback: resolve, + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, }); }); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index dbf7a4050bac7..20fb251d7f8bd 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -133,6 +133,11 @@ jest.mock('@libs/actions/Welcome', () => ({ const originalXHR = HttpUtils.xhr; OnyxUpdateManager(); +let trackedOnyxConnections: Array> = []; +const trackOnyxConnection = (connection: ReturnType) => { + trackedOnyxConnections.push(connection); + return connection; +}; describe('actions/Report', () => { beforeAll(() => { PusherHelper.setup(); @@ -162,6 +167,10 @@ describe('actions/Report', () => { }); afterEach(() => { + for (const connection of trackedOnyxConnections) { + Onyx.disconnect(connection); + } + trackedOnyxConnections = []; jest.clearAllMocks(); PusherHelper.teardown(); }); @@ -185,10 +194,12 @@ describe('actions/Report', () => { }; let reportActions: OnyxEntry; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val), - }); + }), + ); // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -296,10 +307,12 @@ describe('actions/Report', () => { const REPORT_ID = '1'; let reportIsPinned: boolean; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, callback: (val) => (reportIsPinned = val?.isPinned ?? false), - }); + }), + ); // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -354,16 +367,20 @@ describe('actions/Report', () => { let report: OnyxEntry; let reportActionCreatedDate: string; let currentTime: string; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, callback: (val) => (report = val), - }); + }), + ); let reportActions: OnyxTypes.ReportActions; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val ?? {}), - }); + }), + ); const {result: ancestors, rerender} = renderHook(() => useAncestors(report)); @@ -740,17 +757,21 @@ describe('actions/Report', () => { }; let reportActions: OnyxTypes.ReportActions; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val ?? {}), - }); + }), + ); const reportActionsReactions: OnyxCollection = {}; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { reportActionsReactions[key] = val ?? {}; }, - }); + }), + ); let reportAction: OnyxTypes.ReportAction | undefined; let reportActionID: string | undefined; @@ -869,17 +890,21 @@ describe('actions/Report', () => { }; let reportActions: OnyxTypes.ReportActions = {}; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val ?? {}), - }); + }), + ); const reportActionsReactions: OnyxCollection = {}; - Onyx.connect({ + trackOnyxConnection( + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { reportActionsReactions[key] = val ?? {}; }, - }); + }), + ); let resultAction: OnyxTypes.ReportAction | undefined; diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index cbfed8d162e03..28ad789740c86 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -55,62 +55,67 @@ describe('Session', () => { const TEST_REFRESHED_AUTH_TOKEN = 'refreshedAuthToken'; let credentials: OnyxEntry; - Onyx.connect({ + const credentialsConnection = Onyx.connect({ key: ONYXKEYS.CREDENTIALS, callback: (val) => (credentials = val), }); let session: OnyxEntry; - Onyx.connect({ + const sessionConnection = Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => (session = val), }); - // When we sign in with the test user - await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN); - await waitForBatchedUpdates(); + try { + // When we sign in with the test user + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN); + await waitForBatchedUpdates(); - // Then our re-authentication credentials should be generated and our session data - // have the correct information + initial authToken. - expect(credentials?.login).toBe(TEST_USER_LOGIN); - expect(credentials?.autoGeneratedLogin).not.toBeUndefined(); - expect(credentials?.autoGeneratedPassword).not.toBeUndefined(); - expect(session?.authToken).toBe(TEST_INITIAL_AUTH_TOKEN); - expect(session?.accountID).toBe(TEST_USER_ACCOUNT_ID); - expect(session?.email).toBe(TEST_USER_LOGIN); - - // At this point we have an authToken. To simulate it expiring we'll just make another - // request and mock the response so it returns 407. Once this happens we should attempt - // to Re-Authenticate with the stored credentials. Our next call will be to Authenticate - // so we will mock that response with a new authToken and then verify that Onyx has our - // data. - (HttpUtils.xhr as jest.MockedFunction) + // Then our re-authentication credentials should be generated and our session data + // have the correct information + initial authToken. + expect(credentials?.login).toBe(TEST_USER_LOGIN); + expect(credentials?.autoGeneratedLogin).not.toBeUndefined(); + expect(credentials?.autoGeneratedPassword).not.toBeUndefined(); + expect(session?.authToken).toBe(TEST_INITIAL_AUTH_TOKEN); + expect(session?.accountID).toBe(TEST_USER_ACCOUNT_ID); + expect(session?.email).toBe(TEST_USER_LOGIN); + + // At this point we have an authToken. To simulate it expiring we'll just make another + // request and mock the response so it returns 407. Once this happens we should attempt + // to Re-Authenticate with the stored credentials. Our next call will be to Authenticate + // so we will mock that response with a new authToken and then verify that Onyx has our + // data. + (HttpUtils.xhr as jest.MockedFunction) - // This will make the call to OpenApp below return with an expired session code - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) + // This will make the call to OpenApp below return with an expired session code + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, + }), + ) - // The next call should be Authenticate since we are re-authenticating - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - accountID: TEST_USER_ACCOUNT_ID, - authToken: TEST_REFRESHED_AUTH_TOKEN, - email: TEST_USER_LOGIN, - }), - ); + // The next call should be Authenticate since we are re-authenticating + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.SUCCESS, + accountID: TEST_USER_ACCOUNT_ID, + authToken: TEST_REFRESHED_AUTH_TOKEN, + email: TEST_USER_LOGIN, + }), + ); - // When we attempt to fetch the initial app data via the API - confirmReadyToOpenApp(); - openApp(); - await waitForBatchedUpdates(); + // When we attempt to fetch the initial app data via the API + confirmReadyToOpenApp(); + openApp(); + await waitForBatchedUpdates(); - // Then it should fail and reauthenticate the user adding the new authToken to the session - // data in Onyx - expect(session?.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN); + // Then it should fail and reauthenticate the user adding the new authToken to the session + // data in Onyx + expect(session?.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN); + } finally { + Onyx.disconnect(credentialsConnection); + Onyx.disconnect(sessionConnection); + } }); test('Push notifications are subscribed after signing in', async () => { diff --git a/tests/ui/SessionTest.tsx b/tests/ui/SessionTest.tsx index 00221d6945fa5..8d0145a7e397b 100644 --- a/tests/ui/SessionTest.tsx +++ b/tests/ui/SessionTest.tsx @@ -54,6 +54,7 @@ function getInitialURL() { describe('Deep linking', () => { let lastVisitedPath: string | undefined; + let lastVisitedPathConnection: ReturnType | undefined; let originalSignInWithShortLivedAuthToken: typeof Session.signInWithShortLivedAuthToken; let originalOpenApp: typeof AppActions.openApp; @@ -63,7 +64,7 @@ describe('Deep linking', () => { }); beforeEach(() => { - Onyx.connect({ + lastVisitedPathConnection = Onyx.connect({ key: ONYXKEYS.LAST_VISITED_PATH, callback: (val: OnyxEntry) => (lastVisitedPath = val), }); @@ -100,6 +101,10 @@ describe('Deep linking', () => { }); afterEach(async () => { + if (lastVisitedPathConnection) { + Onyx.disconnect(lastVisitedPathConnection); + lastVisitedPathConnection = undefined; + } await Onyx.clear(); await waitForNetworkPromises(); jest.clearAllMocks(); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index c800a7736764a..fec3ebadfacde 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -548,7 +548,7 @@ describe('Unread Indicators', () => { it('Displays the correct chat message preview in the LHN when a comment is added then deleted', () => { let reportActions: OnyxEntry; let lastReportAction: ReportAction | undefined; - Onyx.connect({ + const reportActionsConnection = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val), }); @@ -593,6 +593,7 @@ describe('Unread Indicators', () => { expect(alternateText).toHaveLength(1); expect(screen.getAllByText('Comment 9').at(0)).toBeOnTheScreen(); }) + .finally(() => Onyx.disconnect(reportActionsConnection)) ); }); @@ -700,42 +701,45 @@ describe('Unread Indicators', () => { }; let recentWaypoints: RecentWaypoint[] = []; - Onyx.connect({ + const recentWaypointsConnection = Onyx.connect({ key: ONYXKEYS.NVP_RECENT_WAYPOINTS, callback: (val) => (recentWaypoints = val ?? []), }); + try { + // When the user track an expense on the self DM + const participant = {login: USER_A_EMAIL, accountID: USER_A_ACCOUNT_ID}; + trackExpense({ + report: selfDMReport, + isDraftPolicy: true, + action: CONST.IOU.ACTION.CREATE, + participantParams: { + payeeEmail: participant.login, + payeeAccountID: participant.accountID, + participant, + }, + transactionParams: { + amount: fakeTransaction.amount, + currency: fakeTransaction.currency, + created: format(new Date(), CONST.DATE.FNS_FORMAT_STRING), + }, + isASAPSubmitBetaEnabled: true, + currentUserAccountIDParam: USER_A_ACCOUNT_ID, + currentUserEmailParam: USER_A_EMAIL, + introSelected: undefined, + activePolicyID: undefined, + quickAction: undefined, + recentWaypoints, + betas: [CONST.BETAS.ALL], + }); + await waitForBatchedUpdates(); - // When the user track an expense on the self DM - const participant = {login: USER_A_EMAIL, accountID: USER_A_ACCOUNT_ID}; - trackExpense({ - report: selfDMReport, - isDraftPolicy: true, - action: CONST.IOU.ACTION.CREATE, - participantParams: { - payeeEmail: participant.login, - payeeAccountID: participant.accountID, - participant, - }, - transactionParams: { - amount: fakeTransaction.amount, - currency: fakeTransaction.currency, - created: format(new Date(), CONST.DATE.FNS_FORMAT_STRING), - }, - isASAPSubmitBetaEnabled: true, - currentUserAccountIDParam: USER_A_ACCOUNT_ID, - currentUserEmailParam: USER_A_EMAIL, - introSelected: undefined, - activePolicyID: undefined, - quickAction: undefined, - recentWaypoints, - betas: [CONST.BETAS.ALL], - }); - await waitForBatchedUpdates(); - - // Then the new line indicator shouldn't be displayed - const newMessageLineIndicatorHintText = TestHelper.translateLocal('accessibilityHints.newMessageLineIndicator'); - const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); - expect(unreadIndicator).toHaveLength(0); + // Then the new line indicator shouldn't be displayed + const newMessageLineIndicatorHintText = TestHelper.translateLocal('accessibilityHints.newMessageLineIndicator'); + const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); + expect(unreadIndicator).toHaveLength(0); + } finally { + Onyx.disconnect(recentWaypointsConnection); + } }); it('Mark the chat as unread on clicking "Mark as unread" on an item in LHN when the last message of the chat was deleted by another user', async () => { await signInAndGetAppWithUnreadChat(); diff --git a/tests/unit/DistanceRateTest.ts b/tests/unit/DistanceRateTest.ts index fa2970093db00..bf8b1ea12596c 100644 --- a/tests/unit/DistanceRateTest.ts +++ b/tests/unit/DistanceRateTest.ts @@ -82,9 +82,12 @@ describe('DistanceRate', () => { } await waitForBatchedUpdates(); const transactionViolations = await new Promise>((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - callback: resolve, + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, waitForCollectionCallback: true, }); }); @@ -145,11 +148,12 @@ describe('DistanceRate', () => { } await waitForBatchedUpdates(); const onyxPolicy = await new Promise((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}` as OnyxKey, // eslint-disable-next-line rulesdir/prefer-early-return callback: (value) => { if (value !== undefined) { + Onyx.disconnect(connectionID); resolve(value as Policy); } }, diff --git a/tests/unit/NetworkTest.tsx b/tests/unit/NetworkTest.tsx index 5ff64da41f10c..62f13bf31a1c2 100644 --- a/tests/unit/NetworkTest.tsx +++ b/tests/unit/NetworkTest.tsx @@ -155,7 +155,7 @@ describe('NetworkTests', () => { const NEW_AUTH_TOKEN = 'qwerty12345'; let sessionState: OnyxEntry; - Onyx.connect({ + const sessionConnection = Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => (sessionState = val), }); @@ -199,7 +199,8 @@ describe('NetworkTests', () => { const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); expect(callsToAuthenticate.length).toBe(2); expect(sessionState?.authToken).toBe(NEW_AUTH_TOKEN); - }); + }) + .finally(() => Onyx.disconnect(sessionConnection)); }); test('failing to reauthenticate while offline should not log out user', async () => { @@ -210,70 +211,74 @@ describe('NetworkTests', () => { let sessionState: OnyxEntry; // Set up listeners for session and network state changes - Onyx.connect({ + const sessionConnection = Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => (sessionState = val), }); - // Sign in test user and wait for updates - await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - await Onyx.set(ONYXKEYS.HAS_LOADED_APP, true); - await waitForBatchedUpdates(); + try { + // Sign in test user and wait for updates + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + await Onyx.set(ONYXKEYS.HAS_LOADED_APP, true); + await waitForBatchedUpdates(); - const initialAuthToken = sessionState?.authToken; - expect(initialAuthToken).toBeDefined(); + const initialAuthToken = sessionState?.authToken; + expect(initialAuthToken).toBeDefined(); - // Create a promise that we can resolve later to control the timing - let resolveAuthRequest: (value: unknown) => void = () => {}; - const pendingAuthRequest = new Promise((resolve) => { - resolveAuthRequest = resolve; - }); + // Create a promise that we can resolve later to control the timing + let resolveAuthRequest: (value: unknown) => void = () => {}; + const pendingAuthRequest = new Promise((resolve) => { + resolveAuthRequest = resolve; + }); - // 2. Mock Setup Phase - Configure XHR mocks for the test sequence - const mockedXhr = jest - .fn() - // First call: Return NOT_AUTHENTICATED to trigger reauthentication - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) - // Second call: Return a pending promise that we'll resolve later - .mockImplementationOnce(() => pendingAuthRequest); - - HttpUtils.xhr = mockedXhr; - - // 3. Test Execution Phase - Start with online network - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - - // Trigger reconnect which will fail due to expired token - confirmReadyToOpenApp(); - reconnectApp(); - await waitForBatchedUpdates(); - - // 4. First API Call Verification - Check ReconnectApp - const firstCall = mockedXhr.mock.calls.at(0) as [string, Record]; - expect(firstCall[0]).toBe('ReconnectApp'); - - // 5. Authentication Start - Verify authenticate was triggered - await waitForBatchedUpdates(); - const secondCall = mockedXhr.mock.calls.at(1) as [string, Record]; - expect(secondCall[0]).toBe('Authenticate'); - - // 6. Network State Change - Set offline and back online while authenticate is pending - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - - // 7.Trigger another reconnect due to network change - confirmReadyToOpenApp(); - reconnectApp(); - - // 8. Now fail the pending authentication request - resolveAuthRequest(Promise.reject(new Error('Network request failed'))); - await waitForBatchedUpdates(); // Now we wait for all updates after the auth request fails - - // 9. Verify the session remained intact and wasn't cleared - expect(sessionState?.authToken).toBe(initialAuthToken); + // 2. Mock Setup Phase - Configure XHR mocks for the test sequence + const mockedXhr = jest + .fn() + // First call: Return NOT_AUTHENTICATED to trigger reauthentication + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, + }), + ) + // Second call: Return a pending promise that we'll resolve later + .mockImplementationOnce(() => pendingAuthRequest); + + HttpUtils.xhr = mockedXhr; + + // 3. Test Execution Phase - Start with online network + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + // Trigger reconnect which will fail due to expired token + confirmReadyToOpenApp(); + reconnectApp(); + await waitForBatchedUpdates(); + + // 4. First API Call Verification - Check ReconnectApp + const firstCall = mockedXhr.mock.calls.at(0) as [string, Record]; + expect(firstCall[0]).toBe('ReconnectApp'); + + // 5. Authentication Start - Verify authenticate was triggered + await waitForBatchedUpdates(); + const secondCall = mockedXhr.mock.calls.at(1) as [string, Record]; + expect(secondCall[0]).toBe('Authenticate'); + + // 6. Network State Change - Set offline and back online while authenticate is pending + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + // 7.Trigger another reconnect due to network change + confirmReadyToOpenApp(); + reconnectApp(); + + // 8. Now fail the pending authentication request + resolveAuthRequest(Promise.reject(new Error('Network request failed'))); + await waitForBatchedUpdates(); // Now we wait for all updates after the auth request fails + + // 9. Verify the session remained intact and wasn't cleared + expect(sessionState?.authToken).toBe(initialAuthToken); + } finally { + Onyx.disconnect(sessionConnection); + } }); test('consecutive API calls eventually succeed when authToken is expired', () => { diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index a5b620b62ce70..74402c75c7559 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -51,13 +51,22 @@ const update8 = OnyxUpdateMockUtils.createUpdate(8); describe('OnyxUpdateManager', () => { let lastUpdateIDAppliedToClient = 1; + let lastUpdateConnection: ReturnType | undefined; beforeAll(() => { - Onyx.connect({ + lastUpdateConnection = Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, callback: (value) => (lastUpdateIDAppliedToClient = value ?? 1), }); }); + afterAll(() => { + if (!lastUpdateConnection) { + return; + } + Onyx.disconnect(lastUpdateConnection); + lastUpdateConnection = undefined; + }); + beforeEach(async () => { jest.clearAllMocks(); await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 156d27dfba20a..4409a5e385b02 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4612,20 +4612,24 @@ describe('OptionsListUtils', () => { }; let reportNameValuePair: OnyxEntry; - Onyx.connect({ + const reportNameValuePairConnection = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${participant.reportID}`, waitForCollectionCallback: false, callback: (value) => { reportNameValuePair = value; }, }); - await waitForBatchedUpdates(); + try { + await waitForBatchedUpdates(); - const option = getReportOption(participant, reportNameValuePair?.private_isArchived, POLICY, CURRENT_USER_ACCOUNT_ID, {}); + const option = getReportOption(participant, reportNameValuePair?.private_isArchived, POLICY, CURRENT_USER_ACCOUNT_ID, {}); - expect(option.text).toBe(POLICY.name); - expect(option.alternateText).toBeTruthy(); - expect(option.alternateText === translateLocal('workspace.common.workspace') || option.alternateText?.includes('Submits to')).toBe(true); + expect(option.text).toBe(POLICY.name); + expect(option.alternateText).toBeTruthy(); + expect(option.alternateText === translateLocal('workspace.common.workspace') || option.alternateText?.includes('Submits to')).toBe(true); + } finally { + Onyx.disconnect(reportNameValuePairConnection); + } }); it('should handle draft reports', async () => { @@ -4656,18 +4660,22 @@ describe('OptionsListUtils', () => { }; let reportNameValuePair: OnyxEntry; - Onyx.connect({ + const reportNameValuePairConnection = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${participant.reportID}`, waitForCollectionCallback: false, callback: (value) => { reportNameValuePair = value; }, }); - await waitForBatchedUpdates(); + try { + await waitForBatchedUpdates(); - const option = getReportOption(participant, reportNameValuePair?.private_isArchived, POLICY, CURRENT_USER_ACCOUNT_ID, {}, undefined, draftReports); + const option = getReportOption(participant, reportNameValuePair?.private_isArchived, POLICY, CURRENT_USER_ACCOUNT_ID, {}, undefined, draftReports); - expect(option.isDisabled).toBe(true); + expect(option.isDisabled).toBe(true); + } finally { + Onyx.disconnect(reportNameValuePairConnection); + } }); }); diff --git a/tests/unit/ReportUtilsGetIconsTest.ts b/tests/unit/ReportUtilsGetIconsTest.ts index f9723eb4c8076..afbf47aa3763c 100644 --- a/tests/unit/ReportUtilsGetIconsTest.ts +++ b/tests/unit/ReportUtilsGetIconsTest.ts @@ -105,6 +105,7 @@ const FAKE_POLICIES = { }; const currentUserAccountID = 5; +let privateDomainsConnection: ReturnType | undefined; beforeAll(() => { Onyx.init({ @@ -119,7 +120,15 @@ beforeAll(() => { }); // @ts-expect-error Until we add NVP_PRIVATE_DOMAINS to ONYXKEYS, we need to mock it here // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - Onyx.connect({key: ONYXKEYS.NVP_PRIVATE_DOMAINS, callback: () => {}}); + privateDomainsConnection = Onyx.connect({key: ONYXKEYS.NVP_PRIVATE_DOMAINS, callback: () => {}}); +}); + +afterAll(() => { + if (!privateDomainsConnection) { + return; + } + Onyx.disconnect(privateDomainsConnection); + privateDomainsConnection = undefined; }); describe('getIcons', () => { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 4e7ec819bd8d1..4001b248215a1 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4990,9 +4990,12 @@ describe('ReportUtils', () => { it('should return true for archived report', async () => { const reportNameValuePairs = await new Promise>((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, - callback: resolve, + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, }); }); expect(isArchivedReport(reportNameValuePairs)).toBe(true); @@ -5000,9 +5003,12 @@ describe('ReportUtils', () => { it('should return false for non-archived report', async () => { const reportNameValuePairs = await new Promise>((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${nonArchivedReport.reportID}`, - callback: resolve, + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, }); expect(isArchivedReport(reportNameValuePairs)).toBe(false); }); diff --git a/tests/utils/getOnyxValue.ts b/tests/utils/getOnyxValue.ts index 59f5f22d627de..66e3c9f5411f3 100644 --- a/tests/utils/getOnyxValue.ts +++ b/tests/utils/getOnyxValue.ts @@ -3,9 +3,12 @@ import type {KeyValueMapping, OnyxEntry, OnyxKey} from 'react-native-onyx'; export default function getOnyxValue(key: TKey): Promise> { return new Promise((resolve) => { - Onyx.connect({ + const connectionID = Onyx.connect({ key, - callback: (value) => resolve(value), + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, }); }); } From e6dba20679102474b398d428586580f0e506f0c9 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 6 Feb 2026 10:32:18 -0700 Subject: [PATCH 15/17] Round 2 of fixes --- tests/actions/ReportPreviewActionUtilsTest.ts | 2 +- tests/ui/SwitchToExpensifyClassicTest.tsx | 4 ++-- tests/unit/DateUtilsTest.ts | 4 ++-- tests/unit/DebugUtilsTest.ts | 12 +++--------- tests/unit/LoginUtilsTest.ts | 4 ++-- tests/unit/OnyxDerivedTest.tsx | 16 ++++++---------- tests/unit/PerDiemRequestUtilsTest.ts | 4 +--- tests/unit/PersistedRequests.ts | 2 +- tests/unit/ReportActionsUtilsTest.ts | 4 +--- tests/unit/ReportPrimaryActionUtilsTest.ts | 10 +++++----- tests/unit/ReportSecondaryActionUtilsTest.ts | 6 +++--- tests/unit/SidebarOrderTest.ts | 4 +--- tests/unit/canEditFieldOfMoneyRequestTest.ts | 6 ++---- tests/unit/hooks/useAllTransactions.test.ts | 4 ++-- tests/unit/hooks/useOriginalReportID.test.tsx | 4 ++-- tests/unit/usePolicyData.test.ts | 3 +-- tests/unit/useReportActionAvatarsTest.tsx | 4 +--- tests/unit/useTransactionViolationsTest.ts | 4 ++-- 18 files changed, 38 insertions(+), 59 deletions(-) diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index 4c1b2621ecf74..f80257624b705 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -55,7 +55,7 @@ describe('getReportPreviewAction', () => { }); beforeEach(async () => { - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS}); }); diff --git a/tests/ui/SwitchToExpensifyClassicTest.tsx b/tests/ui/SwitchToExpensifyClassicTest.tsx index baa38ec094807..58f0fef9030a9 100644 --- a/tests/ui/SwitchToExpensifyClassicTest.tsx +++ b/tests/ui/SwitchToExpensifyClassicTest.tsx @@ -64,9 +64,9 @@ function signInAppAndEnterTestFlow(dismissedValue?: boolean): Promise { } describe('Switch to Expensify Classic flow', () => { - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); // Unsubscribe to pusher channels PusherHelper.teardown(); diff --git a/tests/unit/DateUtilsTest.ts b/tests/unit/DateUtilsTest.ts index 623bb9069ac7f..8c12eafce4d62 100644 --- a/tests/unit/DateUtilsTest.ts +++ b/tests/unit/DateUtilsTest.ts @@ -46,10 +46,10 @@ describe('DateUtils', () => { return waitForBatchedUpdates(); }); - afterEach(() => { + afterEach(async () => { jest.restoreAllMocks(); jest.useRealTimers(); - Onyx.clear(); + await Onyx.clear(); }); const datetime = '2022-11-07 00:00:00'; diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 9f25b14510e88..07e7982742616 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -731,9 +731,7 @@ describe('DebugUtils', () => { keys: ONYXKEYS, }); }); - beforeEach(() => { - Onyx.clear(); - }); + beforeEach(() => Onyx.clear()); it('returns null when report is not defined', () => { const reason = DebugUtils.getReasonForShowingRowInLHN({ report: undefined, @@ -1001,9 +999,7 @@ describe('DebugUtils', () => { keys: ONYXKEYS, }); }); - beforeEach(() => { - Onyx.clear(); - }); + beforeEach(() => Onyx.clear()); it('returns undefined reason when report is not defined', () => { const {reason} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(undefined) ?? {}; expect(reason).toBeUndefined(); @@ -1177,9 +1173,7 @@ describe('DebugUtils', () => { }); }); describe('reportAction', () => { - beforeEach(() => { - Onyx.clear(); - }); + beforeEach(() => Onyx.clear()); it('returns undefined when report has no RBR', () => { const {reportAction} = DebugUtils.getReasonAndReportActionForRBRInLHNRow( diff --git a/tests/unit/LoginUtilsTest.ts b/tests/unit/LoginUtilsTest.ts index 6fdf6bc377c72..196234ec136ef 100644 --- a/tests/unit/LoginUtilsTest.ts +++ b/tests/unit/LoginUtilsTest.ts @@ -24,9 +24,9 @@ describe('LoginUtils', () => { return waitForBatchedUpdates(); }); - afterEach(() => { + afterEach(async () => { jest.useRealTimers(); - Onyx.clear(); + await Onyx.clear(); }); describe('getPhoneNumberWithoutSpecialChars', () => { it('Should return valid phone number', () => { diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index 572a66c90cf7c..68ae8315f3bb4 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -27,21 +27,17 @@ const renderLocaleContextProvider = () => { ); }; -const onyxDerivedTestSetup = () => { - Onyx.clear(); +const onyxDerivedTestSetup = async () => { + await Onyx.clear(); Onyx.init({keys: ONYXKEYS}); initOnyxDerivedValues(); }; describe('OnyxDerived', () => { - beforeEach(() => { - Onyx.clear(); - }); + beforeEach(() => Onyx.clear()); describe('reportAttributes', () => { - beforeAll(() => { - onyxDerivedTestSetup(); - }); + beforeAll(() => onyxDerivedTestSetup()); const mockReport: Report = { reportID: `test_1`, @@ -426,7 +422,7 @@ describe('OnyxDerived', () => { describe('nonPersonalAndWorkspaceCardList', () => { beforeAll(async () => { - onyxDerivedTestSetup(); + await onyxDerivedTestSetup(); }); it('returns empty object when dependencies are not set', async () => { @@ -532,7 +528,7 @@ describe('OnyxDerived', () => { describe('todos', () => { beforeAll(async () => { - onyxDerivedTestSetup(); + await onyxDerivedTestSetup(); }); const CURRENT_USER_ACCOUNT_ID = 1; diff --git a/tests/unit/PerDiemRequestUtilsTest.ts b/tests/unit/PerDiemRequestUtilsTest.ts index 48254edcfac64..617397e257f9c 100644 --- a/tests/unit/PerDiemRequestUtilsTest.ts +++ b/tests/unit/PerDiemRequestUtilsTest.ts @@ -28,9 +28,7 @@ describe('PerDiemRequestUtils', () => { }), ); - beforeEach(() => { - Onyx.clear(); - }); + beforeEach(() => Onyx.clear()); it('getDestinationListSections()', () => { const tokenizeSearch = 'Antigua Barbuda'; diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index a1938d592e6e6..ed122ccf74af8 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -27,7 +27,7 @@ beforeEach(() => { afterEach(() => { PersistedRequests.clear(); - Onyx.clear(); + return Onyx.clear(); }); describe('PersistedRequests', () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 24a06868b8767..39a33225a8386 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -57,9 +57,7 @@ describe('ReportActionsUtils', () => { }); // Clear out Onyx after each test so that each test starts with a clean slate - afterEach(() => { - Onyx.clear(); - }); + afterEach(() => Onyx.clear()); describe('getSortedReportActions', () => { const cases = [ diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index 3c3114d5a6feb..38cf9788a684d 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -57,7 +57,7 @@ describe('getPrimaryAction', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS}); }); @@ -939,7 +939,7 @@ describe('isReviewDuplicatesAction', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS}); }); @@ -1029,7 +1029,7 @@ describe('getTransactionThreadPrimaryAction', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS}); }); @@ -1276,7 +1276,7 @@ describe('getTransactionThreadPrimaryAction', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, {accountID: submitterAccountID}); }); @@ -1383,7 +1383,7 @@ describe('getTransactionThreadPrimaryAction', () => { const submitterEmail = 'submitter@example.com'; beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, {accountID: submitterAccountID, email: submitterEmail}); }); diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index f70d5126fc1ff..ee065533c2229 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -51,7 +51,7 @@ describe('getSecondaryAction', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS, [APPROVER_ACCOUNT_ID]: {accountID: APPROVER_ACCOUNT_ID, login: APPROVER_EMAIL}}); }); @@ -2320,7 +2320,7 @@ describe('getSecondaryExportReportActions', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS}); }); @@ -2585,7 +2585,7 @@ describe('getSecondaryTransactionThreadActions', () => { beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS}); }); diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 7daab412f0a1b..1a69022f5f5b4 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -72,9 +72,7 @@ describe('Sidebar', () => { }); // Clear out Onyx after each test so that each test starts with a clean slate - afterEach(() => { - Onyx.clear(); - }); + afterEach(() => Onyx.clear()); describe('in default mode', () => { it('is rendered with empty state when no reports are available', () => { diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index 0631ade1f5a9e..1ba75045ad172 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -99,8 +99,7 @@ describe('canEditFieldOfMoneyRequest', () => { }); afterEach(() => { - Onyx.clear(); - return waitForBatchedUpdates(); + return Onyx.clear().then(waitForBatchedUpdates); }); it('should return false for invoice report action if it is not outstanding report', async () => { @@ -198,8 +197,7 @@ describe('canEditFieldOfMoneyRequest', () => { }); afterEach(() => { - Onyx.clear(); - return waitForBatchedUpdates(); + return Onyx.clear().then(waitForBatchedUpdates); }); it('should return true for submitter of a distance request for amount and currency fields', async () => { diff --git a/tests/unit/hooks/useAllTransactions.test.ts b/tests/unit/hooks/useAllTransactions.test.ts index 0b4b323d6e797..9e4b25c812f82 100644 --- a/tests/unit/hooks/useAllTransactions.test.ts +++ b/tests/unit/hooks/useAllTransactions.test.ts @@ -21,9 +21,9 @@ describe('useAllTransactions', () => { }); }); - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); mockCurrentSearchResults = undefined; }); diff --git a/tests/unit/hooks/useOriginalReportID.test.tsx b/tests/unit/hooks/useOriginalReportID.test.tsx index b8c55405796a0..11f5b61b5fbaa 100644 --- a/tests/unit/hooks/useOriginalReportID.test.tsx +++ b/tests/unit/hooks/useOriginalReportID.test.tsx @@ -14,9 +14,9 @@ describe('useOriginalReportID', () => { }); }); - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); }); it('should return the reportID given a DEW routed action', async () => { diff --git a/tests/unit/usePolicyData.test.ts b/tests/unit/usePolicyData.test.ts index b946077d1a5a9..d824cc27ecb11 100644 --- a/tests/unit/usePolicyData.test.ts +++ b/tests/unit/usePolicyData.test.ts @@ -73,8 +73,7 @@ describe('usePolicyData', () => { }); beforeEach(() => { - Onyx.clear(); - return waitForBatchedUpdates(); + return Onyx.clear().then(waitForBatchedUpdates); }); test('returns data given a policy ID that exists in the onyx', async () => { diff --git a/tests/unit/useReportActionAvatarsTest.tsx b/tests/unit/useReportActionAvatarsTest.tsx index 87652879fbb95..db90680535562 100644 --- a/tests/unit/useReportActionAvatarsTest.tsx +++ b/tests/unit/useReportActionAvatarsTest.tsx @@ -36,9 +36,7 @@ describe('useReportActionAvatars', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${mockPolicy?.policyID}`, mockPolicy); }); - afterEach(() => { - Onyx.clear(); - }); + afterEach(() => Onyx.clear()); test.each( Object.values(CONST.REPORT.ACTIONS.TYPE) diff --git a/tests/unit/useTransactionViolationsTest.ts b/tests/unit/useTransactionViolationsTest.ts index 42e457646d874..61297c1240183 100644 --- a/tests/unit/useTransactionViolationsTest.ts +++ b/tests/unit/useTransactionViolationsTest.ts @@ -58,9 +58,9 @@ describe('useTransactionViolations', () => { }); }); - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); - Onyx.clear(); + await Onyx.clear(); // Default mock implementations (isViolationDismissed as jest.Mock).mockReturnValue(false); From 5e3987ac75bbd2a0a050e754d4b7a866fcfaabdd Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 6 Feb 2026 10:57:44 -0700 Subject: [PATCH 16/17] More memory tests --- .github/workflows/test.yml | 1 + jest.config.js | 3 ++ src/libs/actions/OnyxDerived/index.ts | 9 ++++ src/libs/actions/OnyxUpdateManager/index.ts | 52 ++++++++++++++------- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2dca9fb4d5678..996314517555c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: env: CI: true NODE_OPTIONS: "--experimental-vm-modules --max-old-space-size=8192" + JEST_WORKER_IDLE_MEMORY_LIMIT: "900MB" strategy: fail-fast: false matrix: diff --git a/jest.config.js b/jest.config.js index 904f49eb1f491..ea8ba9cdfc515 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,6 @@ const testFileExtension = 'ts?(x)'; +const workerIdleMemoryLimit = process.env.JEST_WORKER_IDLE_MEMORY_LIMIT ?? '900MB'; + module.exports = { preset: 'jest-expo', collectCoverageFrom: ['/src/**/*.{ts,tsx,js,jsx}', '!/src/**/__mocks__/**', '!/src/**/tests/**', '!**/*.d.ts'], @@ -28,6 +30,7 @@ module.exports = { doNotFake: ['nextTick'], }, testEnvironment: 'jsdom', + workerIdleMemoryLimit, setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.ts'], cacheDirectory: '/.jest-cache', diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index 6b1aca2fb4da0..8825678a3c1ff 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -15,11 +15,20 @@ import ONYX_DERIVED_VALUES from './ONYX_DERIVED_VALUES'; import type {DerivedValueContext} from './types'; import {setDerivedValue} from './utils'; +let isInitialized = false; + /** * Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change. * Using connectWithoutView in this function since this is only executed once while initializing the App. */ function init() { + if (isInitialized) { + Log.info('[OnyxDerived] init() called multiple times, skipping duplicate subscriptions'); + return; + } + + isInitialized = true; + for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) { let areAllConnectionsSet = false; let connectionsEstablishedCount = 0; diff --git a/src/libs/actions/OnyxUpdateManager/index.ts b/src/libs/actions/OnyxUpdateManager/index.ts index 3e485a5bda3b3..ffc9fefc67a70 100644 --- a/src/libs/actions/OnyxUpdateManager/index.ts +++ b/src/libs/actions/OnyxUpdateManager/index.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {Connection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {isClientTheLeader} from '@libs/ActiveClientManager'; import Log from '@libs/Log'; @@ -35,22 +35,33 @@ import { // Therefore, SaveResponseInOnyx.js can't import and use this file directly. let lastUpdateIDAppliedToClient: number = CONST.DEFAULT_NUMBER_ID; -// `lastUpdateIDAppliedToClient` is not dependent on any changes on the UI, -// so it is okay to use `connectWithoutView` here. -Onyx.connectWithoutView({ - key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value ?? CONST.DEFAULT_NUMBER_ID), -}); let isLoadingApp = false; -// `isLoadingApp` is not dependent on any changes on the UI, -// so it is okay to use `connectWithoutView` here. -Onyx.connectWithoutView({ - key: ONYXKEYS.IS_LOADING_APP, - callback: (value) => { - isLoadingApp = value ?? false; - }, -}); +let lastUpdateIDConnection: Connection | undefined; +let isLoadingAppConnection: Connection | undefined; +let updatesFromServerConnection: Connection | undefined; + +function ensureBaseSubscriptions() { + if (!lastUpdateIDConnection) { + // `lastUpdateIDAppliedToClient` is not dependent on any changes on the UI, + // so it is okay to use `connectWithoutView` here. + lastUpdateIDConnection = Onyx.connectWithoutView({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? CONST.DEFAULT_NUMBER_ID), + }); + } + + if (!isLoadingAppConnection) { + // `isLoadingApp` is not dependent on any changes on the UI, + // so it is okay to use `connectWithoutView` here. + isLoadingAppConnection = Onyx.connectWithoutView({ + key: ONYXKEYS.IS_LOADING_APP, + callback: (value) => { + isLoadingApp = value ?? false; + }, + }); + } +} let resolveQueryPromiseWrapper: () => void; const createQueryPromiseWrapper = () => @@ -85,6 +96,8 @@ function finalizeUpdatesAndResumeQueue() { * @returns a promise that resolves when all Onyx updates are done being processed */ function handleMissingOnyxUpdates(onyxUpdatesFromServer: OnyxEntry, clientLastUpdateID?: number): Promise { + ensureBaseSubscriptions(); + // If isLoadingApp is positive it means that OpenApp command hasn't finished yet, and in that case // we don't have base state of the app (reports, policies, etc.) setup. If we apply this update, // we'll only have them overwritten by the openApp response. So let's skip it and return. @@ -231,10 +244,17 @@ function updateAuthTokenIfNecessary(onyxUpdatesFromServer: OnyxEntry { + ensureBaseSubscriptions(); + + if (updatesFromServerConnection) { + console.debug('[OnyxUpdateManager] Already listening for updates from the server'); + return; + } + console.debug('[OnyxUpdateManager] Listening for updates from the server'); // `Onyx updates` are not dependent on any changes on the UI, // so it is okay to use `connectWithoutView` here. - Onyx.connectWithoutView({ + updatesFromServerConnection = Onyx.connectWithoutView({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, callback: (value) => { handleMissingOnyxUpdates(value); From 6f6e33f505cc4878ffbac1e9618a189f48a1bb70 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 6 Feb 2026 11:19:36 -0700 Subject: [PATCH 17/17] Mitigate Jest memory growth via worker idle memory limit --- .github/workflows/test.yml | 1 + jest.config.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2dca9fb4d5678..996314517555c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: env: CI: true NODE_OPTIONS: "--experimental-vm-modules --max-old-space-size=8192" + JEST_WORKER_IDLE_MEMORY_LIMIT: "900MB" strategy: fail-fast: false matrix: diff --git a/jest.config.js b/jest.config.js index 904f49eb1f491..ea8ba9cdfc515 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,6 @@ const testFileExtension = 'ts?(x)'; +const workerIdleMemoryLimit = process.env.JEST_WORKER_IDLE_MEMORY_LIMIT ?? '900MB'; + module.exports = { preset: 'jest-expo', collectCoverageFrom: ['/src/**/*.{ts,tsx,js,jsx}', '!/src/**/__mocks__/**', '!/src/**/tests/**', '!**/*.d.ts'], @@ -28,6 +30,7 @@ module.exports = { doNotFake: ['nextTick'], }, testEnvironment: 'jsdom', + workerIdleMemoryLimit, setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.ts'], cacheDirectory: '/.jest-cache',