UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

1,280 lines (1,279 loc) • 55.9 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ConstantsUtil as CommonConstantsUtil, ConstantsUtil } from '@reown/appkit-common'; import { ApiController, ChainController, ConnectionController, ConnectorController, PublicStateController, StorageUtil } from '../../exports/index.js'; import { useAppKitAccount, useAppKitConnection, useAppKitConnections, useAppKitNetworkCore, useAppKitWallets, useDisconnect } from '../../exports/react.js'; import { extendedMainnet } from '../../exports/testing.js'; import { AssetUtil } from '../../exports/utils.js'; import { ConnectUtil } from '../../src/utils/ConnectUtil.js'; import { ConnectionControllerUtil } from '../../src/utils/ConnectionControllerUtil.js'; import { ConnectorControllerUtil } from '../../src/utils/ConnectorControllerUtil.js'; vi.mock('valtio', () => ({ useSnapshot: vi.fn() })); // Store refs to persist across renders and mock resets let useRefCallCount = 0; const refStore = []; // Factory function that creates a useRef mock implementation function createUseRefMock() { return (initialValue) => { // This simulates React's behavior where useRef returns the same ref across renders const callIndex = useRefCallCount++; if (!refStore[callIndex]) { refStore[callIndex] = { current: initialValue }; } return refStore[callIndex]; }; } vi.mock('react', () => ({ useCallback: vi.fn(fn => fn), useState: vi.fn(() => [0, vi.fn()]), useMemo: vi.fn(fn => fn()), useEffect: vi.fn(), useRef: vi.fn(createUseRefMock()) })); const { useSnapshot } = vi.mocked(await import('valtio'), true); const mockedReact = vi.mocked(await import('react'), true); describe('useAppKitNetwork', () => { beforeEach(() => { vi.resetAllMocks(); }); it('should return the correct network state', () => { useSnapshot.mockReturnValue({ activeCaipNetwork: extendedMainnet, activeChain: 'eip155', chains: new Map([ [ 'eip155', { networkState: { approvedCaipNetworkIds: ['eip155:1', 'eip155:137'], supportsAllNetworks: false } } ] ]) }); const { caipNetwork, chainId, approvedCaipNetworkIds, supportsAllNetworks } = useAppKitNetworkCore(); expect(caipNetwork).toBe(extendedMainnet); expect(chainId).toBe(1); expect(approvedCaipNetworkIds).toEqual(['eip155:1', 'eip155:137']); expect(supportsAllNetworks).toBe(false); expect(useSnapshot).toHaveBeenCalledWith(ChainController.state); }); it('should return defaults when no chain is active', () => { useSnapshot.mockReturnValue({ activeCaipNetwork: undefined, activeChain: undefined, chains: new Map() }); const { caipNetwork, approvedCaipNetworkIds, supportsAllNetworks } = useAppKitNetworkCore(); expect(caipNetwork).toBeUndefined(); expect(approvedCaipNetworkIds).toBeUndefined(); expect(supportsAllNetworks).toBe(true); }); }); describe('useAppKitAccount', () => { beforeEach(() => { vi.resetAllMocks(); }); it('should return the correct account state when disconnected', () => { useSnapshot.mockReturnValue({ activeChain: 'eip155', activeConnectorIds: { eip155: 'test-connector' }, connections: new Map(), chains: new Map([ [ 'eip155', { accountState: { address: undefined, caipAddress: undefined, allAccounts: [], status: 'disconnected' } } ] ]) }); const result = useAppKitAccount(); expect(result).toEqual({ allAccounts: [], address: undefined, caipAddress: undefined, isConnected: false, status: 'disconnected', embeddedWalletInfo: undefined }); }); it('should return the correct account state when connected', () => { const mockCaipAddress = 'eip155:1:0x123...'; const mockPlainAddress = '0x123...'; useSnapshot.mockReturnValue({ activeChain: 'eip155', activeConnectorIds: { eip155: 'test-connector' }, connections: new Map(), chains: new Map([ [ 'eip155', { accountState: { address: mockPlainAddress, caipAddress: mockCaipAddress, allAccounts: [], status: 'connected' } } ] ]) }); const result = useAppKitAccount(); expect(result).toEqual({ allAccounts: [], address: mockPlainAddress, caipAddress: mockCaipAddress, isConnected: true, status: 'connected', embeddedWalletInfo: undefined }); }); it('should return correct embedded wallet info when connected with social provider', () => { const mockCaipAddress = 'eip155:1:0x123...'; const mockPlainAddress = '0x123...'; const authConnector = { id: 'AUTH', name: 'ID Auth', imageUrl: 'https://example.com/id-auth.png' }; vi.spyOn(ConnectorController, 'getAuthConnector').mockReturnValue(authConnector); vi.spyOn(StorageUtil, 'getConnectedConnectorId').mockReturnValue('AUTH'); vi.spyOn(StorageUtil, 'getConnectedSocialUsername').mockReturnValue('test-username'); useSnapshot.mockReturnValue({ activeChain: 'eip155', activeConnectorIds: { eip155: CommonConstantsUtil.CONNECTOR_ID.AUTH }, connections: new Map(), chains: new Map([ [ 'eip155', { accountState: { address: mockPlainAddress, caipAddress: mockCaipAddress, allAccounts: [], status: 'connected', preferredAccountType: 'eoa', socialProvider: 'google', smartAccountDeployed: false, user: { email: 'email@email.test' } } } ] ]) }); const result = useAppKitAccount(); expect(result).toEqual({ allAccounts: [], address: mockPlainAddress, caipAddress: mockCaipAddress, isConnected: true, status: 'connected', embeddedWalletInfo: { user: { email: 'email@email.test', username: 'test-username' }, authProvider: 'google', accountType: 'eoa', isSmartAccountDeployed: false } }); }); it('should return account state with namespace parameter', async () => { vi.spyOn(ConnectorController, 'state', 'get').mockReturnValue({ ...ConnectorController.state, allConnectors: [{}], connected: true, activeConnector: {} }); const mockCaipAddress = 'eip155:1:0x123...'; const mockPlainAddress = '0x123...'; useSnapshot.mockReturnValue({ activeChain: 'eip155', activeConnectorIds: { eip155: 'test-connector' }, connections: new Map(), chains: new Map([ [ 'eip155', { accountState: { address: mockPlainAddress, caipAddress: mockCaipAddress, allAccounts: [], status: 'connected' } } ] ]) }); const result = useAppKitAccount({ namespace: 'solana' }); expect(result).toEqual({ allAccounts: [], address: undefined, caipAddress: undefined, isConnected: false, status: undefined, embeddedWalletInfo: undefined }); }); it('should return allAccounts with caipAddress when connections exist', () => { const mockConnections = [ { connectorId: 'test-connector', accounts: [ { address: '0xABC', type: 'eoa' }, { address: '0xDEF', type: 'smartAccount' } ], caipNetwork: extendedMainnet } ]; useSnapshot.mockReturnValue({ activeChain: 'eip155', activeConnectorIds: { eip155: 'test-connector' }, connections: new Map([['eip155', mockConnections]]), chains: new Map([ [ 'eip155', { accountState: { caipAddress: 'eip155:1:0xABC', status: 'connected' } } ] ]) }); const result = useAppKitAccount(); expect(result.allAccounts).toEqual([ expect.objectContaining({ namespace: 'eip155', address: '0xABC', chainId: '1', caipAddress: 'eip155:1:0xABC', type: 'eoa' }), expect.objectContaining({ namespace: 'eip155', address: '0xDEF', chainId: '1', caipAddress: 'eip155:1:0xDEF', type: 'smartAccount' }) ]); }); }); describe('useDisconnect', () => { it('should disconnect as expected', async () => { const disconnectSpy = vi.spyOn(ConnectionController, 'disconnect'); const { disconnect } = useDisconnect(); await disconnect(); expect(disconnectSpy).toHaveBeenCalled(); }); it('should disconnect for specific namespace as expected', async () => { const disconnectSpy = vi.spyOn(ConnectionController, 'disconnect'); const { disconnect } = useDisconnect(); await disconnect({ namespace: 'solana' }); expect(disconnectSpy).toHaveBeenCalledWith({ namespace: 'solana' }); }); }); describe('useAppKitConnections', () => { const mockConnection = { connectorId: 'test-connector', accounts: [{ address: '0x123...', type: 'eoa' }], caipNetwork: { id: 1, name: 'Ethereum', caipNetworkId: 'eip155:1', chainNamespace: ConstantsUtil.CHAIN.EVM } }; const mockFormattedConnection = { ...mockConnection, name: 'Test Connector', icon: 'connector-icon-url', networkIcon: 'network-icon-url' }; const mockConnector = { id: 'test-connector', type: 'WALLET_CONNECT', name: 'Test Connector', chain: 'eip155' }; beforeEach(() => { vi.resetAllMocks(); mockedReact.useState.mockReturnValue([0, vi.fn()]); mockedReact.useCallback.mockImplementation(fn => fn); }); it('should return formatted connections and storage connections', () => { useSnapshot .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); vi.spyOn(ConnectionControllerUtil, 'getConnectionsData').mockReturnValue({ connections: [mockConnection], recentConnections: [mockConnection] }); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); vi.spyOn(ConnectorController, 'getConnectorName').mockReturnValue('Test Connector'); vi.spyOn(AssetUtil, 'getConnectorImage').mockReturnValue('connector-icon-url'); vi.spyOn(AssetUtil, 'getNetworkImage').mockReturnValue('network-icon-url'); const result = useAppKitConnections(); expect(result).toEqual({ connections: [mockFormattedConnection], recentConnections: [mockFormattedConnection] }); expect(ConnectionControllerUtil.getConnectionsData).toHaveBeenCalledWith('eip155'); expect(ConnectorController.getConnectorById).toHaveBeenCalledWith('test-connector'); expect(AssetUtil.getConnectorImage).toHaveBeenCalled(); expect(AssetUtil.getNetworkImage).toHaveBeenCalledWith(mockConnection.caipNetwork); }); it('should use provided namespace instead of active chain', () => { useSnapshot .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); vi.spyOn(ConnectionControllerUtil, 'getConnectionsData').mockReturnValue({ connections: [], recentConnections: [] }); useAppKitConnections('solana'); expect(ConnectionControllerUtil.getConnectionsData).toHaveBeenCalledWith('solana'); }); it('should throw error when no namespace is found', () => { useSnapshot .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({ activeChain: undefined }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); expect(() => useAppKitConnections()).toThrow('No namespace found'); }); it('should handle empty connections', () => { useSnapshot .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); vi.spyOn(ConnectionControllerUtil, 'getConnectionsData').mockReturnValue({ connections: [], recentConnections: [] }); const result = useAppKitConnections(); expect(result).toEqual({ connections: [], recentConnections: [] }); }); it('should return empty state when multiWallet is disabled', () => { vi.spyOn(ConnectionControllerUtil, 'getConnectionsData').mockReturnValue({ connections: [], recentConnections: [] }); useSnapshot .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({}) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: false } }); const result = useAppKitConnections(); expect(result).toEqual({ connections: [], recentConnections: [] }); }); }); describe('useAppKitConnection', () => { const mockConnection = { connectorId: 'test-connector', accounts: [{ address: '0x123...', type: 'eoa' }], caipNetwork: {} }; const mockOnSuccess = vi.fn(); const mockOnError = vi.fn(); beforeEach(() => { vi.resetAllMocks(); mockedReact.useState.mockReturnValue([0, vi.fn()]); mockedReact.useCallback.mockImplementation(fn => fn); }); it('should return current connection and connection state', () => { const mockConnections = new Map([['eip155', [mockConnection]]]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const result = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); expect(result.connection).toBe(mockConnection); expect(result.isPending).toBe(false); expect(typeof result.switchConnection).toBe('function'); expect(typeof result.deleteConnection).toBe('function'); }); it('should handle switching connection successfully', async () => { const mockConnections = new Map([['eip155', [mockConnection]]]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const setIsSwitchingConnectionSpy = vi.spyOn(ConnectionController, 'setIsSwitchingConnection'); const switchConnectionSpy = vi .spyOn(ConnectionController, 'switchConnection') .mockImplementation(async ({ onChange }) => { onChange?.({ address: '0x456...', namespace: 'eip155', hasSwitchedAccount: true, hasSwitchedWallet: false }); }); const { switchConnection } = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); await switchConnection({ connection: mockConnection, address: '0x456...' }); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(true); expect(switchConnectionSpy).toHaveBeenCalledWith({ connection: mockConnection, address: '0x456...', namespace: 'eip155', onChange: expect.any(Function) }); expect(mockOnSuccess).toHaveBeenCalledWith({ address: '0x456...', namespace: 'eip155', hasSwitchedAccount: true, hasSwitchedWallet: false, hasDeletedWallet: false }); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(false); }); it('should handle switching connection error', async () => { const mockConnections = new Map([['eip155', [mockConnection]]]); const mockError = new Error('Connection failed'); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const setIsSwitchingConnectionSpy = vi.spyOn(ConnectionController, 'setIsSwitchingConnection'); const switchConnectionSpy = vi .spyOn(ConnectionController, 'switchConnection') .mockRejectedValue(mockError); const { switchConnection } = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); await switchConnection({ connection: mockConnection, address: '0x456...' }); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(true); expect(switchConnectionSpy).toHaveBeenCalled(); expect(mockOnError).toHaveBeenCalledWith(mockError); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(false); }); it('should handle non-error exceptions in switchConnection', async () => { const mockConnections = new Map([['eip155', [mockConnection]]]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const setIsSwitchingConnectionSpy = vi.spyOn(ConnectionController, 'setIsSwitchingConnection'); const switchConnectionSpy = vi .spyOn(ConnectionController, 'switchConnection') .mockRejectedValue('String error'); const { switchConnection } = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); await switchConnection({ connection: mockConnection, address: '0x456...' }); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(true); expect(switchConnectionSpy).toHaveBeenCalled(); expect(mockOnError).toHaveBeenCalledWith(new Error('Something went wrong')); expect(setIsSwitchingConnectionSpy).toHaveBeenCalledWith(false); }); it('should handle deleting connection', () => { const mockConnections = new Map([['eip155', [mockConnection]]]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const deleteAddressFromConnectionSpy = vi.spyOn(StorageUtil, 'deleteAddressFromConnection'); const { deleteConnection } = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); deleteConnection({ address: '0x123...', connectorId: 'test-connector' }); expect(deleteAddressFromConnectionSpy).toHaveBeenCalledWith({ connectorId: 'test-connector', address: '0x123...', namespace: 'eip155' }); expect(mockOnSuccess).toHaveBeenCalledWith({ address: '0x123...', namespace: 'eip155', hasSwitchedAccount: false, hasSwitchedWallet: false, hasDeletedWallet: true }); }); it('should use provided namespace instead of active chain', () => { const mockConnections = new Map([ ['solana', [{ ...mockConnection, connectorId: 'solana-connector' }]] ]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { solana: 'solana-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const result = useAppKitConnection({ namespace: 'solana', onSuccess: mockOnSuccess, onError: mockOnError }); expect(result.connection).toEqual({ ...mockConnection, connectorId: 'solana-connector' }); }); it('should throw error when no namespace is found', () => { useSnapshot .mockReturnValueOnce({ connections: new Map(), isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: {} }) .mockReturnValueOnce({ activeChain: undefined }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); expect(() => useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError })).toThrow('No namespace found'); }); it('should return undefined connection when no matching connector found', () => { const mockConnections = new Map([['eip155', [mockConnection]]]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'different-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const result = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); expect(result.connection).toBeUndefined(); }); it('should handle case-insensitive connector matching', () => { const mockConnections = new Map([ ['eip155', [{ ...mockConnection, connectorId: 'TEST-CONNECTOR' }]] ]); useSnapshot .mockReturnValueOnce({ connections: mockConnections, isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: { eip155: 'test-connector' } }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: true } }); const result = useAppKitConnection({ onSuccess: mockOnSuccess, onError: mockOnError }); expect(result.connection).toEqual({ ...mockConnection, connectorId: 'TEST-CONNECTOR' }); }); it('should return empty state when multiWallet is disabled', () => { vi.spyOn(ConnectionControllerUtil, 'getConnectionsData').mockReturnValue({ connections: [], recentConnections: [] }); useSnapshot .mockReturnValueOnce({ connections: new Map(), isSwitchingConnection: false }) .mockReturnValueOnce({ activeConnectorIds: {} }) .mockReturnValueOnce({ activeChain: 'eip155' }) .mockReturnValueOnce({ remoteFeatures: { multiWallet: false } }); const result = useAppKitConnection({ namespace: 'eip155' }); expect(result.connection).toBeUndefined(); expect(result.isPending).toBe(false); }); }); describe('useAppKitWallets', () => { const mockWalletItem = { id: 'test-wallet', name: 'Test Wallet', imageUrl: 'https://example.com/wallet.png', connectors: [ { id: 'test-connector', chain: 'eip155', chainImageUrl: 'https://example.com/chain.png' } ], isInjected: false, isRecent: false, walletInfo: {} }; const mockInjectedWalletItem = { ...mockWalletItem, id: 'injected-wallet', name: 'Injected Wallet', isInjected: true }; beforeEach(() => { // Reset ref tracking to start fresh for each test useRefCallCount = 0; refStore.length = 0; vi.resetAllMocks(); // Re-implement useRef mock after reset to ensure it always returns valid refs // This must be done after resetAllMocks because reset clears the mock implementation mockedReact.useRef.mockImplementation((initialValue) => { const callIndex = useRefCallCount++; if (!refStore[callIndex]) { refStore[callIndex] = { current: initialValue }; } return refStore[callIndex]; }); mockedReact.useState.mockReturnValue([false, vi.fn()]); mockedReact.useMemo.mockImplementation(fn => fn()); mockedReact.useEffect.mockImplementation(fn => fn()); }); it('should return empty state when headless is not enabled', () => { useSnapshot.mockReturnValue({ features: { headless: false }, remoteFeatures: { headless: false } }); // Mock ConnectorController.state since useMemo runs before early return vi.spyOn(ConnectorController, 'state', 'get').mockReturnValue({ connectors: [] }); // Mock ApiController.state since getInitialWallets and getWalletConnectWallets access it ApiController.state.wallets = []; ApiController.state.search = []; const result = useAppKitWallets(); expect(result).toEqual({ wallets: [], wcWallets: [], isFetchingWallets: false, isFetchingWcUri: false, isInitialized: false, wcUri: undefined, connectingWallet: undefined, page: 0, count: 0, connect: expect.any(Function), fetchWallets: expect.any(Function), resetWcUri: expect.any(Function), resetConnectingWallet: expect.any(Function), getWcUri: expect.any(Function), wcError: false, wcClientId: null }); }); it('should return empty state when remoteFeatures.headless is false', () => { useSnapshot.mockReturnValue({ features: { headless: true }, remoteFeatures: { headless: false } }); // Mock ConnectorController.state since useMemo runs before early return vi.spyOn(ConnectorController, 'state', 'get').mockReturnValue({ connectors: [] }); // Mock ApiController.state since getInitialWallets and getWalletConnectWallets access it ApiController.state.wallets = []; ApiController.state.search = []; const result = useAppKitWallets(); expect(result.wallets).toEqual([]); expect(result.wcWallets).toEqual([]); expect(result.isInitialized).toBe(false); }); it('should return wallets and wcWallets when headless is enabled', () => { const mockWallets = [mockWalletItem]; const mockWcWallets = [mockWalletItem, { ...mockWalletItem, id: 'wc-wallet-2' }]; useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [{ id: 'wc-wallet-1' }, { id: 'wc-wallet-2' }], search: [], page: 1, count: 100 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue(mockWallets); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue(mockWcWallets); const result = useAppKitWallets(); expect(result.wallets).toEqual(mockWallets); expect(result.wcWallets).toEqual(mockWcWallets); expect(result.isInitialized).toBe(true); expect(result.isFetchingWcUri).toBe(false); expect(result.page).toBe(1); expect(result.count).toBe(100); }); it('should return correct state values', () => { const setIsFetchingWallets = vi.fn(); const setCurrentWcPayUrl = vi.fn(); mockedReact.useState .mockReturnValueOnce([true, setIsFetchingWallets]) .mockReturnValueOnce([undefined, setCurrentWcPayUrl]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: 'wc:test-uri', wcFetchingUri: true }) .mockReturnValueOnce({ wallets: [], search: [], page: 2, count: 50 }) .mockReturnValueOnce({ initialized: true, connectingWallet: mockWalletItem }) .mockReturnValueOnce({ clientId: 'relay-client-abc123' }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const result = useAppKitWallets(); expect(result.isFetchingWallets).toBe(true); expect(result.isFetchingWcUri).toBe(true); expect(result.wcUri).toBe('wc:test-uri'); expect(result.connectingWallet).toEqual(mockWalletItem); expect(result.page).toBe(2); expect(result.count).toBe(50); expect(result.wcClientId).toBe('relay-client-abc123'); }); it('should fetch wallets without query', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const fetchWalletsByPageSpy = vi .spyOn(ApiController, 'fetchWalletsByPage') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets(); expect(setIsFetchingWallets).toHaveBeenCalledWith(true); expect(fetchWalletsByPageSpy).toHaveBeenCalledWith({ page: 1 }); expect(ApiController.state.search).toEqual([]); expect(setIsFetchingWallets).toHaveBeenCalledWith(false); }); it('should fetch wallets with query', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const searchWalletSpy = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ query: 'metamask' }); expect(setIsFetchingWallets).toHaveBeenCalledWith(true); expect(searchWalletSpy).toHaveBeenCalledWith({ search: 'metamask' }); expect(setIsFetchingWallets).toHaveBeenCalledWith(false); }); it('should fetch wallets with page and entries', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const fetchWalletsByPageSpy = vi .spyOn(ApiController, 'fetchWalletsByPage') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ page: 3 }); expect(fetchWalletsByPageSpy).toHaveBeenCalledWith({ page: 3 }); }); it('should handle fetchWallets error gracefully', async () => { const setIsFetchingWallets = vi.fn(); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); vi.spyOn(ApiController, 'fetchWalletsByPage').mockRejectedValue(new Error('Fetch failed')); const result = useAppKitWallets(); await result.fetchWallets(); expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch WalletConnect wallets:', expect.any(Error)); expect(setIsFetchingWallets).toHaveBeenCalledWith(false); consoleErrorSpy.mockRestore(); }); it('should fetch wallets with search param', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const searchWalletSpy = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ search: 'phantom' }); expect(searchWalletSpy).toHaveBeenCalledWith({ search: 'phantom' }); }); it('should use search over query when both provided', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const searchWalletSpy = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ search: 'phantom', query: 'metamask' }); expect(searchWalletSpy).toHaveBeenCalledWith({ search: 'phantom' }); }); it('should pass entries and badge to fetchWalletsByPage', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const fetchWalletsByPageSpy = vi .spyOn(ApiController, 'fetchWalletsByPage') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ entries: 20, badge: 'certified' }); expect(fetchWalletsByPageSpy).toHaveBeenCalledWith({ page: 1, entries: 20, badge: 'certified' }); }); it('should pass badge and entries to searchWallet', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const searchWalletSpy = vi.spyOn(ApiController, 'searchWallet').mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ query: 'safe', badge: 'certified', entries: 50 }); expect(searchWalletSpy).toHaveBeenCalledWith({ search: 'safe', badge: 'certified', entries: 50 }); }); it('should pass include and exclude to fetchWalletsByPage', async () => { const setIsFetchingWallets = vi.fn(); mockedReact.useState.mockReturnValue([false, setIsFetchingWallets]); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const fetchWalletsByPageSpy = vi .spyOn(ApiController, 'fetchWalletsByPage') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.fetchWallets({ include: ['wallet-1'], exclude: ['wallet-2'] }); expect(fetchWalletsByPageSpy).toHaveBeenCalledWith({ page: 1, include: ['wallet-1'], exclude: ['wallet-2'] }); }); it('should connect to injected wallet', async () => { const mockConnector = { id: 'test-connector', type: 'INJECTED', name: 'Test Connector', chain: 'eip155' }; useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const setSpy = vi.spyOn(PublicStateController, 'set'); const getConnectorSpy = vi .spyOn(ConnectorController, 'getConnector') .mockReturnValue(mockConnector); const connectExternalSpy = vi .spyOn(ConnectorControllerUtil, 'connectExternal') .mockResolvedValue({ address: '0x123', chainNamespace: 'eip155', chainId: '1' }); const result = useAppKitWallets(); await result.connect(mockInjectedWalletItem, 'eip155'); expect(setSpy).toHaveBeenCalledWith({ connectingWallet: mockInjectedWalletItem }); expect(getConnectorSpy).toHaveBeenCalledWith({ id: 'test-connector', namespace: 'eip155' }); expect(connectExternalSpy).toHaveBeenCalledWith(mockConnector); }); it('should connect to WalletConnect wallet', async () => { useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const setSpy = vi.spyOn(PublicStateController, 'set'); const connectWalletConnectSpy = vi .spyOn(ConnectionController, 'connectWalletConnect') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.connect(mockWalletItem); expect(setSpy).toHaveBeenCalledWith({ connectingWallet: mockWalletItem }); expect(connectWalletConnectSpy).toHaveBeenCalledWith({ cache: 'never' }); }); it('should connect to WalletConnect wallet when connector is not found', async () => { useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [], page: 1, count: 0 }) .mockReturnValueOnce({ initialized: true, connectingWallet: undefined }) .mockReturnValueOnce({ clientId: null }); vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([]); vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([]); const setSpy = vi.spyOn(PublicStateController, 'set'); vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined); const connectWalletConnectSpy = vi .spyOn(ConnectionController, 'connectWalletConnect') .mockResolvedValue(undefined); const result = useAppKitWallets(); await result.connect(mockInjectedWalletItem, 'eip155'); expect(setSpy).toHaveBeenCalledWith({ connectingWallet: mockInjectedWalletItem }); expect(connectWalletConnectSpy).toHaveBeenCalledWith({ cache: 'never' }); }); it('should handle connect error and clear connectingWallet', async () => { const mockError = new Error('Connection failed'); useSnapshot .mockReturnValueOnce({ features: { headless: true }, remoteFeatures: { headless: true } }) .mockReturnValueOnce({ wcUri: undefined, wcFetchingUri: false }) .mockReturnValueOnce({ wallets: [], search: [],