Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ac701cc
Add Mock Bank OAuth connection
amyevans Jan 16, 2026
a0245f7
Add ability to break card connection for testing
amyevans Jan 16, 2026
bb0df32
Refine conditions under which we show test options
amyevans Jan 16, 2026
fb2f0c1
Add Mock Bank in additional lists
amyevans Jan 19, 2026
d9a4546
Rename
amyevans Jan 20, 2026
b4e6a3d
Add comment
amyevans Jan 28, 2026
835761b
Merge branch 'main' into amy-mock-bank
amyevans Jan 30, 2026
3556e9f
Fix: Blank distance field is shown when submitting expense with zero …
nyomanjyotisa Feb 2, 2026
84eb326
lint
nyomanjyotisa Feb 2, 2026
7528435
lint fix
nyomanjyotisa Feb 3, 2026
dd49d9d
Merge branch 'main' into amy-mock-bank
amyevans Feb 4, 2026
ae56950
Merge branch 'main' into issue-80060
nyomanjyotisa Feb 5, 2026
ae887bc
lint fix
nyomanjyotisa Feb 5, 2026
836f33a
Merge branch 'main' into issue-80060
nyomanjyotisa Feb 5, 2026
34f5137
Add unit test for getDistanceForDisplay and getDistanceMerchant
nyomanjyotisa Feb 6, 2026
409ad46
Merge branch 'main' into issue-80060
nyomanjyotisa Feb 6, 2026
4b5fa1d
lint fix
nyomanjyotisa Feb 6, 2026
8fbee8b
Pass isManualDistanceRequest to getDistanceMerchant calls
nyomanjyotisa Feb 6, 2026
db6a23d
First round of memory fixes
AndrewGable Feb 6, 2026
e6dba20
Round 2 of fixes
AndrewGable Feb 6, 2026
446d560
Merge branch 'main' into andrew-jest-leak
AndrewGable Feb 6, 2026
fdc702a
Merge pull request #80391 from Expensify/amy-mock-bank
amyevans Feb 6, 2026
81d461d
Merge pull request #81229 from nyomanjyotisa/issue-80060
rlinoz Feb 6, 2026
5e3987a
More memory tests
AndrewGable Feb 6, 2026
6f6e33f
Mitigate Jest memory growth via worker idle memory limit
AndrewGable Feb 6, 2026
0d83801
Merge remote-tracking branch 'refs/remotes/origin/andrew-jest-leak' i…
AndrewGable Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const testFileExtension = 'ts?(x)';
const workerIdleMemoryLimit = process.env.JEST_WORKER_IDLE_MEMORY_LIMIT ?? '900MB';

module.exports = {
preset: 'jest-expo',
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}', '!<rootDir>/src/**/__mocks__/**', '!<rootDir>/src/**/tests/**', '!**/*.d.ts'],
Expand Down Expand Up @@ -28,6 +30,7 @@ module.exports = {
doNotFake: ['nextTick'],
},
testEnvironment: 'jsdom',
workerIdleMemoryLimit,
setupFiles: ['<rootDir>/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'],
setupFilesAfterEnv: ['<rootDir>/jest/setupAfterEnv.ts', '<rootDir>/tests/perf-test/setupAfterEnv.ts'],
cacheDirectory: '<rootDir>/.jest-cache',
Expand Down
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3524,6 +3524,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',
Expand Down Expand Up @@ -3641,6 +3642,7 @@ const CONST = {
CITI_BANK: 'Citibank',
STRIPE: 'Stripe',
WELLS_FARGO: 'Wells Fargo',
MOCK_BANK: 'Mock Bank',
OTHER: 'Other',
},
BANK_CONNECTIONS: {
Expand All @@ -3651,6 +3653,7 @@ const CONST = {
CAPITAL_ONE: 'capitalone',
CITI_BANK: 'citibank',
AMEX: 'americanexpressfdx',
MOCK_BANK: 'mockbank',
},
AMEX_CUSTOM_FEED: {
CORPORATE: 'American Express Corporate Cards',
Expand Down
13 changes: 12 additions & 1 deletion src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,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,
Expand All @@ -896,6 +906,7 @@ function MoneyRequestConfirmationList({
isReadOnly,
isMovingTransactionFromTrackExpense,
getCurrencySymbol,
isManualDistanceRequest,
]);

// Auto select the category if there is only one enabled category and it is required
Expand Down
2 changes: 1 addition & 1 deletion src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ function MoneyRequestConfirmationListFooter({
<MenuItemWithTopDescription
key={translate('common.distance')}
shouldShowRightIcon={!isReadOnly && !isGPSDistanceRequest}
title={DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate)}
title={DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate, undefined, isManualDistanceRequest)}
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ function MoneyRequestView({
let rateToDisplay = isCustomUnitOutOfPolicy
? translate('common.rateOutOfPolicy')
: DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, getCurrencySymbol, isOffline);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate, undefined, isManualDistanceRequest);
let merchantTitle = isEmptyMerchant ? '' : transactionMerchant;
let amountTitle = formattedTransactionAmount?.toString() || '';
if (isTransactionScanning) {
Expand Down
3 changes: 3 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,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,
};

Expand Down Expand Up @@ -466,6 +467,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
Expand All @@ -492,6 +494,7 @@ const getBankCardDetailsImage = (bank: ValueOf<typeof CONST.COMPANY_CARDS.BANKS>
[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];
Expand Down
8 changes: 5 additions & 3 deletions src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,13 @@ function getDistanceForDisplay(
rate: number | undefined,
translate: LocaleContextProps['translate'],
useShortFormUnit?: boolean,
isManualDistanceRequest?: boolean,
): string {
if (!hasRoute || !unit) {
return translate('iou.fieldPending');
}

if (!distanceInMeters) {
if (!distanceInMeters && !isManualDistanceRequest) {
return '';
}

Expand Down Expand Up @@ -222,16 +223,17 @@ function getDistanceMerchant(
translate: LocaleContextProps['translate'],
toLocaleDigit: LocaleContextProps['toLocaleDigit'],
getCurrencySymbol: CurrencyListContextProps['getCurrencySymbol'],
isManualDistanceRequest?: boolean,
): string {
if (!hasRoute || !rate) {
return translate('iou.fieldPending');
}

if (!distanceInMeters) {
if (!distanceInMeters && !isManualDistanceRequest) {
return '';
}

const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate, true);
const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate, true, isManualDistanceRequest);
const ratePerUnit = getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, getCurrencySymbol, undefined, true);

return `${distanceInUnits} ${CONST.DISTANCE_MERCHANT_SEPARATOR} ${ratePerUnit}`;
Expand Down
4 changes: 4 additions & 0 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ function getUpdatedTransaction({
translateLocal,
(digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit),
getCurrencySymbol,
isManualDistanceRequest(transaction),
);

updatedTransaction.amount = updatedAmount;
Expand Down Expand Up @@ -780,6 +781,7 @@ function getUpdatedTransaction({
translateLocal,
(digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit),
getCurrencySymbol,
isManualDistanceRequest(transaction),
);

updatedTransaction.amount = updatedAmount;
Expand Down Expand Up @@ -864,6 +866,7 @@ function getUpdatedTransaction({
translateLocal,
(digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit),
getCurrencySymbol,
isManualDistanceRequest(transaction),
);

updatedTransaction.modifiedAmount = amount;
Expand Down Expand Up @@ -1133,6 +1136,7 @@ function getMerchant(transaction: OnyxInputOrEntry<Transaction>, policyParam: On
translateLocal,
(digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit),
getCurrencySymbol,
isManualDistanceRequest(transaction),
);
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/libs/actions/CompanyCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ function resetFailedWorkspaceCompanyCardUnassignment(domainOrWorkspaceAccountID:
});
}

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<OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST | typeof ONYXKEYS.CARD_LIST>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -524,7 +524,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,
Expand All @@ -537,7 +536,6 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID:
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
lastScrapeResult: CONST.JSON_CODE.SUCCESS,
isLoadingLastUpdated: false,
pendingFields: {
lastScrape: null,
Expand Down Expand Up @@ -582,10 +580,15 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID:
},
];

const parameters = {
const parameters: {cardID: number; breakConnection?: number} = {
cardID: Number(cardID),
};

if (breakConnection) {
// Simulate "Account not found" error code for testing
parameters.breakConnection = 434;
}

API.write(WRITE_COMMANDS.SYNC_CARD, parameters, {optimisticData, finallyData, failureData});
}

Expand Down
9 changes: 9 additions & 0 deletions src/libs/actions/OnyxDerived/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 36 additions & 16 deletions src/libs/actions/OnyxUpdateManager/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 = () =>
Expand Down Expand Up @@ -85,6 +96,8 @@ function finalizeUpdatesAndResumeQueue() {
* @returns a promise that resolves when all Onyx updates are done being processed
*/
function handleMissingOnyxUpdates(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromServer>, clientLastUpdateID?: number): Promise<void> {
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.
Expand Down Expand Up @@ -231,10 +244,17 @@ function updateAuthTokenIfNecessary(onyxUpdatesFromServer: OnyxEntry<OnyxUpdates
}

export default () => {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -104,6 +105,14 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult);
};

const breakConnection = () => {
updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult, true);
};

// 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) {
return translate('workspace.moreFeatures.companyCards.neverUpdated');
Expand Down Expand Up @@ -255,6 +264,14 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
onPress={updateCard}
/>
</OfflineWithFeedback>
{shouldShowBreakConnection && (
<MenuItem
icon={Expensicons.Trashcan}
disabled={isOffline || card?.isLoadingLastUpdated}
title="Break connection (Testing)"
onPress={breakConnection}
/>
)}
<MenuItem
icon={expensifyIcons.RemoveMembers}
title={translate('workspace.moreFeatures.companyCards.unassignCard')}
Expand Down
Loading
Loading