@reown/appkit-controllers
Version:
The full stack toolkit to build onchain app UX.
226 lines • 11.5 kB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiController } from '../../src/controllers/ApiController.js';
import { ChainController } from '../../src/controllers/ChainController.js';
import { ConnectionController } from '../../src/controllers/ConnectionController.js';
import { ConnectorController } from '../../src/controllers/ConnectorController.js';
import { OptionsController } from '../../src/controllers/OptionsController.js';
import { PublicStateController } from '../../src/controllers/PublicStateController.js';
import { ConnectUtil } from '../../src/utils/ConnectUtil.js';
import { ConnectionControllerUtil } from '../../src/utils/ConnectionControllerUtil.js';
import { ConnectorControllerUtil } from '../../src/utils/ConnectorControllerUtil.js';
import { CoreHelperUtil } from '../../src/utils/CoreHelperUtil.js';
import { HeadlessWalletUtil } from '../../src/utils/HeadlessWalletUtil.js';
import { MobileWalletUtil } from '../../src/utils/MobileWallet.js';
const injectedWallet = {
id: 'mm',
name: 'MetaMask',
imageUrl: 'icon',
connectors: [{ id: 'io.metamask', chain: 'eip155' }],
walletInfo: {},
isInjected: true,
isRecent: false
};
const apiWallet = {
id: 'wc_wallet',
name: 'Some Wallet',
imageUrl: 'icon',
connectors: [],
walletInfo: {},
isInjected: false,
isRecent: false
};
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(ChainController, 'state', 'get').mockReturnValue({
activeChain: 'eip155'
});
vi.spyOn(PublicStateController, 'set').mockImplementation(vi.fn());
});
describe('HeadlessWalletUtil.fetchWallets', () => {
it('searches when a search term is given', async () => {
const searchWallet = vi
.spyOn(ApiController, 'searchWallet')
.mockResolvedValue(undefined);
const fetchByPage = vi.spyOn(ApiController, 'fetchWalletsByPage');
await HeadlessWalletUtil.fetchWallets({ search: 'rainbow', includePayOnly: true });
expect(searchWallet).toHaveBeenCalledWith({ includePayOnly: true, search: 'rainbow' });
expect(fetchByPage).not.toHaveBeenCalled();
});
it('maps the deprecated `query` to `search`', async () => {
const searchWallet = vi
.spyOn(ApiController, 'searchWallet')
.mockResolvedValue(undefined);
await HeadlessWalletUtil.fetchWallets({ query: 'rbw' });
expect(searchWallet).toHaveBeenCalledWith({ search: 'rbw' });
});
it('lists from page 1 and clears prior search when no search term', async () => {
const fetchByPage = vi
.spyOn(ApiController, 'fetchWalletsByPage')
.mockResolvedValue(undefined);
ApiController.state.search = [{ id: 'stale' }];
await HeadlessWalletUtil.fetchWallets({ sort: 'wcpay' });
expect(ApiController.state.search).toEqual([]);
expect(fetchByPage).toHaveBeenCalledWith({ page: 1, sort: 'wcpay' });
});
});
describe('HeadlessWalletUtil.getWalletList', () => {
it('projects the initial + WalletConnect lists with pagination', () => {
vi.spyOn(ConnectUtil, 'getInitialWallets').mockReturnValue([injectedWallet]);
vi.spyOn(ConnectUtil, 'getWalletConnectWallets').mockReturnValue([apiWallet]);
ApiController.state.page = 2;
ApiController.state.count = 99;
expect(HeadlessWalletUtil.getWalletList()).toEqual({
wallets: [injectedWallet],
wcWallets: [apiWallet],
page: 2,
count: 99
});
});
});
describe('HeadlessWalletUtil.connect', () => {
it('connects an injected wallet via its connector', async () => {
const connector = { id: 'io.metamask' };
vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(connector);
const connectExternal = vi
.spyOn(ConnectorControllerUtil, 'connectExternal')
.mockResolvedValue(undefined);
await HeadlessWalletUtil.connect(injectedWallet, 'eip155');
expect(PublicStateController.set).toHaveBeenCalledWith({ connectingWallet: injectedWallet });
expect(connectExternal).toHaveBeenCalledWith(connector);
});
it('falls back to a connector found by wallet id (API wallet)', async () => {
const fallback = { id: 'baseAccount', explorerId: 'wc_wallet' };
// apiWallet has no connectors, so the first (guarded) lookup is skipped — only the
// fallback lookup by wallet id runs, and it resolves the Base Account connector.
const getConnector = vi
.spyOn(ConnectorController, 'getConnector')
.mockReturnValue(fallback);
const connectExternal = vi
.spyOn(ConnectorControllerUtil, 'connectExternal')
.mockResolvedValue(undefined);
await HeadlessWalletUtil.connect(apiWallet, 'eip155');
expect(getConnector).toHaveBeenCalledTimes(1);
expect(getConnector).toHaveBeenCalledWith({ id: 'wc_wallet', namespace: 'eip155' });
expect(connectExternal).toHaveBeenCalledWith(fallback);
});
it('deeplinks on mobile when the wallet declares a mobile link', async () => {
vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined);
vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true);
vi.spyOn(ConnectUtil, 'mapWalletItemToWcWallet').mockReturnValue({
id: 'wc_wallet',
name: 'Some Wallet',
mobile_link: 'somewallet://'
});
const onConnectMobile = vi
.spyOn(ConnectionControllerUtil, 'onConnectMobile')
.mockImplementation(vi.fn());
await HeadlessWalletUtil.connect(apiWallet, 'eip155', { wcPayUrl: 'https://pay/x' });
expect(onConnectMobile).toHaveBeenCalledWith(expect.objectContaining({ mobile_link: 'somewallet://' }), 'https://pay/x');
});
it('redirects via MobileWalletUtil on mobile when there is no mobile link', async () => {
vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined);
vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(true);
vi.spyOn(ConnectUtil, 'mapWalletItemToWcWallet').mockReturnValue({
id: 'wc_wallet',
name: 'Some Wallet'
});
vi.spyOn(OptionsController, 'state', 'get').mockReturnValue({ enableCoinbase: true });
const redirect = vi
.spyOn(MobileWalletUtil, 'handleMobileDeeplinkRedirect')
.mockImplementation(vi.fn());
await HeadlessWalletUtil.connect(apiWallet, 'eip155');
expect(redirect).toHaveBeenCalledWith('wc_wallet', 'eip155', { isCoinbaseDisabled: false });
});
it('falls back to WalletConnect on desktop', async () => {
vi.spyOn(ConnectorController, 'getConnector').mockReturnValue(undefined);
vi.spyOn(CoreHelperUtil, 'isMobile').mockReturnValue(false);
const connectWc = vi
.spyOn(ConnectionController, 'connectWalletConnect')
.mockResolvedValue(undefined);
await HeadlessWalletUtil.connect(apiWallet, 'eip155');
expect(connectWc).toHaveBeenCalledWith({ cache: 'never' });
});
it('clears connectingWallet and rethrows on failure', async () => {
vi.spyOn(ConnectorController, 'getConnector').mockReturnValue({ id: 'io.metamask' });
vi.spyOn(ConnectorControllerUtil, 'connectExternal').mockRejectedValue(new Error('boom'));
await expect(HeadlessWalletUtil.connect(injectedWallet, 'eip155')).rejects.toThrow('boom');
expect(PublicStateController.set).toHaveBeenLastCalledWith({ connectingWallet: undefined });
});
it('falls back to the active chain when no namespace is given', async () => {
const getConnector = vi
.spyOn(ConnectorController, 'getConnector')
.mockReturnValue({ id: 'io.metamask' });
vi.spyOn(ConnectorControllerUtil, 'connectExternal').mockResolvedValue(undefined);
await HeadlessWalletUtil.connect(injectedWallet);
expect(getConnector).toHaveBeenCalledWith(expect.objectContaining({ namespace: 'eip155' }));
});
});
describe('HeadlessWalletUtil.prefetchWalletConnectUri', () => {
it('resets then fetches the URI with auto cache', async () => {
const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn());
const setWcLinking = vi.spyOn(ConnectionController, 'setWcLinking').mockImplementation(vi.fn());
const connectWc = vi
.spyOn(ConnectionController, 'connectWalletConnect')
.mockResolvedValue(undefined);
await HeadlessWalletUtil.prefetchWalletConnectUri();
expect(resetUri).toHaveBeenCalled();
expect(setWcLinking).toHaveBeenCalledWith(undefined);
expect(connectWc).toHaveBeenCalledWith({ cache: 'auto' });
});
});
describe('HeadlessWalletUtil.getWalletConnectUri', () => {
it('reads wcUri / wcError / wcFetchingUri from the connection layer', () => {
ConnectionController.state.wcUri = 'wc:read-test';
ConnectionController.state.wcError = true;
ConnectionController.state.wcFetchingUri = false;
expect(HeadlessWalletUtil.getWalletConnectUri()).toEqual({
wcUri: 'wc:read-test',
wcError: true,
wcFetchingUri: false
});
});
it('coerces a missing wcError to a boolean', () => {
ConnectionController.state.wcUri = undefined;
ConnectionController.state.wcError = undefined;
ConnectionController.state.wcFetchingUri = true;
expect(HeadlessWalletUtil.getWalletConnectUri()).toEqual({
wcUri: undefined,
wcError: false,
wcFetchingUri: true
});
});
});
describe('HeadlessWalletUtil.subscribeWalletConnectUri', () => {
it('fires on wcUri / wcError / wcFetchingUri changes and unsubscribes cleanly', async () => {
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
const callback = vi.fn();
// Establish a known baseline before subscribing so each mutation below is a real change.
ConnectionController.state.wcUri = undefined;
ConnectionController.state.wcFetchingUri = false;
const unsubscribe = HeadlessWalletUtil.subscribeWalletConnectUri(callback);
ConnectionController.state.wcUri = 'wc:sub-test';
await flush();
expect(callback).toHaveBeenCalledTimes(1);
ConnectionController.state.wcFetchingUri = true;
await flush();
expect(callback).toHaveBeenCalledTimes(2);
unsubscribe();
ConnectionController.state.wcUri = 'wc:after-unsub';
await flush();
expect(callback).toHaveBeenCalledTimes(2);
});
});
describe('HeadlessWalletUtil resets', () => {
it('resetWcUri resets the URI + linking', () => {
const resetUri = vi.spyOn(ConnectionController, 'resetUri').mockImplementation(vi.fn());
const setWcLinking = vi.spyOn(ConnectionController, 'setWcLinking').mockImplementation(vi.fn());
HeadlessWalletUtil.resetWcUri();
expect(resetUri).toHaveBeenCalled();
expect(setWcLinking).toHaveBeenCalledWith(undefined);
});
it('resetConnectingWallet clears the connecting wallet', () => {
HeadlessWalletUtil.resetConnectingWallet();
expect(PublicStateController.set).toHaveBeenCalledWith({ connectingWallet: undefined });
});
});
//# sourceMappingURL=HeadlessWalletUtil.test.js.map