Skip to content

Commit

Permalink
feat: export and share MockEip1193 provider and use it throughout the…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>

---------

Co-authored-by: Zach Pomerantz <[email protected]>
  • Loading branch information
grabbou and zzmp authored Apr 5, 2023
1 parent 615494c commit 19fc54d
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 68 deletions.
2 changes: 1 addition & 1 deletion packages/coinbase-wallet/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './hooks'
export * from './mocks'
export * from './provider'
8 changes: 4 additions & 4 deletions packages/eip1193/src/mock.ts → packages/core/src/mocks.ts
Original file line number Diff line number Diff line change
@@ -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<T = string> 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)

Expand All @@ -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)}`)
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"jsx": "react",
"outDir": "./dist"
}
}
2 changes: 1 addition & 1 deletion packages/eip1193/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/metamask/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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']
Expand Down
88 changes: 53 additions & 35 deletions packages/walletconnect-v2/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '.'
Expand All @@ -19,38 +18,56 @@ const createTestEnvironment = (opts: Omit<WalletConnectOptions, 'projectId'>) =>
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<number> {
/** 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<unknown> {
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', () => {
Expand All @@ -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)
Expand All @@ -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 () => {
Expand All @@ -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)
})
})
})
33 changes: 9 additions & 24 deletions packages/walletconnect/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,25 @@ 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<unknown> {
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<number> {
/**
* TODO(INFRA-140): We're using the following private API to fix an underlying WalletConnect issue.
* See {@link WalletConnect.activate} for details.
*/
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 () => {
Expand All @@ -47,20 +32,20 @@ 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

await connector.activate()

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,
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"moduleResolution": "Node"
"moduleResolution": "Node",
"jsx": "react"
},
"exclude": ["**/*.spec.ts"]
}

0 comments on commit 19fc54d

Please sign in to comment.