@reown/appkit-controllers
Version:
#### 🔗 [Website](https://reown.com/appkit)
403 lines • 19.1 kB
JavaScript
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