import { fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import BigNumber from 'bignumber.js'; import { AddressField, TransferFee, TransferForm, type TransferFormProps, } from './transfer-form'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { removeDecimal } from '@vegaprotocol/utils'; describe('TransferForm', () => { const renderComponent = (props: TransferFormProps) => { return render(); }; const submit = async () => { await userEvent.click( screen.getByRole('button', { name: 'Confirm transfer' }) ); }; const selectAsset = async (asset: { id: string; name: string; decimals: number; }) => { // Bypass RichSelect and target hidden native select // eslint-disable-next-line fireEvent.change(document.querySelector('select[name="asset"]')!, { target: { value: asset.id }, }); // assert rich select as updated expect(await screen.findByTestId('select-asset')).toHaveTextContent( asset.name ); }; const amount = '100'; const pubKey = '1'.repeat(64); const asset = { id: 'eur', symbol: '€', name: 'EUR', decimals: 2, quantum: '1', }; const props = { pubKey, pubKeys: [pubKey, '2'.repeat(64)], feeFactor: '0.001', submitTransfer: jest.fn(), accounts: [ { type: AccountType.ACCOUNT_TYPE_GENERAL, asset, balance: '100000', }, { type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS, asset, balance: '10000', }, ], minQuantumMultiple: '1', }; const propsNoAssets = { pubKey, pubKeys: [ pubKey, 'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce', ], feeFactor: '0.001', submitTransfer: jest.fn(), accounts: [], minQuantumMultiple: '1', }; it('renders no assets', async () => { renderComponent(propsNoAssets); expect(screen.getByTestId('no-assets-available')).toBeVisible(); }); it('renders no accounts', async () => { renderComponent(propsNoAssets); expect(screen.getByTestId('no-accounts-available')).toBeVisible(); }); it.each([ { targetText: 'Transfer fee', tooltipText: /transfer\.fee\.factor/, }, { targetText: 'Amount to be transferred', tooltipText: /without the fee/, }, { targetText: 'Total amount (with fee)', tooltipText: /total amount taken from your account/, }, ])('Tooltip for "$targetText" shows', async (o) => { // 1003-TRAN-015 // 1003-TRAN-016 // 1003-TRAN-017 // 1003-TRAN-018 // 1003-TRAN-019 renderComponent(props); // Select a pubkey await userEvent.selectOptions( screen.getByLabelText('To Vega key'), props.pubKeys[1] ); // Select asset await selectAsset(asset); // set valid amount const amountInput = screen.getByLabelText('Amount'); await userEvent.type(amountInput, amount); expect(amountInput).toHaveValue(amount); const label = screen.getByText(o.targetText); await userEvent.hover(label); expect(await screen.findByRole('tooltip')).toHaveTextContent(o.tooltipText); }); it('validates a manually entered address', async () => { // 1003-TRAN-012 // 1003-TRAN-013 // 1003-TRAN-004 renderComponent(props); await submit(); expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value const toggle = screen.getByText('Enter manually'); await userEvent.click(toggle); // has switched to input expect(toggle).toHaveTextContent('Select from wallet'); expect(screen.getByLabelText('To Vega key')).toHaveAttribute( 'type', 'text' ); await userEvent.type( screen.getByLabelText('To Vega key'), 'invalid-address' ); expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent( 'Invalid Vega key' ); }); it('sends transfer from general accounts', async () => { // 1003-TRAN-002 // 1003-TRAN-003 // 1002-WITH-010 // 1003-TRAN-011 // 1003-TRAN-014 renderComponent(props); // check current pubkey not shown const keySelect = screen.getByLabelText('To Vega key'); expect(keySelect.children).toHaveLength(3); expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([ '', pubKey, props.pubKeys[1], ]); await submit(); expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value // Select a pubkey await userEvent.selectOptions( screen.getByLabelText('To Vega key'), props.pubKeys[1] ); // Select asset await selectAsset(asset); await userEvent.selectOptions( screen.getByLabelText('From account'), `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` ); const amountInput = screen.getByLabelText('Amount'); // Test use max button await userEvent.click(screen.getByRole('button', { name: 'Use max' })); expect(amountInput).toHaveValue('1000.00'); // Test amount validation await userEvent.clear(amountInput); await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount expect( await screen.findByText(/Amount below minimum requirement/) ).toBeInTheDocument(); await userEvent.clear(amountInput); await userEvent.type(amountInput, '9999999'); expect( await screen.findByText(/cannot transfer more/i) ).toBeInTheDocument(); // set valid amount await userEvent.clear(amountInput); await userEvent.type(amountInput, amount); expect(screen.getByTestId('transfer-fee')).toHaveTextContent( new BigNumber(props.feeFactor).times(amount).toFixed() ); await submit(); await waitFor(() => { expect(props.submitTransfer).toHaveBeenCalledTimes(1); expect(props.submitTransfer).toHaveBeenCalledWith({ fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL, toAccountType: AccountType.ACCOUNT_TYPE_GENERAL, to: props.pubKeys[1], asset: asset.id, amount: removeDecimal(amount, asset.decimals), oneOff: {}, }); }); }); it('sends transfer from vested accounts', async () => { const mockSubmit = jest.fn(); renderComponent({ ...props, submitTransfer: mockSubmit, minQuantumMultiple: '100000', }); // check current pubkey not shown const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key'); const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; expect(keySelect.children).toHaveLength(pubKeyOptions.length); expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( pubKeyOptions ); await submit(); expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value // Select a pubkey await userEvent.selectOptions( screen.getByLabelText('To Vega key'), props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later ); // Select asset await selectAsset(asset); await userEvent.selectOptions( screen.getByLabelText('From account'), `${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${asset.id}` ); // Check switch back to connected key expect(screen.getByLabelText('To Vega key')).toHaveValue(props.pubKey); const amountInput = screen.getByLabelText('Amount'); await userEvent.clear(amountInput); await userEvent.type(amountInput, '50'); expect(await screen.findByText(/Use max to bypass/)).toBeInTheDocument(); // Test use max button await userEvent.click(screen.getByRole('button', { name: 'Use max' })); expect(amountInput).toHaveValue('100.00'); // If transfering from a vested account fees should be 0 const expectedFee = '0'; const total = new BigNumber(amount).plus(expectedFee).toFixed(); expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount); expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); await submit(); await waitFor(() => { // 1003-TRAN-023 expect(mockSubmit).toHaveBeenCalledTimes(1); expect(mockSubmit).toHaveBeenCalledWith({ fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS, toAccountType: AccountType.ACCOUNT_TYPE_GENERAL, to: props.pubKey, asset: asset.id, amount: removeDecimal(amount, asset.decimals), oneOff: {}, }); }); }); it('handles lots of decimal places', async () => { const balance = '904195168829277777'; const expectedBalance = '0.904195168829277777'; const longDecimalAsset = { id: 'assetId', symbol: 'VEGA', name: 'VEGA', decimals: 18, quantum: '1', }; const account = { type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS, asset: longDecimalAsset, balance, }; const mockSubmit = jest.fn(); renderComponent({ ...props, accounts: [account], submitTransfer: mockSubmit, minQuantumMultiple: '100000', }); // Select a pubkey await userEvent.selectOptions( screen.getByLabelText('To Vega key'), props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later ); // Select asset await selectAsset(longDecimalAsset); const accountSelect = screen.getByLabelText('From account'); const option = within(accountSelect) .getAllByRole('option') .find( (o) => o.getAttribute('value') === `${account.type}-${account.asset.id}` ); // plus one for disabled 'please select' option expect(option).toHaveTextContent( `${AccountTypeMapping[account.type]} (${expectedBalance} ${ account.asset.symbol })` ); await userEvent.selectOptions( accountSelect, `${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}` ); expect(accountSelect).toHaveValue( `${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}` ); // Check switch back to connected key const amountInput = screen.getByLabelText('Amount'); // Test use max button await userEvent.click(screen.getByRole('button', { name: 'Use max' })); expect(amountInput).toHaveValue(expectedBalance); await submit(); await waitFor(() => { // 1003-TRAN-023 expect(mockSubmit).toHaveBeenCalledTimes(1); expect(mockSubmit).toHaveBeenCalledWith({ fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS, toAccountType: AccountType.ACCOUNT_TYPE_GENERAL, to: props.pubKey, asset: longDecimalAsset.id, amount: balance, oneOff: {}, }); }); }); describe('IncludeFeesCheckbox', () => { it('validates fields when checkbox is not checked', async () => { renderComponent(props); // check current pubkey not shown const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key'); const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; expect(keySelect.children).toHaveLength(pubKeyOptions.length); expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( pubKeyOptions ); await submit(); expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value // Select a pubkey await userEvent.selectOptions( screen.getByLabelText('To Vega key'), props.pubKeys[1] ); // Select asset await selectAsset(asset); await userEvent.selectOptions( screen.getByLabelText('From account'), `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` ); const amountInput = screen.getByLabelText('Amount'); await userEvent.type(amountInput, amount); const expectedFee = new BigNumber(amount) .times(props.feeFactor) .toFixed(); const total = new BigNumber(amount).plus(expectedFee).toFixed(); // 1003-TRAN-021 expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount); expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); }); }); describe('AddressField', () => { const props = { mode: 'select' as const, select:
select
, input:
input
, onChange: jest.fn(), }; it('renders correct content by mode prop and calls onChange', async () => { const mockOnChange = jest.fn(); const { rerender } = render( ); // select should be shown by default expect(screen.getByText('select')).toBeInTheDocument(); expect(screen.queryByText('input')).not.toBeInTheDocument(); await userEvent.click(screen.getByText('Enter manually')); expect(mockOnChange).toHaveBeenCalled(); rerender(); expect(screen.queryByText('select')).not.toBeInTheDocument(); expect(screen.getByText('input')).toBeInTheDocument(); }); }); describe('TransferFee', () => { const props = { amount: '200', feeFactor: '0.001', fee: '0.2', transferAmount: '200', decimals: 8, }; it('calculates and renders the transfer fee', () => { render(); const expected = new BigNumber(props.amount) .times(props.feeFactor) .toFixed(); const total = new BigNumber(props.amount).plus(expected).toFixed(); expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected); expect(screen.getByTestId('transfer-amount')).toHaveTextContent( props.amount ); expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); }); }); });