diff --git a/ui/hooks/bridge/useTokenSearchResults.test.ts b/ui/hooks/bridge/useTokenSearchResults.test.ts index ba48e0ae573f..8d732a6c1b5d 100644 --- a/ui/hooks/bridge/useTokenSearchResults.test.ts +++ b/ui/hooks/bridge/useTokenSearchResults.test.ts @@ -1,4 +1,6 @@ import { formatChainIdToCaip } from '@metamask/bridge-controller'; +import { type CaipChainId } from '@metamask/utils'; +import { waitFor } from '@testing-library/react'; import { renderHookWithProvider } from '../../../test/lib/render-helpers-navigate'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { @@ -7,6 +9,7 @@ import { MOCK_EXTERNAL_SOLANA_ADDRESS, } from '../../../test/data/bridge/mock-bridge-store'; import { getAccountGroupsByAddress } from '../../selectors/multichain-accounts/account-tree'; +import { fetchTokensBySearchQuery } from '../../pages/bridge/utils/tokens'; import { useTokenSearchResults } from './useTokenSearchResults'; jest.mock('../../pages/bridge/utils/tokens', () => ({ @@ -22,7 +25,16 @@ jest.mock('../../store/actions', () => ({ getBearerToken: jest.fn().mockResolvedValue('mock-jwt'), })); +const mockFetchTokensBySearchQuery = + fetchTokensBySearchQuery as jest.MockedFunction< + typeof fetchTokensBySearchQuery + >; + describe('useTokenSearchResults', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('does not crash when accountAddress is an external address with no matching account group', () => { const mockStoreState = createBridgeMockStore({ featureFlagOverrides: { @@ -100,4 +112,70 @@ describe('useTokenSearchResults', () => { expect(result.current.isSearchResultsLoading).toBe(false); expect(result.current.hasMoreResults).toBe(false); }); + + it('refetches search results when chainIds change with the same search query', async () => { + const mockStoreState = createBridgeMockStore({ + featureFlagOverrides: { + bridgeConfig: { + refreshRate: 30000, + maxRefreshCount: 5, + support: true, + chains: { + [CHAIN_IDS.MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, + [CHAIN_IDS.POLYGON]: { + isActiveSrc: true, + isActiveDest: true, + }, + }, + chainRanking: [ + { chainId: formatChainIdToCaip(CHAIN_IDS.MAINNET) }, + { chainId: formatChainIdToCaip(CHAIN_IDS.POLYGON) }, + ], + }, + }, + }); + + const mainnetChainId = formatChainIdToCaip(CHAIN_IDS.MAINNET); + const polygonChainId = formatChainIdToCaip(CHAIN_IDS.POLYGON); + let chainIds = new Set([mainnetChainId]); + + const { rerender } = renderHookWithProvider( + () => + useTokenSearchResults({ + searchQuery: 'usdc', + // No owned assets match the query so stableMinimalAssetsString stays + // empty across network changes. chainIds must still trigger a refetch. + assetsToInclude: [], + chainIds, + }), + mockStoreState, + ); + + await waitFor(() => { + expect(mockFetchTokensBySearchQuery).toHaveBeenCalledTimes(1); + }); + expect(mockFetchTokensBySearchQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ + chainIds: [mainnetChainId], + query: 'usdc', + }), + ); + + mockFetchTokensBySearchQuery.mockClear(); + chainIds = new Set([polygonChainId]); + rerender(); + + await waitFor(() => { + expect(mockFetchTokensBySearchQuery).toHaveBeenCalledTimes(1); + }); + expect(mockFetchTokensBySearchQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ + chainIds: [polygonChainId], + query: 'usdc', + }), + ); + }); }); diff --git a/ui/hooks/bridge/useTokenSearchResults.ts b/ui/hooks/bridge/useTokenSearchResults.ts index 6432083f4b6c..d7685e1a3516 100644 --- a/ui/hooks/bridge/useTokenSearchResults.ts +++ b/ui/hooks/bridge/useTokenSearchResults.ts @@ -151,7 +151,7 @@ export const useTokenSearchResults = ({ // Debounce the initial fetch until the user stops typing debouncedFetchSearchResults(searchQuery, filteredAssetsToInclude); } - }, [searchQuery, stableMinimalAssetsString, jwt]); + }, [searchQuery, stableMinimalAssetsString, chainIds, jwt]); useEffect(() => { const debouncedFn = debouncedFetchSearchResults;