From 02b04eddbca050456041d5bfdfdaa194424ecaf2 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Tue, 21 Jan 2025 19:45:12 -0700 Subject: [PATCH] feat: hooks and selectors for FE --- .../useTokenSearchDiscovery.test.ts | 67 +++++++++++++++++++ .../useTokenSearchDiscovery.ts | 21 ++++++ .../tokenSearchDiscoveryController.ts | 10 +++ app/selectors/types.ts | 3 + 4 files changed, 101 insertions(+) create mode 100644 app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.test.ts create mode 100644 app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.ts create mode 100644 app/selectors/tokenSearchDiscoveryController.ts diff --git a/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.test.ts b/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.test.ts new file mode 100644 index 00000000000..6a02fb5a51e --- /dev/null +++ b/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.test.ts @@ -0,0 +1,67 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import Engine from '../../../core/Engine'; +import useTokenSearchDiscovery from './useTokenSearchDiscovery'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +jest.mock('../../../core/Engine', () => ({ + context: { + TokenSearchDiscoveryController: { + searchTokens: jest.fn(), + }, + }, +})); + +describe('useTokenSearchDiscovery', () => { + const mockRecentSearches = ['0x123', '0x456']; + + beforeEach(() => { + jest.clearAllMocks(); + (useSelector as jest.Mock).mockReturnValue(mockRecentSearches); + }); + + it('should return searchTokens function and recent searches', () => { + const { result } = renderHook(() => useTokenSearchDiscovery()); + + expect(result.current.searchTokens).toBeDefined(); + expect(result.current.recentSearches).toEqual(mockRecentSearches); + }); + + it('should call TokenSearchDiscoveryController.searchTokens with correct params', async () => { + const mockSearchParams = { + chainId: '0x1', + query: 'DAI', + limit: '10', + }; + const mockSearchResult = { tokens: [] }; + + ( + Engine.context.TokenSearchDiscoveryController.searchTokens as jest.Mock + ).mockResolvedValueOnce(mockSearchResult); + + const { result } = renderHook(() => useTokenSearchDiscovery()); + const response = await result.current.searchTokens(mockSearchParams); + + expect( + Engine.context.TokenSearchDiscoveryController.searchTokens, + ).toHaveBeenCalledWith(mockSearchParams); + expect(response).toEqual(mockSearchResult); + }); + + it('should handle search errors gracefully', async () => { + const mockError = new Error('Search failed'); + ( + Engine.context.TokenSearchDiscoveryController.searchTokens as jest.Mock + ).mockRejectedValueOnce(mockError); + + const { result } = renderHook(() => useTokenSearchDiscovery()); + + await expect(result.current.searchTokens({})).rejects.toThrow( + 'Search failed', + ); + }); +}); diff --git a/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.ts b/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.ts new file mode 100644 index 00000000000..80f0625ab6d --- /dev/null +++ b/app/components/hooks/useTokenSearchDiscovery/useTokenSearchDiscovery.ts @@ -0,0 +1,21 @@ +import { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import Engine from '../../../core/Engine'; +import { selectRecentTokenSearches } from '../../../selectors/tokenSearchDiscoveryController'; +import { TokenSearchParams } from '@metamask/token-search-discovery-controller/dist/types.cjs'; + +export const useTokenSearchDiscovery = () => { + const recentSearches = useSelector(selectRecentTokenSearches); + + const searchTokens = useCallback(async (params: TokenSearchParams) => { + const { TokenSearchDiscoveryController } = Engine.context; + return await TokenSearchDiscoveryController.searchTokens(params); + }, []); + + return { + searchTokens, + recentSearches, + }; +}; + +export default useTokenSearchDiscovery; diff --git a/app/selectors/tokenSearchDiscoveryController.ts b/app/selectors/tokenSearchDiscoveryController.ts new file mode 100644 index 00000000000..760b705e47a --- /dev/null +++ b/app/selectors/tokenSearchDiscoveryController.ts @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect'; +import { RootState } from '../reducers'; + +const selectTokenSearchDiscoveryControllerState = (state: RootState) => + state.engine.backgroundState.TokenSearchDiscoveryController; + +export const selectRecentTokenSearches = createSelector( + selectTokenSearchDiscoveryControllerState, + (state) => state.recentSearches, +); diff --git a/app/selectors/types.ts b/app/selectors/types.ts index ecfedfb3ac4..af524cb1667 100644 --- a/app/selectors/types.ts +++ b/app/selectors/types.ts @@ -18,9 +18,11 @@ import { GasFeeController } from '@metamask/gas-fee-controller'; import { PPOMState } from '@metamask/ppom-validator'; import { ApprovalControllerState } from '@metamask/approval-controller'; import { AccountsControllerState } from '@metamask/accounts-controller'; +import { TokenSearchDiscoveryControllerState } from '@metamask/token-search-discovery-controller'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { SnapController } from '@metamask/snaps-controllers'; ///: END:ONLY_INCLUDE_IF + export interface EngineState { engine: { backgroundState: { @@ -45,6 +47,7 @@ export interface EngineState { TokensController: TokensControllerState; ApprovalController: ApprovalControllerState; AccountsController: AccountsControllerState; + TokenSearchDiscoveryController: TokenSearchDiscoveryControllerState; }; }; }