UNPKG

@reown/appkit-controllers

Version:

#### 🔗 [Website](https://reown.com/appkit)

403 lines • 19.1 kB
import { polygon } from 'viem/chains'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { ConstantsUtil as CommonConstantsUtil, ParseUtil } from '@reown/appkit-common'; import { ChainController, ConnectionController, ConnectionControllerUtil, ConnectorController, ConnectorControllerUtil, ConstantsUtil, CoreHelperUtil } from '../../exports/index.js'; import { AccountController } from '../../exports/index.js'; // -- Setup -------------------------------------------------------------------- const chain = CommonConstantsUtil.CHAIN.EVM; const walletConnectUri = 'wc://uri?=123'; const externalId = 'coinbaseWallet'; const type = 'WALLET_CONNECT'; const caipNetworks = [ { ...polygon, chainNamespace: chain, caipNetworkId: 'eip155:137' } ]; const client = { connectWalletConnect: async () => { }, disconnect: async () => Promise.resolve(), signMessage: async (message) => Promise.resolve(message), estimateGas: async () => Promise.resolve(BigInt(0)), connectExternal: async (_id) => Promise.resolve({ address: '' }), checkInstalled: _id => true, parseUnits: value => BigInt(value), formatUnits: value => value.toString(), sendTransaction: () => Promise.resolve('0x'), writeContract: () => Promise.resolve('0x'), getEnsAddress: async (value) => Promise.resolve(value), getEnsAvatar: async (value) => Promise.resolve(value), getCapabilities: async () => Promise.resolve(''), grantPermissions: async () => Promise.resolve('0x'), revokePermissions: async () => Promise.resolve('0x'), walletGetAssets: async () => Promise.resolve({}), updateBalance: () => Promise.resolve() }; const clientConnectWalletConnectSpy = vi.spyOn(client, 'connectWalletConnect'); const clientConnectExternalSpy = vi.spyOn(client, 'connectExternal'); const clientCheckInstalledSpy = vi.spyOn(client, 'checkInstalled'); const partialClient = { connectWalletConnect: async () => Promise.resolve(), disconnect: async () => Promise.resolve(), estimateGas: async () => Promise.resolve(BigInt(0)), signMessage: async (message) => Promise.resolve(message), parseUnits: value => BigInt(value), formatUnits: value => value.toString(), sendTransaction: () => Promise.resolve('0x'), writeContract: () => Promise.resolve('0x'), getEnsAddress: async (value) => Promise.resolve(value), getEnsAvatar: async (value) => Promise.resolve(value), getCapabilities: async () => Promise.resolve(''), grantPermissions: async () => Promise.resolve('0x'), revokePermissions: async () => Promise.resolve('0x'), walletGetAssets: async () => Promise.resolve({}), updateBalance: () => Promise.resolve() }; const evmAdapter = { namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: client }; const solanaAdapter = { namespace: CommonConstantsUtil.CHAIN.SOLANA, connectionControllerClient: client }; const bip122Adapter = { namespace: CommonConstantsUtil.CHAIN.BITCOIN, connectionControllerClient: client }; const adapters = [evmAdapter, solanaAdapter, bip122Adapter]; // -- Tests -------------------------------------------------------------------- beforeAll(() => { ChainController.initialize(adapters, [], { connectionControllerClient: client, networkControllerClient: vi.fn() }); ConnectionController.setClient(evmAdapter.connectionControllerClient); }); describe('ConnectionController', () => { it('should have valid default state', () => { ChainController.initialize([ { namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: client, caipNetworks } ], caipNetworks, { connectionControllerClient: client, networkControllerClient: vi.fn() }); expect(ConnectionController.state).toEqual({ connections: new Map(), wcError: false, buffering: false, isSwitchingConnection: false, status: 'disconnected', _client: evmAdapter.connectionControllerClient }); }); it('should update state correctly and set wcPromisae on connectWalletConnect()', async () => { const setConnectorIdSpy = vi.spyOn(ConnectorController, 'setConnectorId'); // Await on set promise and check results await ConnectionController.connectWalletConnect(); expect(clientConnectWalletConnectSpy).toHaveBeenCalled(); expect(setConnectorIdSpy).not.toBeCalled(); // Just in case vi.useRealTimers(); }); it('connectExternal() should trigger internal client call and set connector in storage', async () => { const options = { id: externalId, type }; await ConnectionController.connectExternal(options, chain); expect(clientConnectExternalSpy).toHaveBeenCalledWith(options); }); it('checkInstalled() should trigger internal client call', () => { ConnectionController.checkInstalled([externalId]); expect(clientCheckInstalledSpy).toHaveBeenCalledWith([externalId]); }); it('should not throw on checkInstalled() without ids', () => { ConnectionController.checkInstalled(); expect(clientCheckInstalledSpy).toHaveBeenCalledWith(undefined); }); it('should not throw when optional methods are undefined', async () => { ChainController.initialize([ { namespace: CommonConstantsUtil.CHAIN.EVM, connectionControllerClient: partialClient, caipNetworks: [] } ], [], { connectionControllerClient: partialClient, networkControllerClient: vi.fn() }); await ConnectionController.connectExternal({ id: externalId, type }, chain); ConnectionController.checkInstalled([externalId]); expect(clientCheckInstalledSpy).toHaveBeenCalledWith([externalId]); expect(clientCheckInstalledSpy).toHaveBeenCalledWith(undefined); expect(ConnectionController._getClient()).toEqual(evmAdapter.connectionControllerClient); }); it('should update state correctly on resetWcConnection()', () => { ConnectionController.resetWcConnection(); expect(ConnectionController.state.wcUri).toEqual(undefined); expect(ConnectionController.state.wcPairingExpiry).toEqual(undefined); }); it('should set wcUri correctly', () => { // Setup timers for pairing expiry const fakeDate = new Date(0); vi.useFakeTimers(); vi.setSystemTime(fakeDate); ConnectionController.setUri(walletConnectUri); expect(ConnectionController.state.wcUri).toEqual(walletConnectUri); expect(ConnectionController.state.wcPairingExpiry).toEqual(ConstantsUtil.FOUR_MINUTES_MS); }); it('should disconnect correctly', async () => { const disconnectSpy = vi.spyOn(client, 'disconnect'); await ConnectionController.disconnect(); expect(disconnectSpy).toHaveBeenCalled(); }); it('should handle connectWalletConnect correctly on telegram or safari on ios', async () => { const connectWalletConnectSpy = vi.spyOn(client, 'connectWalletConnect'); vi.spyOn(CoreHelperUtil, 'isPairingExpired').mockReturnValue(true); vi.spyOn(CoreHelperUtil, 'isTelegram').mockReturnValue(true); vi.spyOn(CoreHelperUtil, 'isSafari').mockReturnValue(true); vi.spyOn(CoreHelperUtil, 'isIos').mockReturnValue(true); expect(ConnectionController.state.status).toEqual('disconnected'); await ConnectionController.connectWalletConnect(); expect(connectWalletConnectSpy).toHaveBeenCalledTimes(1); expect(ConnectionController.state.status).toEqual('connected'); }); it('should set connections for a namespace', () => { const connections = [{ connectorId: 'test-connector', accounts: [{ address: '0x123' }] }]; ConnectionController.setConnections(connections, chain); expect(ConnectionController.state.connections.get(chain)).toEqual(connections); }); it('should overwrite existing connections for a namespace', () => { const initialConnections = [ { connectorId: 'initial-connector', accounts: [{ address: '0xabc' }] } ]; const newConnections = [{ connectorId: 'new-connector', accounts: [{ address: '0xdef' }] }]; ConnectionController.setConnections(initialConnections, chain); ConnectionController.setConnections(newConnections, chain); expect(ConnectionController.state.connections.get(chain)).toEqual(newConnections); }); describe('switchConnection', () => { const mockConnection = { connectorId: 'test-connector', accounts: [{ address: '0x123' }, { address: '0x456' }], name: 'Test Wallet', icon: 'test-icon.png' }; const mockConnector = { id: 'test-connector', type: 'INJECTED', name: 'Test Connector', chain: chain }; beforeEach(() => { vi.clearAllMocks(); vi.spyOn(ConnectionControllerUtil, 'validateAccountSwitch').mockImplementation(() => { }); }); it('should call validateAccountSwitch before proceeding to switching', async () => { vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); const validateSpy = vi.spyOn(ConnectionControllerUtil, 'validateAccountSwitch'); await ConnectionController.switchConnection({ connection: mockConnection, address: '0x123', namespace: chain }); expect(validateSpy).toHaveBeenCalledWith({ namespace: chain, connection: mockConnection, address: '0x123' }); }); it('should call parseCaipAddress when caipAddress is available', async () => { const mockCaipAddress = 'eip155:137:0x789'; vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue(mockCaipAddress); const parseSpy = vi.spyOn(ParseUtil, 'parseCaipAddress'); await ConnectionController.switchConnection({ connection: mockConnection, namespace: chain }); expect(AccountController.getCaipAddress).toHaveBeenCalledWith(chain); expect(parseSpy).toHaveBeenCalledWith(mockCaipAddress); }); it('should not call parseCaipAddress when caipAddress is not available', async () => { vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue(undefined); const parseSpy = vi.spyOn(ParseUtil, 'parseCaipAddress'); await ConnectionController.switchConnection({ connection: mockConnection, namespace: chain }); expect(AccountController.getCaipAddress).toHaveBeenCalledWith(chain); expect(parseSpy).not.toHaveBeenCalled(); }); it.each([ { address: '0x123', hasSwitchedAccount: true, hasSwitchedWallet: true, status: 'active' }, { address: '0x321', hasSwitchedAccount: false, hasSwitchedWallet: true, status: 'active' }, { address: '0x123', hasSwitchedAccount: true, hasSwitchedWallet: false, status: 'connected' }, { address: '0x321', hasSwitchedAccount: false, hasSwitchedWallet: false, status: 'connected' } ])('should handle active and connected connection when switching to different addresses', async ({ address, hasSwitchedAccount, hasSwitchedWallet, status }) => { vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue(status); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue('eip155:137:0x321'); const connectExternalSpy = vi .spyOn(ConnectionController, 'connectExternal') .mockResolvedValue({ address }); const onChange = vi.fn(); await ConnectionController.switchConnection({ connection: mockConnection, address, namespace: chain, onChange }); expect(connectExternalSpy).toHaveBeenCalledWith({ id: mockConnector.id, type: mockConnector.type, provider: mockConnector.provider, address, chain }, chain); expect(onChange).toHaveBeenCalledWith({ address, namespace: chain, hasSwitchedAccount, hasSwitchedWallet }); }); it.each(['active', 'connected'])('should handle auth account switch for %s connection status', async (status) => { vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue(status); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue('eip155:137:0x321'); const onChange = vi.fn(); vi.spyOn(ConnectionController, 'connectExternal').mockResolvedValue({ address: '0x123' }); const handleAuthAccountSwitchSpy = vi.spyOn(ConnectionController, 'handleAuthAccountSwitch'); await ConnectionController.switchConnection({ connection: { ...mockConnection, connectorId: CommonConstantsUtil.CONNECTOR_ID.AUTH }, address: '0x123', namespace: chain, onChange }); expect(handleAuthAccountSwitchSpy).toHaveBeenCalledWith({ address: '0x123', connection: { ...mockConnection, connectorId: CommonConstantsUtil.CONNECTOR_ID.AUTH }, namespace: chain }); }); it('should handle disconnected connection when trying to connect with external connector', async () => { const address = '0x321'; vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue('disconnected'); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue(`eip155:137:${address}`); const connectExternalSpy = vi .spyOn(ConnectionController, 'connectExternal') .mockResolvedValue({ address }); const onChange = vi.fn(); await ConnectionController.switchConnection({ connection: mockConnection, address, namespace: chain, onChange }); expect(connectExternalSpy).toHaveBeenCalledWith({ id: mockConnector.id, type: mockConnector.type, provider: mockConnector.provider, chain }, chain); expect(onChange).toHaveBeenCalledWith({ address, namespace: chain, hasSwitchedAccount: true, hasSwitchedWallet: true }); }); it.each(['google', 'x', 'discord', 'github', 'apple', 'facebook', 'farcaster'])('should handle disconnected connection when trying to connect with %s', async (social) => { const address = '0x321'; vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue('disconnected'); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); const connectSocialSpy = vi .spyOn(ConnectorControllerUtil, 'connectSocial') .mockResolvedValue({ address: '0x321' }); const onChange = vi.fn(); await ConnectionController.switchConnection({ connection: { ...mockConnection, auth: { name: social, username: undefined }, connectorId: CommonConstantsUtil.CONNECTOR_ID.AUTH }, address, namespace: chain, onChange }); expect(connectSocialSpy).toHaveBeenCalledWith({ social, onOpenFarcaster: expect.any(Function), onConnect: expect.any(Function) }); expect(onChange).toHaveBeenCalledWith({ address, namespace: chain, hasSwitchedAccount: true, hasSwitchedWallet: true }); }); it('should handle disconnected connection when trying to connect with email', async () => { const address = '0x321'; vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue('disconnected'); vi.spyOn(ConnectorController, 'getConnectorById').mockReturnValue(mockConnector); const connectEmailSpy = vi .spyOn(ConnectorControllerUtil, 'connectEmail') .mockResolvedValue({ address: '0x321' }); const onChange = vi.fn(); await ConnectionController.switchConnection({ connection: { ...mockConnection, auth: { name: 'email', username: undefined }, connectorId: CommonConstantsUtil.CONNECTOR_ID.AUTH }, address, namespace: chain, onChange }); expect(connectEmailSpy).toHaveBeenCalledWith({ onOpen: expect.any(Function), onConnect: expect.any(Function) }); expect(onChange).toHaveBeenCalledWith({ address, namespace: chain, hasSwitchedAccount: true, hasSwitchedWallet: true }); }); it('should throw error if connection status is invalid', async () => { vi.spyOn(AccountController, 'getCaipAddress').mockReturnValue(undefined); vi.spyOn(ConnectionControllerUtil, 'getConnectionStatus').mockReturnValue('connecting'); vi.spyOn(ConnectionController, 'handleActiveConnection').mockResolvedValue('0x123'); const onChange = vi.fn(); expect(async () => await ConnectionController.switchConnection({ connection: mockConnection, namespace: chain, onChange })).rejects.toThrow('Invalid connection status: connecting'); }); }); }); //# sourceMappingURL=ConnectionController.test.js.map