UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

565 lines • 28.6 kB
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { SafeLocalStorage, getSafeConnectorIdKey } from '@reown/appkit-common'; import { SafeLocalStorageKeys } from '@reown/appkit-common'; import { W3mFrameConstants, W3mFrameHelpers, W3mFrameStorage } from '@reown/appkit-wallet'; import { StorageUtil } from '../../src/utils/StorageUtil'; const previousLocalStorage = globalThis.localStorage; const previousWindow = globalThis.window; let store = {}; afterAll(() => { Object.assign(globalThis, { localStorage: previousLocalStorage, window: previousWindow }); }); describe('StorageUtil', () => { beforeAll(() => { Object.assign(globalThis, { window: {}, localStorage: { getItem: (key) => store[key] || null, setItem: (key, value) => { store[key] = value.toString(); }, removeItem: (key) => { delete store[key]; }, clear: () => { store = {}; } } }); }); beforeEach(() => { // Clear localStorage before each test SafeLocalStorage.clear(); }); afterEach(() => { // Restore all mocks after each test vi.restoreAllMocks(); }); describe('setWalletConnectDeepLink', () => { it('should set WalletConnect deep link in localStorage', () => { const deepLink = { href: 'https://example.com', name: 'Example Wallet' }; StorageUtil.setWalletConnectDeepLink(deepLink); expect(SafeLocalStorageKeys.DEEPLINK_CHOICE).toBe('WALLETCONNECT_DEEPLINK_CHOICE'); const savedDL = SafeLocalStorage.getItem(SafeLocalStorageKeys.DEEPLINK_CHOICE); expect(savedDL).toBe(JSON.stringify({ href: deepLink.href, name: deepLink.name })); }); it('should handle errors when setting deep link', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); vi.spyOn(localStorage, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.setWalletConnectDeepLink({ href: 'https://example.com', name: 'Example Wallet' }); expect(consoleSpy).toHaveBeenCalledWith('Unable to set WalletConnect deep link'); }); }); describe('getWalletConnectDeepLink', () => { it('should get WalletConnect deep link from localStorage', () => { const deepLink = { href: 'https://example.com', name: 'Example Wallet' }; SafeLocalStorage.setItem(SafeLocalStorageKeys.DEEPLINK_CHOICE, JSON.stringify({ href: deepLink.href, name: deepLink.name })); expect(StorageUtil.getWalletConnectDeepLink()).toEqual({ href: deepLink.href, name: deepLink.name }); }); it('should return undefined if deep link is not set', () => { expect(StorageUtil.getWalletConnectDeepLink()).toBeUndefined(); }); it('should handle errors when getting deep link', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); vi.spyOn(localStorage, 'getItem').mockImplementation(() => { throw new Error('Storage error'); }); expect(StorageUtil.getWalletConnectDeepLink()).toBeUndefined(); expect(consoleSpy).toHaveBeenCalledWith('Unable to get WalletConnect deep link'); }); }); describe('deleteWalletConnectDeepLink', () => { it('should delete WalletConnect deep link from localStorage', () => { SafeLocalStorage.setItem(SafeLocalStorageKeys.DEEPLINK_CHOICE, JSON.stringify({ href: 'https://example.com', name: 'Example' })); StorageUtil.deleteWalletConnectDeepLink(); expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.DEEPLINK_CHOICE)).toBeUndefined(); }); it('should handle errors when deleting deep link', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); vi.spyOn(localStorage, 'removeItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.deleteWalletConnectDeepLink(); expect(consoleSpy).toHaveBeenCalledWith('Unable to delete WalletConnect deep link'); }); }); describe('setAppKitRecent', () => { it('should add a new wallet to recent wallets', () => { const wallet = { id: 'wallet1', name: 'Wallet 1' }; StorageUtil.setAppKitRecent(wallet); expect(StorageUtil.getRecentWallets()).toEqual([wallet]); }); it('should not add duplicate wallets', () => { const wallet = { id: 'wallet1', name: 'Wallet 1' }; StorageUtil.setAppKitRecent(wallet); StorageUtil.setAppKitRecent(wallet); expect(StorageUtil.getRecentWallets()).toEqual([wallet]); }); it('should limit recent wallets to 2', () => { const wallet1 = { id: 'wallet1', name: 'Wallet 1' }; const wallet2 = { id: 'wallet2', name: 'Wallet 2' }; const wallet3 = { id: 'wallet3', name: 'Wallet 3' }; StorageUtil.setAppKitRecent(wallet1); StorageUtil.setAppKitRecent(wallet2); StorageUtil.setAppKitRecent(wallet3); expect(StorageUtil.getRecentWallets()).toEqual([wallet3, wallet2]); }); }); describe('getRecentWallets', () => { it('should return an empty array if no recent wallets', () => { expect(StorageUtil.getRecentWallets()).toEqual([]); }); it('should return recent wallets', () => { const wallet = { id: 'wallet1', name: 'Wallet 1' }; SafeLocalStorage.setItem(SafeLocalStorageKeys.RECENT_WALLETS, JSON.stringify([wallet])); expect(StorageUtil.getRecentWallets()).toEqual([wallet]); }); }); describe('setConnectedConnectorId', () => { it('should set connected connector', () => { const connectorId = 'io.metamask'; StorageUtil.setConnectedConnectorId('eip155', connectorId); const key = getSafeConnectorIdKey('eip155'); expect(SafeLocalStorage.getItem(key)).toBe(connectorId); }); }); describe('getConnectedConnector', () => { it('should get connected connector', () => { const connectorId = 'io.metamask'; const key = getSafeConnectorIdKey('eip155'); SafeLocalStorage.setItem(key, connectorId); expect(StorageUtil.getConnectedConnectorId('eip155')).toBe(connectorId); }); }); describe('setConnectedSocialProvider', () => { it('should set connected social provider', () => { const provider = 'google'; StorageUtil.setConnectedSocialProvider(provider); expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_SOCIAL)).toBe(provider); }); }); describe('getConnectedSocialProvider', () => { it('should get connected social provider', () => { const provider = 'google'; SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_SOCIAL, provider); expect(StorageUtil.getConnectedSocialProvider()).toBe(provider); }); }); describe('getConnectedSocialUsername', () => { it('should set username on W3mFrameStorage and get connected social username', () => { const username = 'testuser'; vi.spyOn(W3mFrameHelpers, 'isClient', 'get').mockReturnValue(true); W3mFrameStorage.set(W3mFrameConstants.SOCIAL_USERNAME, username); expect(StorageUtil.getConnectedSocialUsername()).toBe(username); }); }); describe('setConnections', () => { const mockConnection1 = { connectorId: 'connector1', accounts: [ { address: '0x123...', type: 'eoa' }, { address: '0x456...', type: 'eoa' } ], caipNetwork: { id: 1, name: 'Ethereum', caipNetworkId: 'eip155:1', chainNamespace: 'eip155' } }; const mockConnection2 = { connectorId: 'connector2', accounts: [{ address: '0x789...', type: 'eoa' }], caipNetwork: { id: 1, name: 'Ethereum', caipNetworkId: 'eip155:1', chainNamespace: 'eip155' } }; beforeEach(() => { SafeLocalStorage.clear(); }); it('should set connections for a new namespace', () => { StorageUtil.setConnections([mockConnection1, mockConnection2], 'eip155'); const connections = StorageUtil.getConnections(); expect(connections.eip155).toEqual([mockConnection1, mockConnection2]); }); it('should merge new connections with existing ones', () => { StorageUtil.setConnections([mockConnection1], 'eip155'); StorageUtil.setConnections([mockConnection2], 'eip155'); const connections = StorageUtil.getConnections(); expect(connections.eip155).toHaveLength(2); expect(connections.eip155).toEqual(expect.arrayContaining([mockConnection1, mockConnection2])); }); it('should merge accounts for existing connectors (non-auth)', () => { const existingConnection = { connectorId: 'connector1', accounts: [{ address: '0x123...', type: 'eoa' }] }; const newConnection = { connectorId: 'connector1', accounts: [ { address: '0x123...', type: 'eoa' }, { address: '0x999...', type: 'eoa' } ] }; StorageUtil.setConnections([existingConnection], 'eip155'); StorageUtil.setConnections([newConnection], 'eip155'); const connections = StorageUtil.getConnections(); const connector1 = connections.eip155.find(c => c.connectorId === 'connector1'); expect(connector1?.accounts).toHaveLength(2); expect(connector1?.accounts).toEqual(expect.arrayContaining([ { address: '0x123...', type: 'eoa' }, { address: '0x999...', type: 'eoa' } ])); }); it('should replace auth connections instead of merging', () => { const existingAuthConnection = { connectorId: 'AUTH', accounts: [{ address: '0xold...', type: 'eoa' }] }; const newAuthConnection = { connectorId: 'AUTH', accounts: [{ address: '0xnew...', type: 'eoa' }] }; StorageUtil.setConnections([existingAuthConnection], 'eip155'); StorageUtil.setConnections([newAuthConnection], 'eip155'); const connections = StorageUtil.getConnections(); const authConnector = connections.eip155.find(c => c.connectorId === 'AUTH'); expect(authConnector?.accounts).toEqual([{ address: '0xnew...', type: 'eoa' }]); }); it('should handle case-insensitive address deduplication', () => { const existingConnection = { connectorId: 'connector1', accounts: [{ address: '0x123ABC...', type: 'eoa' }] }; const newConnection = { connectorId: 'connector1', accounts: [{ address: '0x123abc...', type: 'eoa' }] // same address, different case }; StorageUtil.setConnections([existingConnection], 'eip155'); StorageUtil.setConnections([newConnection], 'eip155'); const connections = StorageUtil.getConnections(); const connector1 = connections.eip155.find(c => c.connectorId === 'connector1'); expect(connector1?.accounts).toHaveLength(1); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.setConnections([mockConnection1], 'eip155'); expect(consoleSpy).toHaveBeenCalledWith('Unable to sync connections to storage', expect.any(Error)); }); it('should preserve connections from other namespaces', () => { StorageUtil.setConnections([mockConnection1], 'eip155'); StorageUtil.setConnections([mockConnection2], 'solana'); const connections = StorageUtil.getConnections(); expect(connections.eip155).toEqual([mockConnection1]); expect(connections.solana).toEqual([mockConnection2]); }); }); describe('getConnections', () => { it('should return empty object when no connections stored', () => { const connections = StorageUtil.getConnections(); expect(connections).toEqual({}); }); it('should return stored connections', () => { const mockConnections = { eip155: [{ connectorId: 'connector1', accounts: [] }], solana: [{ connectorId: 'connector2', accounts: [] }] }; SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTIONS, JSON.stringify(mockConnections)); const connections = StorageUtil.getConnections(); expect(connections).toEqual(mockConnections); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(() => { throw new Error('Storage error'); }); const connections = StorageUtil.getConnections(); expect(connections).toEqual({}); expect(consoleSpy).toHaveBeenCalledWith('Unable to get connections from storage', expect.any(Error)); }); it('should handle invalid JSON gracefully', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTIONS, 'invalid-json'); const connections = StorageUtil.getConnections(); expect(connections).toEqual({}); expect(consoleSpy).toHaveBeenCalledWith('Unable to get connections from storage', expect.any(Error)); }); }); describe('deleteAddressFromConnection', () => { const mockConnection = { connectorId: 'connector1', accounts: [ { address: '0x123...', type: 'eoa' }, { address: '0x456...', type: 'eoa' }, { address: '0x789...', type: 'eoa' } ] }; beforeEach(() => { SafeLocalStorage.clear(); StorageUtil.setConnections([mockConnection], 'eip155'); }); it('should remove specific address from connection', () => { StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0x456...', namespace: 'eip155' }); const connections = StorageUtil.getConnections(); const connector = connections.eip155.find(c => c.connectorId === 'connector1'); expect(connector?.accounts).toHaveLength(2); expect(connector?.accounts).not.toContain(expect.objectContaining({ address: '0x456...' })); }); it('should handle case-insensitive address matching', () => { StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0X123...', // uppercase namespace: 'eip155' }); const connections = StorageUtil.getConnections(); const connector = connections.eip155.find(c => c.connectorId === 'connector1'); expect(connector?.accounts).toHaveLength(2); expect(connector?.accounts).not.toContain(expect.objectContaining({ address: '0x123...' })); }); it('should remove entire connector when no accounts left', () => { const singleAccountConnection = { connectorId: 'connector2', accounts: [{ address: '0xsingle...', type: 'eoa' }] }; StorageUtil.setConnections([singleAccountConnection], 'eip155'); StorageUtil.deleteAddressFromConnection({ connectorId: 'connector2', address: '0xsingle...', namespace: 'eip155' }); const connections = StorageUtil.getConnections(); const connector = connections.eip155.find(c => c.connectorId === 'connector2'); expect(connector).toBeUndefined(); }); it('should do nothing when connector not found', () => { const initialConnections = StorageUtil.getConnections(); StorageUtil.deleteAddressFromConnection({ connectorId: 'nonexistent', address: '0x123...', namespace: 'eip155' }); const finalConnections = StorageUtil.getConnections(); expect(finalConnections).toEqual(initialConnections); }); it('should do nothing when address not found', () => { const initialConnections = StorageUtil.getConnections(); StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0xnonexistent...', namespace: 'eip155' }); const finalConnections = StorageUtil.getConnections(); expect(finalConnections).toEqual(initialConnections); }); it('should do nothing when namespace not found', () => { const initialConnections = StorageUtil.getConnections(); StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0x123...', namespace: 'nonexistent' }); const finalConnections = StorageUtil.getConnections(); expect(finalConnections).toEqual({ ...initialConnections, nonexistent: [] }); }); it('should handle storage errors', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0x123...', namespace: 'eip155' }); expect(consoleSpy).toHaveBeenCalledWith('Unable to remove address "0x123..." from connector "connector1" in namespace "eip155"'); }); it('should preserve other namespaces when deleting', () => { StorageUtil.setConnections([mockConnection], 'solana'); StorageUtil.deleteAddressFromConnection({ connectorId: 'connector1', address: '0x123...', namespace: 'eip155' }); const connections = StorageUtil.getConnections(); expect(connections.solana).toEqual([mockConnection]); }); }); describe('getDisconnectedConnectorIds', () => { it('should return empty object when no disconnected connectors', () => { const result = StorageUtil.getDisconnectedConnectorIds(); expect(result).toEqual({}); }); it('should return stored disconnected connector IDs', () => { const mockDisconnectedIds = { eip155: ['connector1', 'connector2'], solana: ['connector3'] }; SafeLocalStorage.setItem(SafeLocalStorageKeys.DISCONNECTED_CONNECTOR_IDS, JSON.stringify(mockDisconnectedIds)); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result).toEqual(mockDisconnectedIds); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(() => { throw new Error('Storage error'); }); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result).toEqual({}); expect(consoleSpy).toHaveBeenCalledWith('Unable to get disconnected connector ids'); }); it('should handle invalid JSON gracefully', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); SafeLocalStorage.setItem(SafeLocalStorageKeys.DISCONNECTED_CONNECTOR_IDS, 'invalid-json'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result).toEqual({}); expect(consoleSpy).toHaveBeenCalledWith('Unable to get disconnected connector ids'); }); }); describe('addDisconnectedConnectorId', () => { beforeEach(() => { SafeLocalStorage.clear(); }); it('should add disconnected connector ID to empty storage', () => { StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector1']); }); it('should add disconnected connector ID to existing namespace', () => { StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector2', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector1', 'connector2']); }); it('should add disconnected connector ID to new namespace', () => { StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector2', 'solana'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector1']); expect(result.solana).toEqual(['connector2']); }); it('should deduplicate connector IDs', () => { StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector1']); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); expect(consoleSpy).toHaveBeenCalledWith('Unable to set disconnected connector id "connector1" for namespace "eip155"'); }); }); describe('removeDisconnectedConnectorId', () => { beforeEach(() => { SafeLocalStorage.clear(); StorageUtil.addDisconnectedConnectorId('connector1', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector2', 'eip155'); StorageUtil.addDisconnectedConnectorId('connector3', 'solana'); }); it('should remove specific disconnected connector ID', () => { StorageUtil.removeDisconnectedConnectorId('connector1', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector2']); expect(result.solana).toEqual(['connector3']); }); it('should handle case-insensitive removal', () => { StorageUtil.removeDisconnectedConnectorId('CONNECTOR1', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual(['connector2']); }); it('should do nothing when connector ID not found', () => { const initialResult = StorageUtil.getDisconnectedConnectorIds(); StorageUtil.removeDisconnectedConnectorId('nonexistent', 'eip155'); const finalResult = StorageUtil.getDisconnectedConnectorIds(); expect(finalResult).toEqual(initialResult); }); it('should do nothing when namespace not found', () => { const initialResult = StorageUtil.getDisconnectedConnectorIds(); StorageUtil.removeDisconnectedConnectorId('connector1', 'nonexistent'); const finalResult = StorageUtil.getDisconnectedConnectorIds(); expect(finalResult).toEqual({ ...initialResult, nonexistent: [] }); }); it('should handle empty namespace', () => { SafeLocalStorage.clear(); StorageUtil.removeDisconnectedConnectorId('connector1', 'eip155'); const result = StorageUtil.getDisconnectedConnectorIds(); expect(result.eip155).toEqual([]); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); vi.spyOn(SafeLocalStorage, 'setItem').mockImplementation(() => { throw new Error('Storage error'); }); StorageUtil.removeDisconnectedConnectorId('connector1', 'eip155'); expect(consoleSpy).toHaveBeenCalledWith('Unable to remove disconnected connector id "connector1" for namespace "eip155"'); }); }); describe('isConnectorDisconnected', () => { beforeEach(() => { SafeLocalStorage.clear(); StorageUtil.addDisconnectedConnectorId('disconnected1', 'eip155'); StorageUtil.addDisconnectedConnectorId('disconnected2', 'eip155'); StorageUtil.addDisconnectedConnectorId('disconnected3', 'solana'); }); it('should return true for disconnected connector', () => { const result = StorageUtil.isConnectorDisconnected('disconnected1', 'eip155'); expect(result).toBe(true); }); it('should return false for connected connector', () => { const result = StorageUtil.isConnectorDisconnected('connected1', 'eip155'); expect(result).toBe(false); }); it('should handle case-insensitive comparison', () => { const result = StorageUtil.isConnectorDisconnected('DISCONNECTED1', 'eip155'); expect(result).toBe(true); }); it('should return false for connector in different namespace', () => { const result = StorageUtil.isConnectorDisconnected('disconnected3', 'eip155'); expect(result).toBe(false); }); it('should return false when namespace not found', () => { const result = StorageUtil.isConnectorDisconnected('disconnected1', 'nonexistent'); expect(result).toBe(false); }); it('should handle storage errors gracefully', () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => { }); vi.spyOn(StorageUtil, 'getDisconnectedConnectorIds').mockImplementation(() => { throw new Error('Storage error'); }); const result = StorageUtil.isConnectorDisconnected('disconnected1', 'eip155'); expect(result).toBe(false); expect(consoleSpy).toHaveBeenCalledWith('Unable to get disconnected connector id "disconnected1" for namespace "eip155"'); }); it('should return false when no disconnected connectors exist', () => { SafeLocalStorage.clear(); const result = StorageUtil.isConnectorDisconnected('any-connector', 'eip155'); expect(result).toBe(false); }); }); }); //# sourceMappingURL=StorageUtil.test.js.map