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 { getTestSelector } from '../utils'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
describe('Landing Page', () => { describe('Landing Page', () => {
it('shows landing page when no selectedWallet', () => { it('shows landing page when no user state exists', () => {
cy.visit('/', { noWallet: true }) cy.visit('/', { userState: {} })
cy.get(getTestSelector('landing-page')) cy.get(getTestSelector('landing-page'))
cy.screenshot() cy.screenshot()
}) })
it('redirects to swap page when selectedWallet is INJECTED', () => { it('redirects to swap page when a user has already connected a wallet', () => {
cy.visit('/', { selectedWallet: 'INJECTED' }) cy.visit('/', { userState: CONNECTED_WALLET_USER_STATE })
cy.get('#swap-page') cy.get('#swap-page')
cy.url().should('include', '/swap') cy.url().should('include', '/swap')
cy.screenshot() cy.screenshot()
}) })
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => { it('shows landing page when a user has already connected a wallet but ?intro=true is in query', () => {
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' }) cy.visit('/?intro=true', { userState: CONNECTED_WALLET_USER_STATE })
cy.get(getTestSelector('landing-page')) cy.get(getTestSelector('landing-page'))
}) })
......
import { USDC_MAINNET } from '../../src/constants/tokens'
import { WETH_GOERLI } from '../fixtures/constants' import { WETH_GOERLI } from '../fixtures/constants'
import { HardhatProvider } from '../support/hardhat'
import { getTestSelector } from '../utils' import { getTestSelector } from '../utils'
describe('Swap', () => { describe('Swap', () => {
let hardhat: HardhatProvider
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => { const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
if (amountText === null) { if (amountText === null) {
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value') cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
...@@ -43,7 +46,9 @@ describe('Swap', () => { ...@@ -43,7 +46,9 @@ describe('Swap', () => {
} }
before(() => { before(() => {
cy.visit('/swap') cy.visit('/swap', { ethereum: 'hardhat' }).then((window) => {
hardhat = window.hardhat
})
}) })
it('starts with ETH selected by default', () => { it('starts with ETH selected by default', () => {
...@@ -73,6 +78,33 @@ describe('Swap', () => { ...@@ -73,6 +78,33 @@ describe('Swap', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0') 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', () => { it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap') cy.visit('/swap')
verifyToken('input', 'ETH') verifyToken('input', 'ETH')
......
...@@ -18,7 +18,7 @@ describe('Universal search bar', () => { ...@@ -18,7 +18,7 @@ describe('Universal search bar', () => {
.and('contain.text', 'UNI') .and('contain.text', 'UNI')
.and('contain.text', '$') .and('contain.text', '$')
.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') cy.get('div').contains('Uniswap').should('exist')
// Stats should have: TVL, 24H Volume, 52W low, 52W high. // Stats should have: TVL, 24H Volume, 52W low, 52W high.
......
...@@ -5,25 +5,43 @@ ...@@ -5,25 +5,43 @@
// https://on.cypress.io/configuration // https://on.cypress.io/configuration
// *********************************************************** // ***********************************************************
// Import commands.ts using ES2015 syntax:
import '@cypress/code-coverage/support' import '@cypress/code-coverage/support'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import assert from 'assert' import assert from 'assert'
import { Network } from 'cypress-hardhat/lib/browser'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags' 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 { injected } from './ethereum'
import { HardhatProvider } from './hardhat'
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress { namespace Cypress {
interface ApplicationWindow { interface ApplicationWindow {
ethereum: typeof injected ethereum: Eip1193Bridge
hardhat: HardhatProvider
} }
interface VisitOptions { interface VisitOptions {
serviceWorker?: true serviceWorker?: true
featureFlags?: Array<FeatureFlag> 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( ...@@ -35,37 +53,46 @@ Cypress.Commands.overwrite(
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => { (original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
assert(typeof url === 'string') 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({ original({
...options, ...options,
url: url: hashUrl,
(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) +
`${url.includes('?') ? '&' : '?'}chain=goerli`,
onBeforeLoad(win) { onBeforeLoad(win) {
options?.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() win.localStorage.clear()
const userState = { // Set initial user state.
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined, win.localStorage.setItem(
fiatOnrampDismissed: true, 'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
} JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState)) )
// Set feature flags, if configured.
if (options?.featureFlags) { if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce( const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
(flags, flag) => ({
...flags,
[flag]: 'enabled',
}),
{}
)
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags)) 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 win.ethereum = injected
}
}, },
}) })
}) )
} }
) )
...@@ -88,6 +115,10 @@ beforeEach(() => { ...@@ -88,6 +115,10 @@ beforeEach(() => {
res.headers['origin'] = 'https://app.uniswap.org' res.headers['origin'] = 'https://app.uniswap.org'
res.continue() res.continue()
}) })
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (res) => {
res.reply(JSON.stringify({}))
})
}) })
Cypress.on('uncaught:exception', () => { 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 */ /* eslint-env node */
require('dotenv').config() 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 = { const mainnetFork = {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
blockNumber: 17023328, blockNumber: BLOCK_NUMBER,
httpHeaders: { httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin Origin: 'localhost:3000', // infura allowlists requests by origin
}, },
......
...@@ -311,6 +311,7 @@ export default function SwapCurrencyInputPanel({ ...@@ -311,6 +311,7 @@ export default function SwapCurrencyInputPanel({
{account ? ( {account ? (
<RowFixed style={{ height: '17px' }}> <RowFixed style={{ height: '17px' }}>
<ThemedText.DeprecatedBody <ThemedText.DeprecatedBody
data-testid="balance-text"
color={theme.textSecondary} color={theme.textSecondary}
fontWeight={400} fontWeight={400}
fontSize={14} fontSize={14}
......
...@@ -227,6 +227,7 @@ export function CurrencySearch({ ...@@ -227,6 +227,7 @@ export function CurrencySearch({
<SearchInput <SearchInput
type="text" type="text"
id="token-search-input" id="token-search-input"
data-testid="token-search-input"
placeholder={t`Search name or paste address`} placeholder={t`Search name or paste address`}
autoComplete="off" autoComplete="off"
value={searchQuery} value={searchQuery}
......
...@@ -153,7 +153,7 @@ function TransactionSubmittedContent({ ...@@ -153,7 +153,7 @@ function TransactionSubmittedContent({
)} )}
</ButtonLight> </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}> <Text fontWeight={600} fontSize={20} color={theme.accentTextLightPrimary}>
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>} {inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
</Text> </Text>
......
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core' import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import { UNI_ADDRESS } from './addresses' import { UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export const NATIVE_CHAIN_ID = 'NATIVE' 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