From 19fc54df3b6b6f103ba29e56b67ffbe146f1ac40 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Wed, 5 Apr 2023 14:23:31 +0400 Subject: [PATCH] feat: export and share MockEip1193 provider and use it throughout the test suite (#793) * feat: export mock and update tests * chore: add todo * chore: bring back comment * chore * chore * chore * chore * chore: update packages/core/src/mocks.ts Co-authored-by: Zach Pomerantz --------- Co-authored-by: Zach Pomerantz --- packages/coinbase-wallet/src/index.spec.ts | 2 +- packages/core/src/index.tsx | 1 + .../src/mock.ts => core/src/mocks.ts} | 8 +- packages/core/tsconfig.json | 1 - packages/eip1193/src/index.spec.ts | 2 +- packages/metamask/src/index.spec.ts | 2 +- packages/walletconnect-v2/src/index.spec.ts | 88 +++++++++++-------- packages/walletconnect/src/index.spec.ts | 33 ++----- tsconfig.json | 3 +- 9 files changed, 72 insertions(+), 68 deletions(-) rename packages/{eip1193/src/mock.ts => core/src/mocks.ts} (83%) diff --git a/packages/coinbase-wallet/src/index.spec.ts b/packages/coinbase-wallet/src/index.spec.ts index b84a39e..62a6d94 100644 --- a/packages/coinbase-wallet/src/index.spec.ts +++ b/packages/coinbase-wallet/src/index.spec.ts @@ -1,7 +1,7 @@ import { createWeb3ReactStoreAndActions } from '@web3-react/store' import type { Actions, Web3ReactStore } from '@web3-react/types' import { CoinbaseWallet } from '.' -import { MockEIP1193Provider } from '../../eip1193/src/mock' +import { MockEIP1193Provider } from '@web3-react/core' jest.mock( '@coinbase/wallet-sdk', diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 00620ab..07a2857 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -1,2 +1,3 @@ export * from './hooks' +export * from './mocks' export * from './provider' diff --git a/packages/eip1193/src/mock.ts b/packages/core/src/mocks.ts similarity index 83% rename from packages/eip1193/src/mock.ts rename to packages/core/src/mocks.ts index 6d45378..28b5811 100644 --- a/packages/eip1193/src/mock.ts +++ b/packages/core/src/mocks.ts @@ -1,11 +1,11 @@ import type { ProviderRpcError, RequestArguments } from '@web3-react/types' import { EventEmitter } from 'eventemitter3' -export class MockEIP1193Provider extends EventEmitter { - public chainId?: string +export class MockEIP1193Provider extends EventEmitter { + public chainId?: T public accounts?: string[] - public eth_chainId = jest.fn((chainId?: string) => chainId) + public eth_chainId = jest.fn((chainId?: T) => chainId) public eth_accounts = jest.fn((accounts?: string[]) => accounts) public eth_requestAccounts = jest.fn((accounts?: string[]) => accounts) @@ -21,7 +21,7 @@ export class MockEIP1193Provider extends EventEmitter { case 'eth_requestAccounts': return Promise.resolve(this.eth_requestAccounts(this.accounts)) default: - throw new Error() + throw new Error(`Method not supported on mock: ${JSON.stringify(x)}`) } } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 214416c..67531bb 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "include": ["./src"], "compilerOptions": { - "jsx": "react", "outDir": "./dist" } } diff --git a/packages/eip1193/src/index.spec.ts b/packages/eip1193/src/index.spec.ts index 4f72407..f02d1f0 100644 --- a/packages/eip1193/src/index.spec.ts +++ b/packages/eip1193/src/index.spec.ts @@ -1,9 +1,9 @@ import { Eip1193Bridge } from '@ethersproject/experimental' import { Web3Provider } from '@ethersproject/providers' import { createWeb3ReactStoreAndActions } from '@web3-react/store' +import { MockEIP1193Provider } from '@web3-react/core' import type { Actions, Web3ReactStore } from '@web3-react/types' import { EIP1193 } from '.' -import { MockEIP1193Provider } from './mock' class MockProviderRpcError extends Error { public code: number diff --git a/packages/metamask/src/index.spec.ts b/packages/metamask/src/index.spec.ts index 0cf1237..8402b99 100644 --- a/packages/metamask/src/index.spec.ts +++ b/packages/metamask/src/index.spec.ts @@ -1,7 +1,7 @@ import { createWeb3ReactStoreAndActions } from '@web3-react/store' import type { Actions, Web3ReactStore } from '@web3-react/types' import { MetaMask } from '.' -import { MockEIP1193Provider } from '../../eip1193/src/mock' +import { MockEIP1193Provider } from '@web3-react/core' const chainId = '0x1' const accounts: string[] = ['0x0000000000000000000000000000000000000000'] diff --git a/packages/walletconnect-v2/src/index.spec.ts b/packages/walletconnect-v2/src/index.spec.ts index 2de21cf..f4f2cc8 100644 --- a/packages/walletconnect-v2/src/index.spec.ts +++ b/packages/walletconnect-v2/src/index.spec.ts @@ -2,10 +2,9 @@ global.TextEncoder = jest.fn() global.TextDecoder = jest.fn() -// We are not using Web3Modal and it is not available in the `node` environment either -jest.mock('@web3modal/standalone', () => ({ Web3Modal: jest.fn().mockImplementation() })) - import { createWeb3ReactStoreAndActions } from '@web3-react/store' +import { MockEIP1193Provider } from '@web3-react/core' +import { RequestArguments } from '@web3-react/types' import { EthereumProvider } from '@walletconnect/ethereum-provider' import { WalletConnect, WalletConnectOptions } from '.' @@ -19,38 +18,56 @@ const createTestEnvironment = (opts: Omit) => const accounts = ['0x0000000000000000000000000000000000000000'] const chains = [1, 2, 3] +/* + * TODO: Move this to `@web3-react/types` and further narrow down types for `RequestArguments` + */ +type SwitchEthereumChainRequestArguments = { + method: 'wallet_switchEthereumChain', + params: [{ chainId: string }] +} + +const isSwitchEthereumChainRequest = (x: RequestArguments): x is SwitchEthereumChainRequestArguments => { + return x.method === 'wallet_switchEthereumChain' +} + +class MockWalletConnectProvider extends MockEIP1193Provider { + /** per {@link https://eips.ethereum.org/EIPS/eip-3326#specification EIP-3326} */ + public eth_switchEthereumChain = jest.fn((args: string) => null) + + public request(x: RequestArguments | SwitchEthereumChainRequestArguments): Promise { + if (isSwitchEthereumChainRequest(x)) { + this.chainId = parseInt(x.params[0].chainId, 16) + return Promise.resolve(this.eth_switchEthereumChain(JSON.stringify(x))) + } else { + return super.request(x) + } + } + + public enable() { + return super.request({ method: 'eth_requestAccounts' }) + } + + // session is an object when connected, undefined otherwise + get session() { + return this.eth_requestAccounts.mock.calls.length > 0 ? {} : undefined + } +} + describe('WalletConnect', () => { - const wc2RequestMock = jest.fn() let wc2InitMock: jest.Mock beforeEach(() => { - const wc2EnableMock = jest.fn().mockResolvedValue(accounts) + /* + * TypeScript error is expected here. We're mocking a factory `init` method + * to only define a subset of `EthereumProvider` that we use internally + */ // @ts-ignore - // TypeScript error is expected here. We're mocking a factory `init` method - // to only define a subset of `EthereumProvider` that we use internally - wc2InitMock = jest.spyOn(EthereumProvider, 'init').mockImplementation(async (opts) => ({ - // we read this in `enable` to get current chain - accounts, - chainId: opts.chains[0], - // session is an object when connected, undefined otherwise - get session() { - return wc2EnableMock.mock.calls.length > 0 ? {} : undefined - }, - // methods used in `activate` and `isomorphicInitialize` - enable: wc2EnableMock, - // mock EIP-1193 - request: wc2RequestMock, - on() { - return this - }, - removeListener() { - return this - }, - })) - }) - - afterEach(() => { - wc2RequestMock.mockReset() + wc2InitMock = jest.spyOn(EthereumProvider, 'init').mockImplementation(async (opts) => { + const provider = new MockWalletConnectProvider() + provider.chainId = opts.chains[0] + provider.accounts = accounts + return provider + }) }) describe('#connectEagerly', () => { @@ -62,7 +79,7 @@ describe('WalletConnect', () => { describe(`#isomorphicInitialize`, () => { test('should initialize exactly one provider and return a Promise if pending initialization', async () => { - const {connector, store} = createTestEnvironment({ chains }) + const {connector} = createTestEnvironment({ chains }) connector.activate() connector.activate() expect(wc2InitMock).toHaveBeenCalledTimes(1) @@ -82,9 +99,9 @@ describe('WalletConnect', () => { }) test('should activate passed chain', async () => { - const {connector, store} = createTestEnvironment({ chains }) + const {connector} = createTestEnvironment({ chains }) await connector.activate(2) - expect(store.getState().chainId).toEqual(2) + expect(connector.provider?.chainId).toEqual(2) }) test('should throw an error for invalid chain', async () => { @@ -95,15 +112,16 @@ describe('WalletConnect', () => { test('should switch chain if already connected', async () => { const {connector} = createTestEnvironment({ chains }) await connector.activate() + expect(connector.provider?.chainId).toEqual(1) await connector.activate(2) - expect(wc2RequestMock).toHaveBeenCalledWith({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x2' }] }) + expect(connector.provider?.chainId).toEqual(2) }) test('should not switch chain if already connected', async () => { const {connector} = createTestEnvironment({ chains }) await connector.activate(2) await connector.activate(2) - expect(wc2RequestMock).toBeCalledTimes(0) + expect((connector.provider as unknown as MockWalletConnectProvider).eth_switchEthereumChain).toBeCalledTimes(0) }) }) }) diff --git a/packages/walletconnect/src/index.spec.ts b/packages/walletconnect/src/index.spec.ts index f66fb1a..26413bd 100644 --- a/packages/walletconnect/src/index.spec.ts +++ b/packages/walletconnect/src/index.spec.ts @@ -2,24 +2,9 @@ import { createWeb3ReactStoreAndActions } from '@web3-react/store' import type { Actions, RequestArguments, Web3ReactStore } from '@web3-react/types' import EventEmitter from 'eventemitter3' import { WalletConnect } from '.' -import { MockEIP1193Provider } from '../../eip1193/src/mock' - -// necessary because walletconnect returns chainId as a number -class MockMockWalletConnectProvider extends MockEIP1193Provider { - public connector = new EventEmitter() - - public eth_chainId_number = jest.fn((chainId?: string) => - chainId === undefined ? chainId : Number.parseInt(chainId, 16) - ) - - public request(x: RequestArguments): Promise { - if (x.method === 'eth_chainId') { - return Promise.resolve(this.eth_chainId_number(this.chainId)) - } else { - return super.request(x) - } - } +import { MockEIP1193Provider } from '@web3-react/core' +class MockWalletConnectProvider extends MockEIP1193Provider { /** * TODO(INFRA-140): We're using the following private API to fix an underlying WalletConnect issue. * See {@link WalletConnect.activate} for details. @@ -27,15 +12,15 @@ class MockMockWalletConnectProvider extends MockEIP1193Provider { private setHttpProvider() {} } -jest.mock('@walletconnect/ethereum-provider', () => MockMockWalletConnectProvider) +jest.mock('@walletconnect/ethereum-provider', () => MockWalletConnectProvider) -const chainId = '0x1' +const chainId = 1 const accounts: string[] = [] describe('WalletConnect', () => { let store: Web3ReactStore let connector: WalletConnect - let mockProvider: MockMockWalletConnectProvider + let mockProvider: MockWalletConnectProvider describe('works', () => { beforeEach(async () => { @@ -47,7 +32,7 @@ describe('WalletConnect', () => { test('#activate', async () => { await connector.connectEagerly().catch(() => {}) - mockProvider = connector.provider as unknown as MockMockWalletConnectProvider + mockProvider = connector.provider as unknown as MockWalletConnectProvider mockProvider.chainId = chainId mockProvider.accounts = accounts @@ -55,12 +40,12 @@ describe('WalletConnect', () => { expect(mockProvider.eth_requestAccounts).toHaveBeenCalled() expect(mockProvider.eth_accounts).not.toHaveBeenCalled() - expect(mockProvider.eth_chainId_number).toHaveBeenCalled() - expect(mockProvider.eth_chainId_number.mock.invocationCallOrder[0]) + expect(mockProvider.eth_chainId).toHaveBeenCalled() + expect(mockProvider.eth_chainId.mock.invocationCallOrder[0]) .toBeGreaterThan(mockProvider.eth_requestAccounts.mock.invocationCallOrder[0]) expect(store.getState()).toEqual({ - chainId: Number.parseInt(chainId, 16), + chainId, accounts, activating: false, error: undefined, diff --git a/tsconfig.json b/tsconfig.json index 3e510d8..de9d02a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "module": "CommonJS", "declaration": true, - "moduleResolution": "Node" + "moduleResolution": "Node", + "jsx": "react" }, "exclude": ["**/*.spec.ts"] }