Task/252 Eth wallet connection and deposit form validation tests (#309)

* add feature/scenarios for deposits

* add file for auction orders tests

* update feature file for deposits

* update feature tests for deposit

* add feature/scenarios for deposits

* add file for auction orders tests

* update feature file for deposits

* update feature tests for deposit

* add test for wallet not connected

* fix lint warning

* add mock ethereum provider to allow connecting ethereum wallet

* add basic test for required validation errors

* add  aria for input errors for a11y and test targeting, expand submit form helper

* use mnemonic for private key generation, update tests to not submit and just assert validation message updates

* add chain id to cypress config

* update scenario

* remove feature file

* lint fix

* Update apps/trading-e2e/cypress.json

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* use mnemonic from github secret, update cypress.json env vars to match

* fix typo in test name and mnemonic env var

* update env variables

* update eth wallet mnemonic env

* Update libs/cypress/src/lib/eip1193-bridge.ts

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* remove unused reference to chainId

* update casing

* chainId reference from cypress.json

* Update apps/trading-e2e/cypress.json

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* Update apps/trading-e2e/src/support/step_definitions/deposits.step.ts

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* ignore a known failing step in the test due to wallet connected having approved status

* update testid

* update tests for deposits

* tidy up comments in custom cypress commands

* add comment about eager connect when running in cypress

* update deposits tests

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
Ditmir-Vega 2022-05-10 20:37:09 +01:00 committed by GitHub
parent 81ea3fe946
commit 6bccfaf8ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 485 additions and 30 deletions

View File

@ -59,6 +59,7 @@ jobs:
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPESS_ETH_WALLET_MNEMONIC }}
run: npx nx affected:e2e --parallel=5 --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
pr:
name: Run end-to-end tests - PR
@ -109,4 +110,5 @@ jobs:
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPESS_ETH_WALLET_MNEMONIC }}
run: npx nx affected:e2e --parallel=5 --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome

View File

@ -13,12 +13,17 @@
"screenshotsFolder": "../../dist/cypress/apps/trading-e2e/screenshots",
"chromeWebSecurity": false,
"projectId": "et4snf",
"env": {
"ethereumProviderUrl": "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8",
"ethereumChainId": 3,
"vegaPublicKey": "47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278",
"vegaPublicKey2": "1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4",
"truncatedVegaPubKey": "47836c…c7d278",
"truncatedVegaPubKey2": "1a18cd…0cf2e4",
"depositAsset": "5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c",
"tsConfig": "tsconfig.json",
"TAGS": "not @todo and not @ignore and not @manual"
"TAGS": "not @todo and not @ignore and not @manual",
"tBtcContract": "5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c"
}
}

View File

@ -0,0 +1,123 @@
Feature: Deposits to vega wallet
Background:
Given I navigate to deposits page
# wallet is already connected before tests start and doesn't prompt the disconnected state
@ignore
Scenario: Connecting Ethereum wallet
Then I can see the eth not connected message "Connect your Ethereum wallet"
And the connect button is displayed
When I connect my Ethereum wallet
Then I can see the deposit form
@todo
Scenario: Cannot deposit if approved amount is 0 (approval amount is 0)
And I connect my Ethereum wallet
When I set "0" tokens to be approved
And I approve the asset tokens
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 50 |
And I click to the deposit the funds
And I approve the ethereum transaction
# The following step is valid and are commented out as currently it cannot be automated
# Then I can see the deposit is unsuccessful
@todo
Scenario: Cannot deposit if approved amount is lower than deposit amount
When I set "2" tokens to be approved
And I approve the asset tokens
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 50 |
And I click to the deposit the funds
And I approve the ethereum transaction
# The following step is valid and are commented out as currently it cannot be automated
# Then I can see the deposit is unsuccessful
@todo
Scenario: Can succesfully deposit (approved amount is greater than deposit)
When I set "200000000" tokens to be approved
And I approve the asset tokens
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 50 |
And I click to the deposit the funds
And I approve the ethereum transaction
# The following steps are valid and are commented out as currently they cannot be automated
# Then I can see the deposit is Successfull
# And Balance is updated to reflect deposit amount
Scenario: Validation errors
# wallet is connected on before hook so this step may no longer be required
# Given I connect my Ethereum wallet
When I submit a deposit with empty fields
Then I can see validation errors present
And I enter an invalid public key
Then Invalid Vega key is shown
And I enter an amount less than the minimum viable amount
Then Amount too small message shown
And I enter a valid amount
# This next step is being skipped due to account having approved status
# Then Not approved message shown
@todo
Scenario: Use the 'Use Maximum' button to populate amount input with the balance in the connected wallet
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 0 |
When I click the use maximum button
Then I can see the field is updated with the maximum amount of the asset from my wallet
@todo
Scenario: User is warned if the the amount to deposit is greater than what is available in the connected wallet"
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 60000000 |
And I click to the deposit the funds
Then an error message is shown stating not enough tokens in wallet to deposit
@todo
Scenario: Deposit to a vega wallet key which is not your own
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | VEGA KEY of another wallet |
| Amount | 50 |
And I click to the deposit the funds
And I approve the ethereum transaction
# The following steps are valid and are commented out as currently they cannot be automated
# Then I can see the deposit is Successfull
# And Balance is updated to reflect deposit amount
@todo
Scenario: Deposit when vega wallet is not connected
And I disconnect my vega wallet
And I can see the deposit form is displayed
And I select "" asset from the dropdown list
When I enter the following details
| Field | Value |
| To (Vega key) | xxxxxxxx |
| Amount | 50 |
And I click to the deposit the funds
And I approve the ethereum transaction
# The following step is valid and are commented out as currently it cannot be automated
# Then I can see the deposit is unsuccessful

View File

@ -0,0 +1,22 @@
export class EthereumWallet {
connectWalletBtnId = 'connect-eth-wallet-btn';
connectWalletMsgId = 'connect-eth-wallet-msg';
connect() {
cy.getByTestId(this.connectWalletBtnId).should('be.enabled').click();
cy.getByTestId('web3-connector-list').should('be.visible');
cy.getByTestId('web3-connector-MetaMask').click();
}
verifyEthConnectBtnIsDisplayed() {
cy.getByTestId(this.connectWalletBtnId)
.should('be.visible')
.and('have.text', 'Connect');
}
verifyConnectWalletMsg(ethNotConnectedText: string) {
cy.getByTestId(this.connectWalletMsgId)
.should('be.visible')
.and('have.text', ethNotConnectedText);
}
}

View File

@ -0,0 +1 @@
export * from './ethereum-wallet';

View File

@ -0,0 +1,58 @@
import BasePage from './base-page';
export default class DepositsPage extends BasePage {
requiredText = 'Required';
assetError = '[role="alert"][aria-describedby="asset"]';
toError = '[role="alert"][aria-describedby="to"]';
amountError = '[role="alert"][aria-describedby="amount"]';
navigateToDeposits() {
cy.visit('/portfolio');
cy.get(`a[href='/portfolio/deposit']`).click();
cy.url().should('include', '/portfolio/deposit');
}
verifyFormDisplayed() {
cy.getByTestId('deposit-form').should('be.visible');
}
updateForm(args?: { asset?: string; to?: string; amount?: string }) {
if (args?.asset) {
cy.get('select[name="asset"]').select(args.asset);
}
if (args?.to) {
cy.get('input[name="to"]').clear().type(args.to);
}
if (args?.amount) {
cy.get('input[name="amount"]').clear().type(args.amount);
}
}
submitForm() {
cy.getByTestId('deposit-submit').click();
}
verifyFieldsAreRequired() {
cy.get(this.assetError).contains(this.requiredText);
cy.get(this.toError).contains(this.requiredText);
cy.get(this.amountError).contains(this.requiredText);
cy.getByTestId('input-error-text').should('have.length', 3);
}
verifyInvalidPublicKey() {
cy.get(this.toError).contains('Invalid Vega key').should('be.visible');
}
verifyAmountTooSmall() {
cy.get(this.amountError)
.contains('Value is below minimum')
.should('be.visible');
}
verifyNotApproved() {
cy.get(this.amountError)
.contains('Amount is above approved amount')
.should('be.visible');
cy.contains('Deposits of tBTC not approved').should('be.visible');
}
}

View File

@ -0,0 +1,75 @@
import { And, Then, When } from 'cypress-cucumber-preprocessor/steps';
import { EthereumWallet } from '../ethereum-wallet';
import DepositsPage from '../pages/deposits-page';
const depositsPage = new DepositsPage();
const ethWallet = new EthereumWallet();
const tBTC = Cypress.env('tBtcContract');
const invalidPublicKey =
'zzz85edfa7ffdb6ed996ca912e9258998e47bf3515c885cf3c63fb56b15de36f';
beforeEach(() => {
cy.mockWeb3Provider();
});
Then('I navigate to deposits page', () => {
depositsPage.navigateToDeposits();
});
Then('I can see the eth not connected message {string}', (message) => {
ethWallet.verifyConnectWalletMsg(message);
});
And('the connect button is displayed', () => {
ethWallet.verifyEthConnectBtnIsDisplayed();
});
When('I connect my Ethereum wallet', () => {
ethWallet.connect();
});
Then('I can see the deposit form', () => {
depositsPage.verifyFormDisplayed();
});
When('I submit a deposit with empty fields', () => {
depositsPage.updateForm();
depositsPage.submitForm();
});
Then('I can see validation errors present', () => {
depositsPage.verifyFieldsAreRequired();
});
And('I enter an invalid public key', () => {
depositsPage.updateForm({
asset: tBTC,
to: invalidPublicKey,
amount: '1',
});
});
Then('Invalid Vega key is shown', () => {
depositsPage.verifyInvalidPublicKey();
});
And('I enter an amount less than the minimum viable amount', () => {
depositsPage.updateForm({
asset: tBTC,
to: invalidPublicKey,
amount: '0.00000000000001',
});
});
Then('Amount too small message shown', () => {
depositsPage.verifyAmountTooSmall();
});
And('I enter a valid amount', () => {
depositsPage.updateForm({ amount: '1' });
});
Then('Not approved message shown', () => {
depositsPage.verifyNotApproved();
});

View File

@ -108,7 +108,11 @@ export const Web3Content = ({
const { isActive, error, connector, chainId } = useWeb3React();
useEffect(() => {
if (connector?.connectEagerly) {
if (
connector?.connectEagerly &&
// Dont eager connect if this is a cypress test run
'Cypress' in window
) {
connector.connectEagerly();
}
}, [connector]);
@ -127,8 +131,15 @@ export const Web3Content = ({
if (!isActive) {
return (
<SplashWrapper>
<p className="mb-12">{t('Connect your Ethereum wallet')}</p>
<Button onClick={() => setDialogOpen(true)}>{t('Connect')}</Button>
<p data-testid="connect-eth-wallet-msg" className="mb-12">
{t('Connect your Ethereum wallet')}
</p>
<Button
onClick={() => setDialogOpen(true)}
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</Button>
</SplashWrapper>
);
}

View File

@ -1,9 +1,11 @@
import { addGetTestIdcommand } from './lib/commands/get-by-test-id';
import { addMockGQLCommand } from './lib/commands/mock-gql';
import { addMockVegaWalletCommands } from './lib/commands/mock-vega-wallet';
import { addMockWeb3ProviderCommand } from './lib/commands/mock-web3-provider';
import { addSlackCommand } from './lib/commands/slack';
addGetTestIdcommand();
addSlackCommand();
addMockGQLCommand();
addMockVegaWalletCommands();
addMockWeb3ProviderCommand();

View File

@ -7,9 +7,8 @@ declare global {
}
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export function addGetTestIdcommand() {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-testid=${selector}]`, ...args);
});

View File

@ -11,13 +11,9 @@ declare global {
}
export function addMockGQLCommand() {
Cypress.Commands.add(
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
'mockGQL',
(alias: string, handler: RouteHandler) => {
Cypress.Commands.add('mockGQL', (alias: string, handler: RouteHandler) => {
cy.intercept('POST', 'https://lb.testnet.vega.xyz/query', handler).as(
alias
);
}
);
});
}

View File

@ -14,7 +14,6 @@ declare global {
export function addMockVegaWalletCommands() {
Cypress.Commands.add(
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
'mockVegaCommandSync',
(override?: PartialDeep<TransactionResponse>) => {
const defaultTransactionResponse = {

View File

@ -0,0 +1,21 @@
import { createBridge } from '../eip1193-bridge';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
mockWeb3Provider(): void;
}
}
}
export function addMockWeb3ProviderCommand() {
Cypress.Commands.add('mockWeb3Provider', () => {
cy.log('Mocking web3');
cy.on('window:before:load', (win) => {
// @ts-ignore ethereum object is injected so won't exist on window object
win.ethereum = createBridge();
});
});
}

View File

@ -0,0 +1,124 @@
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge';
import { JsonRpcProvider } from '@ethersproject/providers';
import { Wallet } from '@ethersproject/wallet';
import { ethers } from 'ethers';
// Address of the above key
export class CustomizedBridge extends Eip1193Bridge {
chainId = Cypress.env('ethereumChainId');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async sendAsync(...args: any) {
console.debug('sendAsync called', ...args);
return this.send(...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override async send(...args: any) {
console.debug('send called', ...args);
const isCallbackForm =
typeof args[0] === 'object' && typeof args[1] === 'function';
let callback;
let method;
let params;
if (isCallbackForm) {
callback = args[1];
method = args[0].method;
params = args[0].params;
} else {
method = args[0];
params = args[1];
}
try {
// Mock out request accounts and chainId
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
const address = this.signer ? [await this.signer.getAddress()] : [];
if (isCallbackForm) {
callback({ result: address });
} else {
return Promise.resolve(address);
}
}
if (method === 'eth_chainId') {
if (isCallbackForm) {
callback(null, { result: '0x3' });
} else {
return Promise.resolve('0x3');
}
}
// Hacky, https://github.com/ethers-io/ethers.js/issues/1683#issuecomment-1016227588
// If from is present on eth_call it errors, removing it makes the library set
// from as the connected wallet which works fine
if (params && params.length && params[0].from && method === 'eth_call')
delete params[0].from;
let result;
// For sending a transaction if we call send it will error
// as it wants gasLimit in sendTransaction but hexlify sets the property gas
// to gasLimit which makes sensd transaction error.
// This has taken the code from the super method for sendTransaction and altered
// it slightly to make it work with the gas limit issues.
if (
params &&
params.length &&
params[0].from &&
method === 'eth_sendTransaction'
) {
// Hexlify will not take gas, must be gasLimit, set this property to be gasLimit
params[0].gasLimit = params[0].gas;
delete params[0].gas;
// If from is present on eth_sendTransaction it errors, removing it makes the library set
// from as the connected wallet which works fine
delete params[0].from;
const req = ethers.providers.JsonRpcProvider.hexlifyTransaction(
params[0]
);
// Hexlify sets the gasLimit property to be gas again and send transaction requires gasLimit
req['gasLimit'] = req['gas'];
delete req['gas'];
if (!this.signer) {
throw new Error('No signer');
}
// Send the transaction
const tx = await this.signer.sendTransaction(req);
result = tx.hash;
} else {
// All other transactions the base class works for
result = await super.send(method, params);
}
console.debug('result received', method, params, result);
if (isCallbackForm) {
callback(null, { result });
} else {
return result;
}
} catch (error) {
console.log(error);
if (isCallbackForm) {
callback(error, null);
} else {
throw error;
}
}
}
}
const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`;
const getProvider = () =>
new JsonRpcProvider(
Cypress.env('ethereumProviderUrl'),
Cypress.env('ethereumChainId')
);
export const createBridge = () => {
const provider = getProvider();
const privateKey = Wallet.fromMnemonic(
Cypress.env('ETH_WALLET_MNEMONIC'),
getAccount(0)
).privateKey;
const signer = new Wallet(privateKey, provider);
return new CustomizedBridge(signer, provider);
};

View File

@ -131,7 +131,11 @@ export const DepositForm = ({
}, [assetId, onSelectAsset]);
return (
<form onSubmit={handleSubmit(onDeposit)} noValidate={true}>
<form
onSubmit={handleSubmit(onDeposit)}
noValidate={true}
data-testid="deposit-form"
>
<FormGroup
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
@ -156,7 +160,7 @@ export const DepositForm = ({
))}
</Select>
{errors.asset?.message && (
<InputError intent="danger" className="mt-4">
<InputError intent="danger" className="mt-4" forInput="asset">
{errors.asset.message}
</InputError>
)}
@ -166,17 +170,13 @@ export const DepositForm = ({
</UseButton>
)}
</FormGroup>
<FormGroup
label={t('To (Vega key)')}
labelFor="vega-key"
className="relative"
>
<FormGroup label={t('To (Vega key)')} labelFor="to" className="relative">
<Input
{...register('to', { validate: { required, vegaPublicKey } })}
id="vega-key"
id="to"
/>
{errors.to?.message && (
<InputError intent="danger" className="mt-4">
<InputError intent="danger" className="mt-4" forInput="to">
{errors.to.message}
</InputError>
)}
@ -202,8 +202,8 @@ export const DepositForm = ({
autoComplete="off"
id="amount"
{...register('amount', {
required: t('Required'),
validate: {
required,
minSafe: (value) => minSafe(min)(value),
maxSafe: (v) => {
const value = new BigNumber(v);
@ -220,7 +220,7 @@ export const DepositForm = ({
})}
/>
{errors.amount?.message && (
<InputError intent="danger" className="mt-4">
<InputError intent="danger" className="mt-4" forInput="amount">
{errors.amount.message}
</InputError>
)}
@ -265,7 +265,7 @@ const FormButton = ({
if (!selectedAsset) {
button = (
<Button type="submit" className="w-full">
<Button type="submit" className="w-full" data-testid="deposit-submit">
{t('Deposit')}
</Button>
);
@ -276,14 +276,18 @@ const FormButton = ({
</>
);
button = (
<Button type="submit" className="w-full">
<Button type="submit" className="w-full" data-testid="deposit-submit">
{t('Deposit')}
</Button>
);
} else {
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
button = (
<Button onClick={onApproveClick} className="w-full">
<Button
onClick={onApproveClick}
className="w-full"
data-testid="deposit-approve-submit"
>
{t(`Approve ${selectedAsset.symbol}`)}
</Button>
);

View File

@ -6,12 +6,14 @@ interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
className?: string;
intent?: 'danger' | 'warning';
forInput?: string;
}
export const InputError = ({
intent = 'danger',
className,
children,
forInput,
...props
}: InputErrorProps) => {
const effectiveClassName = classNames(
@ -37,6 +39,7 @@ export const InputError = ({
return (
<div
data-testid="input-error-text"
aria-describedby={forInput}
className={effectiveClassName}
{...props}
role="alert"

View File

@ -73,6 +73,7 @@
"@apollo/react-testing": "^4.0.0",
"@babel/core": "7.12.13",
"@babel/preset-typescript": "7.12.13",
"@ethersproject/experimental": "^5.6.0",
"@nrwl/cli": "13.10.3",
"@nrwl/cypress": "13.10.3",
"@nrwl/eslint-plugin-nx": "13.10.3",

View File

@ -1644,6 +1644,15 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/transactions" "^5.6.0"
"@ethersproject/experimental@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.6.0.tgz#c72ef00a79b746c522eb79736712169d71c55f64"
integrity sha512-lSEM/6t+BicbeyRxat5meoQhXZLoBEziVrxZqeCIhsPntvq4DlMobPBKXF0Iz3m0dMvl9uga7fHEO4YD9SgCgw==
dependencies:
"@ethersproject/web" "^5.6.0"
ethers "^5.6.0"
scrypt-js "3.0.1"
"@ethersproject/hash@5.6.0", "@ethersproject/hash@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.0.tgz#d24446a5263e02492f9808baa99b6e2b4c3429a2"