Commit 01684977 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

test: add a 'hardhat' mock window.ethereum (#6395)

parent 025a84de
import { getTestSelector } from '../utils'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
describe('Landing Page', () => {
it('shows landing page when no selectedWallet', () => {
cy.visit('/', { noWallet: true })
it('shows landing page when no user state exists', () => {
cy.visit('/', { userState: {} })
cy.get(getTestSelector('landing-page'))
cy.screenshot()
})
it('redirects to swap page when selectedWallet is INJECTED', () => {
cy.visit('/', { selectedWallet: 'INJECTED' })
it('redirects to swap page when a user has already connected a wallet', () => {
cy.visit('/', { userState: CONNECTED_WALLET_USER_STATE })
cy.get('#swap-page')
cy.url().should('include', '/swap')
cy.screenshot()
})
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
it('shows landing page when a user has already connected a wallet but ?intro=true is in query', () => {
cy.visit('/?intro=true', { userState: CONNECTED_WALLET_USER_STATE })
cy.get(getTestSelector('landing-page'))
})
......
import { USDC_MAINNET } from '../../src/constants/tokens'
import { WETH_GOERLI } from '../fixtures/constants'
import { HardhatProvider } from '../support/hardhat'
import { getTestSelector } from '../utils'
describe('Swap', () => {
let hardhat: HardhatProvider
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
if (amountText === null) {
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
......@@ -43,7 +46,9 @@ describe('Swap', () => {
}
before(() => {
cy.visit('/swap')
cy.visit('/swap', { ethereum: 'hardhat' }).then((window) => {
hardhat = window.hardhat
})
})
it('starts with ETH selected by default', () => {
......@@ -73,6 +78,33 @@ describe('Swap', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('can swap ETH for USDC', () => {
const TOKEN_ADDRESS = USDC_MAINNET.address
const BALANCE_INCREMENT = 1
cy.then(() => hardhat.utils.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('[data-testid="token-search-input"]').clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get('[data-testid="dismiss-tx-confirmation"]').click()
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
// chain state check
cy.then(() => hardhat.utils.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
})
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap')
verifyToken('input', 'ETH')
......
......@@ -18,7 +18,7 @@ describe('Universal search bar', () => {
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
cy.get('div').contains('Uniswap').should('exist')
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
......
......@@ -5,25 +5,43 @@
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import '@cypress/code-coverage/support'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import assert from 'assert'
import { Network } from 'cypress-hardhat/lib/browser'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { injected } from './ethereum'
import { HardhatProvider } from './hardhat'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: typeof injected
ethereum: Eip1193Bridge
hardhat: HardhatProvider
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
selectedWallet?: string
noWallet?: boolean
/**
* The mock ethereum provider to inject into the page.
* @default 'goerli'
*/
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
ethereum?: 'goerli' | 'hardhat'
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
*/
userState?: Partial<UserState>
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
task(event: 'hardhat'): Chainable<Network>
}
}
}
......@@ -35,37 +53,46 @@ Cypress.Commands.overwrite(
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
assert(typeof url === 'string')
cy.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }).then(() => {
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
return cy
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
.task('hardhat')
.then((network) =>
original({
...options,
url:
(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) +
`${url.includes('?') ? '&' : '?'}chain=goerli`,
url: hashUrl,
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
// We want to test from a clean state, so we clear the local storage (which clears redux).
win.localStorage.clear()
const userState = {
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
fiatOnrampDismissed: true,
}
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
// Set initial user state.
win.localStorage.setItem(
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
)
// Set feature flags, if configured.
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce(
(flags, flag) => ({
...flags,
[flag]: 'enabled',
}),
{}
)
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
}
// Inject the mock ethereum provider.
if (options?.ethereum === 'hardhat') {
// The provider is exposed via hardhat to allow mocking / network manipulation.
win.hardhat = new HardhatProvider(network)
win.ethereum = win.hardhat
} else {
win.ethereum = injected
}
},
})
})
)
}
)
......@@ -88,6 +115,10 @@ beforeEach(() => {
res.headers['origin'] = 'https://app.uniswap.org'
res.continue()
})
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (res) => {
res.reply(JSON.stringify({}))
})
})
Cypress.on('uncaught:exception', () => {
......
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { JsonRpcProvider } from '@ethersproject/providers'
import { Wallet } from '@ethersproject/wallet'
import { HardhatUtils, Network } from 'cypress-hardhat/lib/browser'
export class HardhatProvider extends Eip1193Bridge {
readonly utils: HardhatUtils
readonly chainId: string
readonly wallet: Wallet
isMetaMask = true
constructor(network: Network) {
const utils = new HardhatUtils(network)
const wallet = new Wallet(utils.account.privateKey, utils.provider)
super(wallet, utils.provider)
this.utils = utils
this.chainId = `0x${network.chainId.toString(16)}`
this.wallet = wallet
}
async sendAsync(...args: any[]) {
return this.send(...args)
}
async send(...args: any[]) {
console.debug('hardhat:send', ...args)
// Parse callback form.
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
let callback = <T>(error: Error | null, result?: { result: T }) => {
if (error) throw error
return result?.result
}
let method
let params
if (isCallbackForm) {
callback = args[1]
method = args[0].method
params = args[0].params
} else {
method = args[0]
params = args[1]
}
let result
try {
switch (method) {
case 'eth_requestAccounts':
case 'eth_accounts':
result = [this.wallet.address]
break
case 'eth_chainId':
result = this.chainId
break
case 'eth_sendTransaction': {
// Eip1193Bridge doesn't support .gas and .from directly, so we massage it to satisfy ethers' expectations.
// See https://github.com/ethers-io/ethers.js/issues/1683.
params[0].gasLimit = params[0].gas
delete params[0].gas
delete params[0].from
const req = JsonRpcProvider.hexlifyTransaction(params[0])
req.gasLimit = req.gas
delete req.gas
result = (await this.signer.sendTransaction(req)).hash
break
}
default:
result = await super.send(method, params)
}
console.debug('hardhat:receive', method, result)
return callback(null, { result })
} catch (error) {
console.debug('hardhat:error', method, error)
return callback(error as Error)
}
}
}
import { UserState } from '../../src/state/user/reducer'
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }
/* eslint-env node */
require('dotenv').config()
// Block selection is arbitrary, as e2e tests will build up their own state.
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
const BLOCK_NUMBER = 17023328
const mainnetFork = {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
blockNumber: 17023328,
blockNumber: BLOCK_NUMBER,
httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin
},
......
......@@ -311,6 +311,7 @@ export default function SwapCurrencyInputPanel({
{account ? (
<RowFixed style={{ height: '17px' }}>
<ThemedText.DeprecatedBody
data-testid="balance-text"
color={theme.textSecondary}
fontWeight={400}
fontSize={14}
......
......@@ -227,6 +227,7 @@ export function CurrencySearch({
<SearchInput
type="text"
id="token-search-input"
data-testid="token-search-input"
placeholder={t`Search name or paste address`}
autoComplete="off"
value={searchQuery}
......
......@@ -153,7 +153,7 @@ function TransactionSubmittedContent({
)}
</ButtonLight>
)}
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }} data-testid="dismiss-tx-confirmation">
<Text fontWeight={600} fontSize={20} color={theme.accentTextLightPrimary}>
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
</Text>
......
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import invariant from 'tiny-invariant'
import { UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export const NATIVE_CHAIN_ID = 'NATIVE'
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment