Skip to content

Commit

Permalink
test(wallet): Add unit tests for useOnboard hook
Browse files Browse the repository at this point in the history
  • Loading branch information
nevendyulgerov authored and brunomenezes committed Jun 19, 2023
1 parent 2477a01 commit 5ef5aae
Show file tree
Hide file tree
Showing 2 changed files with 332 additions and 16 deletions.
342 changes: 329 additions & 13 deletions packages/wallet/__tests__/useOnboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,83 @@
// This program is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU General Public License for more details.
import { buildConfig, checkNetwork } from '../src/useOnboard';
import { networks } from '@explorer/utils';

import { renderHook, waitFor, cleanup } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import {
buildConfig,
checkNetwork,
convertToHex,
getWalletType,
handlerBuilder,
useOnboard,
} from '../src/useOnboard';
import { Network, networks } from '@explorer/utils';
import { UnsupportedNetworkError } from '../src';
import { WalletType } from '../src/definitions';
import Onboard, {
EIP1193Provider,
OnboardAPI,
WalletState,
} from '@web3-onboard/core';
import { ConnectedChain } from '@web3-onboard/core/dist/types';

jest.mock('ethers');

jest.mock('@unleash/proxy-client-react', () => ({
useUnleashContext: () => jest.fn(),
useFlag: jest.fn(),
}));

jest.mock('@web3-onboard/core', () => {
const originalModule = jest.requireActual('@web3-onboard/core');
return {
__esModule: true,
...originalModule,
default: jest.fn(),
};
});

const defaultChainIds = Object.keys(networks).map(
(key) => `0x${Number(key).toString(16)}`
);
const defaultAppMetadata: Record<string, string> = {
name: 'Cartesi Explorer App',
description: 'A place where you can stake CTSI and much more...',
};

const chain = {
id: `0x${convertToHex(Network.GOERLI)}`,
namespace: 'ETH',
} as unknown as ConnectedChain;

const account = {
address: '0x907eA0e65Ecf3af503007B382E1280Aeb46104ad',
ens: null,
balance: null,
};

const wallet = {
provider: 'Some provider' as unknown as EIP1193Provider,
label: 'ledger',
chains: [chain],
accounts: [account],
} as WalletState;

const mockedOnboard = Onboard as jest.MockedFunction<typeof Onboard>;

describe('useOnBoard', () => {
const initialEnv = process.env;

afterAll(() => {
process.env = initialEnv;
});

afterEach(() => {
jest.clearAllMocks();
cleanup();
});

it('should build config with correct chains', () => {
const chainIds = Object.keys(networks)
.slice(0, 2)
Expand All @@ -26,25 +98,30 @@ describe('useOnBoard', () => {
});

it('should build config with correct appMetadata', () => {
const chainIds = Object.keys(networks).map(
(key) => `0x${Number(key).toString(16)}`
);
const appMetadata: Record<string, string> = {
name: 'Cartesi Explorer App',
description: 'A place where you can stake CTSI and much more...',
};
const config = buildConfig(false, chainIds, appMetadata);
const config = buildConfig(false, defaultChainIds, defaultAppMetadata);

expect(config.appMetadata?.name).toBe(appMetadata.name);
Object.keys(appMetadata).forEach((appMetadataKey) => {
expect(config.appMetadata?.name).toBe(defaultAppMetadata.name);
Object.keys(defaultAppMetadata).forEach((appMetadataKey) => {
expect(
(config.appMetadata as unknown as Record<string, string>)?.[
appMetadataKey
]
).toBe(appMetadata[appMetadataKey]);
).toBe(defaultAppMetadata[appMetadataKey]);
});
});

it('should build config with correct accountCenter', () => {
const config = buildConfig(false, defaultChainIds, defaultAppMetadata);

expect(config.accountCenter?.desktop.enabled).toBe(false);
expect(config.accountCenter?.mobile.enabled).toBe(false);
});

it('should build config with correct number of wallets', () => {
const config = buildConfig(false, defaultChainIds, defaultAppMetadata);
expect(config.wallets.length).toBe(5);
});

it('should generate error when unsupported network is selected', () => {
const [firstNetwork] = Object.keys(networks).map(Number);
const chainIds = Object.keys(networks).slice(1, 3).map(Number);
Expand All @@ -60,4 +137,243 @@ describe('useOnBoard', () => {

expect(error).toBe(null);
});

it('should correctly convert number to hex string', () => {
const prefix = '0x';
expect(`${prefix}${convertToHex(Network.MAINNET)}`).toBe(`${prefix}1`);
});

it('should get correct wallet type', () => {
const injectedWallets = new Set(['metamask', 'coinbase']);
for (const value of injectedWallets) {
expect(getWalletType(value)).toBe(WalletType.INJECTED);
}

const hardwareWallets = new Set(['ledger']);
for (const value of hardwareWallets) {
expect(getWalletType(value)).toBe(WalletType.HARDWARE);
}

const sdkWallets = new Set(['gnosis safe', 'safe']);
for (const value of sdkWallets) {
expect(getWalletType(value)).toBe(WalletType.SDK);
}
});

it('should correctly check if network is supported', () => {
const chainIdsAsNumbers = defaultChainIds.map((id) => parseInt(id, 16));
const chainId = parseInt(defaultChainIds[0]);

expect(checkNetwork(chainId, chainIdsAsNumbers)).toBe(null);

expect(
checkNetwork(10, chainIdsAsNumbers)
?.toString()
.includes('UnsupportedNetworkError')
).toBe(true);
});

it('should correctly build state when wallets are defined', () => {
let expectedState = null;
const callback = (state: any) => {
expectedState = state();
return undefined;
};
const handler = handlerBuilder(callback, defaultChainIds);
const wallets: WalletState[] = [wallet];

handler(wallets);
expect(expectedState).toStrictEqual({
error: null,
account: account.address?.toLowerCase(),
chainId: parseInt(chain.id),
isHardwareWallet: true,
isGnosisSafe: false,
walletType: WalletType.HARDWARE,
walletLabel: wallet.label,
});
});

it('should correctly build state when wallets are undefined', () => {
let expectedState = null;
const callback = (state: any) => {
expectedState = state();
return undefined;
};
const handler = handlerBuilder(callback, defaultChainIds);

handler([]);
expect(expectedState).toStrictEqual({});
});

it('should subscribe wallets after setting valid onboard', async () => {
const wallets: WalletState[] = [wallet];
let isSubscribed = false;
const onboard = {
connectWallet: () => Promise.resolve(wallets),
disconnectWallet: () => Promise.resolve(wallets),
setChain: jest.fn(),
state: {
get: () => ({
wallets,
}),
select: (type: any) => {
if (type === 'wallets') {
return {
subscribe: () => {
isSubscribed = true;
return {
unsubscribe: jest.fn(),
};
},
};
}
},
},
} as unknown as OnboardAPI;
mockedOnboard.mockReturnValue(onboard);

renderHook(() =>
useOnboard({
chainIds: defaultChainIds,
appMetaData: defaultAppMetadata,
})
);

await waitFor(() => {
expect(isSubscribed).toBe(true);
});
});

it('should correctly disconnect wallet', async () => {
const wallets: WalletState[] = [wallet];
let isDisconnectWalletCalled = false;
const onboard = {
connectWallet: () => Promise.resolve(wallets),
disconnectWallet: () => {
isDisconnectWalletCalled = true;
return Promise.resolve(wallets);
},
setChain: jest.fn(),
state: {
get: () => ({
wallets,
}),
select: (type: any) => {
if (type === 'wallets') {
return {
subscribe: () => ({
unsubscribe: jest.fn(),
}),
};
}
},
},
} as unknown as OnboardAPI;
mockedOnboard.mockReturnValue(onboard);

const { result } = renderHook(() =>
useOnboard({
chainIds: defaultChainIds,
appMetaData: defaultAppMetadata,
})
);

await act(async () => {
await result.current.disconnectWallet();
});

await waitFor(() => {
expect(isDisconnectWalletCalled).toBe(true);
});
});

it('should correctly connect wallet', async () => {
const wallets: WalletState[] = [wallet];
let isConnectWalledCalled = false;
const onboard = {
connectWallet: () => {
isConnectWalledCalled = true;
return Promise.resolve(wallets);
},
disconnectWallet: () => Promise.resolve(wallets),
setChain: jest.fn(),
state: {
get: () => ({
wallets,
}),
select: (type: any) => {
if (type === 'wallets') {
return {
subscribe: () => ({
unsubscribe: jest.fn(),
}),
};
}
},
},
} as unknown as OnboardAPI;
mockedOnboard.mockReturnValue(onboard);

const { result } = renderHook(() =>
useOnboard({
chainIds: defaultChainIds,
appMetaData: defaultAppMetadata,
})
);

await act(async () => {
await result.current.connectWallet();
});

await waitFor(() => {
expect(isConnectWalledCalled).toBe(true);
});
});

it('should correctly set chain to MainNet', async () => {
process.env = {
NODE_ENV: 'production',
};

const wallets: WalletState[] = [wallet];
const mockedSetChain = jest.fn();
const onboard = {
connectWallet: () => Promise.resolve(wallets),
disconnectWallet: () => Promise.resolve(wallets),
setChain: mockedSetChain,
state: {
get: () => ({
wallets,
}),
select: (type: any) => {
if (type === 'wallets') {
return {
subscribe: () => ({
unsubscribe: jest.fn(),
}),
};
}
},
},
} as unknown as OnboardAPI;
mockedOnboard.mockReturnValue(onboard);

const { result } = renderHook(() =>
useOnboard({
chainIds: defaultChainIds,
appMetaData: defaultAppMetadata,
})
);

await act(async () => {
await result.current.connectWallet();
});

await waitFor(() => {
expect(mockedSetChain).toHaveBeenCalledWith({
chainId: `0x${convertToHex(Network.MAINNET)}`,
});
});
});
});
6 changes: 3 additions & 3 deletions packages/wallet/src/useOnboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const walletConnect = walletConnectModule();
const coinbase = coinbaseWalletModule({ darkMode: true });
const gnosis = gnosisModule();

const convertToHex = (num: number): string => num.toString(16);
export const convertToHex = (num: number): string => num.toString(16);

export const buildConfig = (
ankrEnabled: boolean,
Expand Down Expand Up @@ -119,7 +119,7 @@ export const buildConfig = (
/**
* Check what type of wallet is based on its label e.g. Ledger
*/
const getWalletType = (label = ''): WalletType | null => {
export const getWalletType = (label = ''): WalletType | null => {
const name = label.toLocaleLowerCase();
return injectedWallets.has(name)
? WalletType.INJECTED
Expand All @@ -145,7 +145,7 @@ export const checkNetwork = (
return error;
};

const handlerBuilder =
export const handlerBuilder =
(stateUpdateCb: Dispatch<SetStateAction<PropState>>, chainIds: string[]) =>
(wallets: WalletState[]) => {
const [connectedWallet] = wallets;
Expand Down

0 comments on commit 5ef5aae

Please sign in to comment.