UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

559 lines • 28.2 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ConstantsUtil } from '@reown/appkit-common'; import {} from '@reown/appkit-controllers'; import { ApiController as ApiControllerRelative } from '../../src/controllers/ApiController.js'; import { ConnectionController as ConnectionControllerRelative } from '../../src/controllers/ConnectionController.js'; import { ConnectorController as ConnectorControllerRelative } from '../../src/controllers/ConnectorController.js'; import { OptionsController as OptionsControllerRelative } from '../../src/controllers/OptionsController.js'; import { ConnectorUtil } from '../../src/utils/ConnectorUtil'; import { CoreHelperUtil } from '../../src/utils/CoreHelperUtil.js'; import { OptionsUtil } from '../../src/utils/OptionsUtil.js'; import { WalletUtil } from '../../src/utils/WalletUtil'; const INJECTED = { id: 'injected' }; const RECENT = { id: 'recent' }; const FEATURED = { id: 'featured' }; const CUSTOM = { id: 'custom' }; const EXTERNAL = { id: 'external' }; const MULTI_CHAIN = { id: 'multiChain' }; const INJECTED_CONNECTOR = { id: 'injected', type: 'INJECTED', info: { rdns: 'browser.wallet' }, name: 'Browser Wallet', chain: { id: 'eip155:1' } }; const ANNOUNCED_CONNECTOR = { id: 'announced', type: 'ANNOUNCED', info: { rdns: 'announced.wallet' }, name: 'Announced Wallet', chain: { id: 'eip155:1' } }; describe('ConnectorUtil', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getConnectorTypeOrder', () => { it('should return connector positions in order of overriddenConnectors first then enabled connectors', () => { vi.spyOn(ConnectorUtil, 'getIsConnectedWithWC').mockReturnValue(false); const result = ConnectorUtil.getConnectorTypeOrder({ recommended: [], featured: [FEATURED], custom: [CUSTOM], recent: [RECENT], announced: [INJECTED], injected: [INJECTED], multiChain: [MULTI_CHAIN], external: [EXTERNAL], overriddenConnectors: ['featured', 'walletConnect', 'injected'] }); expect(result).toEqual([ 'featured', 'walletConnect', 'injected', 'recent', 'custom', 'external' ]); }); it('should use default connectorPosition from OptionsController when overriddenConnectors not provided', () => { vi.spyOn(ConnectorUtil, 'getIsConnectedWithWC').mockReturnValue(false); const originalFeatures = OptionsControllerRelative.state.features; OptionsControllerRelative.state.features = { ...originalFeatures, connectorTypeOrder: ['injected', 'walletConnect'] }; const result = ConnectorUtil.getConnectorTypeOrder({ recommended: [], featured: [FEATURED], custom: [CUSTOM], recent: [RECENT], announced: [INJECTED], injected: [INJECTED], multiChain: [MULTI_CHAIN], external: [EXTERNAL] }); OptionsControllerRelative.state.features = originalFeatures; expect(result).toEqual([ 'injected', 'walletConnect', 'recent', 'featured', 'custom', 'external' ]); }); it('should only include enabled connectors', () => { vi.spyOn(ConnectorUtil, 'getIsConnectedWithWC').mockReturnValue(false); const originalFeatures = OptionsControllerRelative.state.features; OptionsControllerRelative.state.features = { ...originalFeatures, connectorTypeOrder: ['injected', 'recommended'] }; const result = ConnectorUtil.getConnectorTypeOrder({ recommended: [], featured: [FEATURED], custom: [CUSTOM], recent: [RECENT], announced: [], injected: [], multiChain: [], external: [EXTERNAL] }); OptionsControllerRelative.state.features = originalFeatures; expect(result).toEqual(['walletConnect', 'recent', 'featured', 'custom', 'external']); expect(result).not.toContain('injected'); }); }); describe('showConnector', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should hide browser wallet on desktop', () => { vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(false); expect(ConnectorUtil.showConnector(INJECTED_CONNECTOR)).toBe(false); }); it('should show browser wallet on mobile', () => { vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true); vi.spyOn(ConnectionControllerRelative, 'checkInstalled').mockReturnValue(true); ApiControllerRelative.state.excludedWallets = []; expect(ConnectorUtil.showConnector(INJECTED_CONNECTOR)).toBe(true); }); it('should hide injected connector when not installed and no rdns', () => { vi.spyOn(ConnectionControllerRelative, 'checkInstalled').mockReturnValue(false); expect(ConnectorUtil.showConnector({ ...INJECTED_CONNECTOR, info: undefined })).toBe(false); }); it('should hide connector when rdns is excluded', () => { ApiControllerRelative.state.excludedWallets = [ { rdns: 'browser.wallet', name: 'Test Wallet' } ]; expect(ConnectorUtil.showConnector(INJECTED_CONNECTOR)).toBe(false); }); it('should hide connector when name is excluded', () => { ApiControllerRelative.state.excludedWallets = [ { name: 'Browser Wallet', rdns: 'test.wallet' } ]; expect(ConnectorUtil.showConnector(INJECTED_CONNECTOR)).toBe(false); }); it('should hide announced connector when excluded with rdns', () => { ApiControllerRelative.state.excludedWallets = [ { rdns: 'announced.wallet', name: 'Announced Wallet' } ]; expect(ConnectorUtil.showConnector(ANNOUNCED_CONNECTOR)).toBe(false); }); it('should hide announced connector when excluded with name', () => { ApiControllerRelative.state.excludedWallets = [ { name: 'Announced Wallet', rdns: 'announced' } ]; expect(ConnectorUtil.showConnector(ANNOUNCED_CONNECTOR)).toBe(false); }); it('should show injected connector when not excluded', () => { ApiControllerRelative.state.excludedWallets = []; vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true); vi.spyOn(ConnectionControllerRelative, 'checkInstalled').mockReturnValue(true); expect(ConnectorUtil.showConnector(INJECTED_CONNECTOR)).toBe(true); }); it('should show announced connector when not excluded', () => { ApiControllerRelative.state.excludedWallets = []; expect(ConnectorUtil.showConnector(ANNOUNCED_CONNECTOR)).toBe(true); }); it('should hide connector whose explorerId is in excludeWalletIds', () => { ApiControllerRelative.state.excludedWallets = []; const originalExclude = OptionsControllerRelative.state.excludeWalletIds; OptionsControllerRelative.state.excludeWalletIds = ['phantom-id']; const phantom = { ...ANNOUNCED_CONNECTOR, explorerId: 'phantom-id' }; expect(ConnectorUtil.showConnector(phantom)).toBe(false); OptionsControllerRelative.state.excludeWalletIds = originalExclude; }); it('should hide connector when includeWalletIds is set and connector id not included', () => { ApiControllerRelative.state.excludedWallets = []; const originalInclude = OptionsControllerRelative.state.includeWalletIds; OptionsControllerRelative.state.includeWalletIds = ['metamask-id']; const phantom = { ...ANNOUNCED_CONNECTOR, explorerId: 'phantom-id' }; expect(ConnectorUtil.showConnector(phantom)).toBe(false); OptionsControllerRelative.state.includeWalletIds = originalInclude; }); it('should show connector when its explorerId is in includeWalletIds', () => { ApiControllerRelative.state.excludedWallets = []; const originalInclude = OptionsControllerRelative.state.includeWalletIds; OptionsControllerRelative.state.includeWalletIds = ['metamask-id']; const metamask = { ...ANNOUNCED_CONNECTOR, explorerId: 'metamask-id' }; expect(ConnectorUtil.showConnector(metamask)).toBe(true); OptionsControllerRelative.state.includeWalletIds = originalInclude; }); it('should fall back to explorerWallet.id when explorerId is not set', () => { ApiControllerRelative.state.excludedWallets = []; const originalExclude = OptionsControllerRelative.state.excludeWalletIds; OptionsControllerRelative.state.excludeWalletIds = ['phantom-id']; const phantom = { ...ANNOUNCED_CONNECTOR, explorerWallet: { id: 'phantom-id' } }; expect(ConnectorUtil.showConnector(phantom)).toBe(false); OptionsControllerRelative.state.excludeWalletIds = originalExclude; }); it('should not filter EXTERNAL connectors by include/excludeWalletIds', () => { ApiControllerRelative.state.excludedWallets = []; const originalInclude = OptionsControllerRelative.state.includeWalletIds; OptionsControllerRelative.state.includeWalletIds = ['metamask-id']; const external = { id: 'external', type: 'EXTERNAL', info: { rdns: 'external.wallet' }, name: 'External Wallet', chain: { id: 'eip155:1' }, explorerId: 'external-id' }; expect(ConnectorUtil.showConnector(external)).toBe(true); OptionsControllerRelative.state.includeWalletIds = originalInclude; }); }); describe('getAuthName', () => { it('should return socialUsername when provided and not discord ending with 0', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: 'john_doe', socialProvider: 'github' }); expect(result).toBe('john_doe'); }); it('should return socialUsername without last character when discord provider and ends with 0', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: 'john_doe0', socialProvider: 'discord' }); expect(result).toBe('john_doe'); }); it('should return socialUsername as-is when discord provider but does not end with 0', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: 'john_doe1', socialProvider: 'discord' }); expect(result).toBe('john_doe1'); }); it('should return socialUsername when provided and socialProvider is null', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: 'john_doe', socialProvider: null }); expect(result).toBe('john_doe'); }); it('should return email when socialUsername is not provided', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com' }); expect(result).toBe('test@example.com'); }); it('should return email when socialUsername is null', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: null }); expect(result).toBe('test@example.com'); }); it('should return email when socialUsername is empty string', () => { const result = ConnectorUtil.getAuthName({ email: 'test@example.com', socialUsername: '' }); expect(result).toBe('test@example.com'); }); it('should truncate email when longer than 30 characters', () => { const longEmail = 'verylongemailaddress@verylongdomain.com'; const result = ConnectorUtil.getAuthName({ email: longEmail }); expect(result).toBe(`${longEmail.slice(0, -3)}...`); expect(result).toBe('verylongemailaddress@verylongdomain....'); }); it('should return full email when exactly 30 characters', () => { const email = 'test12345@example12345678.com'; const result = ConnectorUtil.getAuthName({ email }); expect(result).toBe(email); }); it('should return full email when less than 30 characters', () => { const shortEmail = 'short@test.com'; const result = ConnectorUtil.getAuthName({ email: shortEmail }); expect(result).toBe(shortEmail); }); }); describe('fetchProviderData', () => { const mockProvider = { request: vi.fn() }; const mockConnector = { name: 'Test Wallet', id: 'test-connector', provider: mockProvider }; beforeEach(() => { vi.clearAllMocks(); mockProvider.request.mockClear(); }); it('should return empty data for Browser Wallet on desktop', async () => { vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(false); const browserWalletConnector = { ...mockConnector, name: 'Browser Wallet' }; const result = await ConnectorUtil.fetchProviderData(browserWalletConnector); expect(result).toEqual({ accounts: [], chainId: undefined }); expect(mockProvider.request).not.toHaveBeenCalled(); }); it('should return empty data for AUTH connector', async () => { const authConnector = { ...mockConnector, id: ConstantsUtil.CONNECTOR_ID.AUTH }; const result = await ConnectorUtil.fetchProviderData(authConnector); expect(result).toEqual({ accounts: [], chainId: undefined }); expect(mockProvider.request).not.toHaveBeenCalled(); }); it('should fetch accounts and chainId successfully', async () => { const mockAccounts = ['0x123', '0x456']; const mockChainId = '0x1'; mockProvider.request.mockResolvedValueOnce(mockAccounts).mockResolvedValueOnce(mockChainId); const result = await ConnectorUtil.fetchProviderData(mockConnector); expect(result).toEqual({ accounts: mockAccounts, chainId: 1 }); expect(mockProvider.request).toHaveBeenCalledTimes(2); expect(mockProvider.request).toHaveBeenNthCalledWith(1, { method: 'eth_accounts' }); expect(mockProvider.request).toHaveBeenNthCalledWith(2, { method: 'eth_chainId' }); }); it('should return empty data when provider is undefined', async () => { const connectorWithoutProvider = { ...mockConnector, provider: undefined }; const result = await ConnectorUtil.fetchProviderData(connectorWithoutProvider); expect(result).toEqual({ accounts: undefined, chainId: undefined }); }); it('should handle eth_accounts request failure gracefully', async () => { mockProvider.request .mockRejectedValueOnce(new Error('eth_accounts failed')) .mockResolvedValueOnce('0x1'); const result = await ConnectorUtil.fetchProviderData(mockConnector); expect(result).toEqual({ accounts: [], chainId: undefined }); }); it('should handle eth_chainId request failure gracefully', async () => { mockProvider.request .mockResolvedValueOnce(['0x123']) .mockRejectedValueOnce(new Error('eth_chainId failed')); const result = await ConnectorUtil.fetchProviderData(mockConnector); expect(result).toEqual({ accounts: [], chainId: undefined }); }); it('should handle both requests failing gracefully', async () => { mockProvider.request .mockRejectedValueOnce(new Error('eth_accounts failed')) .mockRejectedValueOnce(new Error('eth_chainId failed')); const result = await ConnectorUtil.fetchProviderData(mockConnector); expect(result).toEqual({ accounts: [], chainId: undefined }); }); it('should convert various hex chainId formats correctly', async () => { const testCases = [ { hex: '0x1', expected: 1 }, { hex: '0xa', expected: 10 }, { hex: '0x38', expected: 56 }, { hex: '0x89', expected: 137 }, { hex: '0xa4b1', expected: 42161 } ]; for (const { hex, expected } of testCases) { mockProvider.request.mockClear(); mockProvider.request.mockResolvedValueOnce(['0x123']).mockResolvedValueOnce(hex); const result = await ConnectorUtil.fetchProviderData(mockConnector); expect(result.chainId).toBe(expected); } }); it('should handle Browser Wallet on mobile correctly', async () => { vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true); const browserWalletConnector = { ...mockConnector, name: 'Browser Wallet' }; const mockAccounts = ['0x789']; const mockChainId = '0x1'; mockProvider.request.mockResolvedValueOnce(mockAccounts).mockResolvedValueOnce(mockChainId); const result = await ConnectorUtil.fetchProviderData(browserWalletConnector); expect(result).toEqual({ accounts: mockAccounts, chainId: 1 }); expect(mockProvider.request).toHaveBeenCalledTimes(2); expect(mockProvider.request).toHaveBeenNthCalledWith(1, { method: 'eth_accounts' }); expect(mockProvider.request).toHaveBeenNthCalledWith(2, { method: 'eth_chainId' }); }); }); describe('getCappedRecommendedWallets', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should return empty when no wc connector, no injected connectors, and no custom wallets', () => { const originalConnectors = ConnectorControllerRelative.state.connectors; const originalCustomWallets = OptionsControllerRelative.state.customWallets; const originalFeaturedWalletIds = OptionsControllerRelative.state.featuredWalletIds; ConnectorControllerRelative.state.connectors = []; OptionsControllerRelative.state.customWallets = undefined; OptionsControllerRelative.state.featuredWalletIds = []; const recommended = [{ id: 'w1', name: 'Wallet 1' }]; const result = ConnectorUtil.getCappedRecommendedWallets(recommended); ConnectorControllerRelative.state.connectors = originalConnectors; OptionsControllerRelative.state.customWallets = originalCustomWallets; OptionsControllerRelative.state.featuredWalletIds = originalFeaturedWalletIds; expect(result).toEqual([]); }); it('should cap recommended to fill remaining slots up to 4', () => { const WC = { id: 'walletConnect', type: 'EXTERNAL', name: 'WalletConnect', chain: { id: 'eip155:1' } }; const originalConnectors = ConnectorControllerRelative.state.connectors; const originalCustomWallets = OptionsControllerRelative.state.customWallets; const originalFeaturedWalletIds = OptionsControllerRelative.state.featuredWalletIds; ConnectorControllerRelative.state.connectors = [WC]; OptionsControllerRelative.state.customWallets = [{ id: 'c1' }]; OptionsControllerRelative.state.featuredWalletIds = ['f1']; vi.spyOn(OptionsUtil, 'isEmailEnabled').mockReturnValue(false); vi.spyOn(OptionsUtil, 'isSocialsEnabled').mockReturnValue(false); vi.spyOn(WalletUtil, 'filterOutDuplicateWallets').mockImplementation(w => w); const recommended = [ { id: 'w1', name: 'Wallet 1' }, { id: 'w2', name: 'Wallet 2' }, { id: 'w3', name: 'Wallet 3' } ]; // featured(1) + custom(1) + injected(0) + email(0) + social(0) = 2 => slice 2 const result = ConnectorUtil.getCappedRecommendedWallets(recommended); ConnectorControllerRelative.state.connectors = originalConnectors; OptionsControllerRelative.state.customWallets = originalCustomWallets; OptionsControllerRelative.state.featuredWalletIds = originalFeaturedWalletIds; expect(result.map(w => w.id)).toEqual(['w1', 'w2']); }); it('should return empty when displayed wallets are already 4 or more', () => { const WC = { id: 'walletConnect', type: 'EXTERNAL', name: 'WalletConnect', chain: { id: 'eip155:1' } }; const INJECTED_ONE = { id: 'inj-1', type: 'INJECTED', name: 'Injected One', chain: { id: 'eip155:1' } }; const originalConnectors = ConnectorControllerRelative.state.connectors; const originalCustomWallets = OptionsControllerRelative.state.customWallets; const originalFeaturedWalletIds = OptionsControllerRelative.state.featuredWalletIds; ConnectorControllerRelative.state.connectors = [WC, INJECTED_ONE]; OptionsControllerRelative.state.customWallets = [{ id: 'c1' }]; OptionsControllerRelative.state.featuredWalletIds = ['f1']; vi.spyOn(OptionsUtil, 'isEmailEnabled').mockReturnValue(true); vi.spyOn(OptionsUtil, 'isSocialsEnabled').mockReturnValue(true); const filterSpy = vi.spyOn(WalletUtil, 'filterOutDuplicateWallets'); const recommended = [ { id: 'w1', name: 'Wallet 1' }, { id: 'w2', name: 'Wallet 2' } ]; // featured(1) + custom(1) + injected(1) + email(1) + social(1) = 5 => slice 0 const result = ConnectorUtil.getCappedRecommendedWallets(recommended); ConnectorControllerRelative.state.connectors = originalConnectors; OptionsControllerRelative.state.customWallets = originalCustomWallets; OptionsControllerRelative.state.featuredWalletIds = originalFeaturedWalletIds; expect(result).toEqual([]); expect(filterSpy).not.toHaveBeenCalled(); }); it('should ignore Browser Wallet and WalletConnect in injected count', () => { const WC = { id: 'walletConnect', type: 'EXTERNAL', name: 'WalletConnect', chain: { id: 'eip155:1' } }; const BROWSER_WALLET = { id: 'inj-browser', type: 'INJECTED', name: 'Browser Wallet', chain: { id: 'eip155:1' } }; const WC_INJECTED = { id: 'walletConnect', type: 'INJECTED', name: 'WalletConnect', chain: { id: 'eip155:1' } }; const originalConnectors = ConnectorControllerRelative.state.connectors; const originalCustomWallets = OptionsControllerRelative.state.customWallets; const originalFeaturedWalletIds = OptionsControllerRelative.state.featuredWalletIds; ConnectorControllerRelative.state.connectors = [WC, BROWSER_WALLET, WC_INJECTED]; OptionsControllerRelative.state.customWallets = []; OptionsControllerRelative.state.featuredWalletIds = []; vi.spyOn(OptionsUtil, 'isEmailEnabled').mockReturnValue(false); vi.spyOn(OptionsUtil, 'isSocialsEnabled').mockReturnValue(false); vi.spyOn(WalletUtil, 'filterOutDuplicateWallets').mockImplementation(w => w); const recommended = [ { id: 'w1', name: 'Wallet 1' }, { id: 'w2', name: 'Wallet 2' }, { id: 'w3', name: 'Wallet 3' } ]; // injected connectors include only Browser Wallet and WalletConnect => injected count = 0 // featured(0) + custom(0) + injected(0) + email(0) + social(0) = 0 => slice 4 const result = ConnectorUtil.getCappedRecommendedWallets(recommended); ConnectorControllerRelative.state.connectors = originalConnectors; OptionsControllerRelative.state.customWallets = originalCustomWallets; OptionsControllerRelative.state.featuredWalletIds = originalFeaturedWalletIds; expect(result.map(w => w.id)).toEqual(['w1', 'w2', 'w3']); }); it('should slice after duplicate filtering', () => { const WC = { id: 'walletConnect', type: 'EXTERNAL', name: 'WalletConnect', chain: { id: 'eip155:1' } }; const originalConnectors = ConnectorControllerRelative.state.connectors; const originalCustomWallets = OptionsControllerRelative.state.customWallets; const originalFeaturedWalletIds = OptionsControllerRelative.state.featuredWalletIds; ConnectorControllerRelative.state.connectors = [WC]; OptionsControllerRelative.state.customWallets = [{ id: 'c1' }]; OptionsControllerRelative.state.featuredWalletIds = []; vi.spyOn(OptionsUtil, 'isEmailEnabled').mockReturnValue(false); vi.spyOn(OptionsUtil, 'isSocialsEnabled').mockReturnValue(false); // Simulate duplicates being removed so only one remains before slice vi.spyOn(WalletUtil, 'filterOutDuplicateWallets').mockReturnValue([ { id: 'w1', name: 'Wallet 1' } ]); const recommended = [ { id: 'w1', name: 'Wallet 1' }, { id: 'w2', name: 'Wallet 2' }, { id: 'w3', name: 'Wallet 3' } ]; // featured(0) + custom(1) + injected(0) + email(0) + social(0) = 1 => slice 3 // but filtered list has length 1, result should be that single wallet const result = ConnectorUtil.getCappedRecommendedWallets(recommended); ConnectorControllerRelative.state.connectors = originalConnectors; OptionsControllerRelative.state.customWallets = originalCustomWallets; OptionsControllerRelative.state.featuredWalletIds = originalFeaturedWalletIds; expect(result.map(w => w.id)).toEqual(['w1']); }); }); }); //# sourceMappingURL=ControllerUtil.test.js.map