import React from 'react'; import configureMockStore from 'redux-mock-store'; import { act, fireEvent } from '@testing-library/react'; import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../test/lib/render-helpers'; import { KeyringType } from '../../../shared/constants/keyring'; import TokenAllowance from './token-allowance'; const testTokenAddress = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'; const state = { appState: { customTokenAmount: '1', }, metamask: { accounts: { '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', balance: '0x0', }, }, gasEstimateType: 'none', selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', identities: { '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', name: 'Account 1', }, '0xc42edfcc21ed14dda456aa0756c153f7985d8813': { address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', name: 'Account 2', }, }, cachedBalances: {}, addressBook: [ { address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', chainId: '0x5', isEns: false, memo: '', name: 'Address Book Account 1', }, ], providerConfig: { type: 'mainnet', nickname: '', }, networkDetails: { EIPS: { 1559: true, }, }, preferences: { showFiatInTestnets: true, }, knownMethodData: {}, tokens: [ { address: testTokenAddress, symbol: 'SNX', decimals: 18, image: 'testImage', isERC721: false, }, { address: '0xaD6D458402F60fD3Bd25163575031ACDce07538U', symbol: 'DAU', decimals: 18, image: null, isERC721: false, }, ], unapprovedTxs: {}, keyringTypes: [], keyrings: [ { type: KeyringType.hdKeyTree, accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], }, ], nextNonce: 1, customNonceValue: '', }, history: { mostRecentOverviewPage: '/', }, confirmTransaction: { txData: {}, }, send: { draftTransactions: {}, }, }; const mockShowModal = jest.fn(); jest.mock('../../store/actions', () => ({ disconnectGasFeeEstimatePoller: jest.fn(), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), getGasFeeEstimatesAndStartPolling: jest .fn() .mockImplementation(() => Promise.resolve()), addPollingTokenToAppState: jest.fn(), removePollingTokenFromAppState: jest.fn(), updateTransactionGasFees: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), updatePreviousGasParams: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), createTransactionEventFragment: jest.fn(), getNextNonce: () => jest.fn(), showModal: () => mockShowModal, updateCustomNonce: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), estimateGas: jest.fn().mockImplementation(() => Promise.resolve()), })); jest.mock('../../contexts/gasFee', () => ({ useGasFeeContext: () => ({ maxPriorityFeePerGas: '0.1', maxFeePerGas: '0.1', updateTransaction: jest.fn(), }), })); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); return { ...original, useHistory: () => ({ push: jest.fn(), }), useParams: () => ({ address: testTokenAddress, }), }; }); describe('TokenAllowancePage', () => { const props = { origin: 'https://metamask.github.io', siteImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg', useNonceField: false, currentCurrency: 'usd', nativeCurrency: 'GoerliETH', ethTransactionTotal: '0.0012', fiatTransactionTotal: '1.6', hexTransactionTotal: '0x44364c5bb0000', isMultiLayerFeeNetwork: false, supportsEIP1559: true, userAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', tokenAddress: '0x55797717b9947b31306f4aac7ad1365c6e3923bd', data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170', isSetApproveForAll: false, setApproveForAllArg: false, decimals: '4', dappProposedTokenAmount: '7', currentTokenBalance: '10', toAddress: '0x9bc5baf874d2da8d216ae9f137804184ee5afef4', tokenSymbol: 'TST', showCustomizeGasModal: jest.fn(), warning: '', txData: { id: 3049568294499567, time: 1664449552289, status: 'unapproved', metamaskNetworkId: '3', originalGasEstimate: '0xea60', userEditedGasLimit: false, chainId: '0x3', loadingDefaults: false, dappSuggestedGasFees: { gasPrice: '0x4a817c800', gas: '0xea60', }, sendFlowHistory: [], txParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd', value: '0x0', data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170', gas: '0xea60', gasPrice: '0x4a817c800', maxFeePerGas: '0x4a817c800', }, origin: 'https://metamask.github.io', type: 'approve', userFeeLevel: 'custom', defaultGasEstimates: { estimateType: 'custom', gas: '0xea60', maxFeePerGas: '0x4a817c800', maxPriorityFeePerGas: '0x4a817c800', gasPrice: '0x4a817c800', }, }, }; let store; beforeEach(() => { store = configureMockStore([thunk])(state); }); it('should match snapshot', () => { const { container } = renderWithProvider( , store, ); expect(container).toMatchSnapshot(); }); it('should render title "Spending cap request for your" in token allowance page', () => { const { getByText } = renderWithProvider( , store, ); expect(getByText('Spending cap request for your')).toBeInTheDocument(); }); it('should render reject button', () => { const { getByTestId } = renderWithProvider( , store, ); const onCloseBtn = getByTestId('page-container-footer-cancel'); expect(onCloseBtn).toBeInTheDocument(); }); it('should not render customize nonce modal if useNonceField is set to false', () => { const { queryByText } = renderWithProvider( , store, ); expect(queryByText('Nonce')).not.toBeInTheDocument(); expect(queryByText('1')).not.toBeInTheDocument(); expect(mockShowModal).not.toHaveBeenCalledTimes(1); }); it('should render customize nonce modal if useNonceField is set to true', () => { props.useNonceField = true; props.nextNonce = 1; const { queryByText, getByText } = renderWithProvider( , store, ); const editButton = getByText('Edit'); expect(queryByText('Nonce')).toBeInTheDocument(); expect(queryByText('1')).toBeInTheDocument(); fireEvent.click(editButton); expect(mockShowModal).toHaveBeenCalledTimes(1); }); it('should render nextNonce value when custom nonce value is a empty string', () => { props.useNonceField = true; props.customNonceValue = ''; const { queryByText, getByText } = renderWithProvider( , store, ); const editButton = getByText('Edit'); expect(queryByText('Nonce')).toBeInTheDocument(); expect(queryByText('1')).toBeInTheDocument(); fireEvent.click(editButton); expect(mockShowModal).toHaveBeenCalledTimes(2); }); it('should render edited custom nonce value', () => { props.useNonceField = true; state.metamask.customNonceValue = '3'; const { queryByText, getByText } = renderWithProvider( , store, ); const editButton = getByText('Edit'); expect(queryByText('Nonce')).toBeInTheDocument(); expect(queryByText('3')).toBeInTheDocument(); fireEvent.click(editButton); expect(mockShowModal).toHaveBeenCalledTimes(3); }); it('should render customize nonce warning if custom nonce value is higher than nextNonce value', () => { props.useNonceField = true; props.nextNonce = 2; props.customNonceValue = '3'; props.warning = 'Nonce is higher than suggested nonce of 2'; const { getByText } = renderWithProvider( , store, ); expect( getByText('Nonce is higher than suggested nonce of 2'), ).toBeInTheDocument(); }); it('should not render customize nonce warning if custom nonce value is lower than nextNonce value', () => { props.useNonceField = true; props.nextNonce = 2; props.customNonceValue = '1'; props.warning = ''; const { container } = renderWithProvider( , store, ); const customizeNonceWarning = container.querySelector( '.token-allowance-container__custom-nonce-warning', ); expect(customizeNonceWarning).not.toBeInTheDocument(); }); it('should render customize nonce modal when next button is clicked and if useNonceField is set to true', () => { props.useNonceField = true; state.metamask.customNonceValue = '2'; const { getByText, getAllByText, queryByText } = renderWithProvider( , store, ); const nextButton = getByText('Next'); fireEvent.click(nextButton); const editButton = getAllByText('Edit'); expect(queryByText('Nonce')).toBeInTheDocument(); expect(queryByText('2')).toBeInTheDocument(); fireEvent.click(editButton[1]); expect(mockShowModal).toHaveBeenCalledTimes(4); }); it('should render customize nonce modal when next button is clicked, than back button is clicked, than return to previous page and if useNonceField is set to true', () => { props.useNonceField = true; state.metamask.customNonceValue = '2'; const { getByText, queryByText } = renderWithProvider( , store, ); const nextButton = getByText('Next'); fireEvent.click(nextButton); const backButton = getByText('< Back'); fireEvent.click(backButton); const editButton = getByText('Edit'); expect(queryByText('Nonce')).toBeInTheDocument(); expect(queryByText('2')).toBeInTheDocument(); fireEvent.click(editButton); expect(mockShowModal).toHaveBeenCalledTimes(5); }); it('should click View details and show function type', () => { const { getByText } = renderWithProvider( , store, ); const viewDetailsButton = getByText('View details'); fireEvent.click(viewDetailsButton); expect(getByText('Function: Approve')).toBeInTheDocument(); }); it('should load the page with dappProposedAmount prefilled and "Use site suggestion" should not be displayed', () => { const { queryByText, getByTestId } = renderWithProvider( , store, ); act(() => { const useSiteSuggestion = queryByText('Use site suggestion'); expect(useSiteSuggestion).not.toBeInTheDocument(); }); const input = getByTestId('custom-spending-cap-input'); expect(input.value).toBe('7'); }); it('should click Use site suggestion and set input value to default', () => { const { getByText, getByTestId } = renderWithProvider( , store, ); const textField = getByTestId('custom-spending-cap-input'); expect(textField.value).toBe('7'); fireEvent.change(textField, { target: { value: '1' } }); expect(textField.value).toBe('1'); act(() => { const useSiteSuggestion = getByText('Use site suggestion'); expect(useSiteSuggestion).toBeInTheDocument(); fireEvent.click(useSiteSuggestion); }); expect(textField.value).toBe('7'); }); it('should call back button when button is clicked and return to previous page', () => { const { getByText } = renderWithProvider( , store, ); const nextButton = getByText('Next'); fireEvent.click(nextButton); expect(getByText('Site requested spending cap')).toBeInTheDocument(); const backButton = getByText('< Back'); fireEvent.click(backButton); expect(getByText('Spending cap request for your')).toBeInTheDocument(); }); it('should click Verify third-party details and show popup Third-party details, then close popup', () => { const { getByText } = renderWithProvider( , store, ); const verifyThirdPartyDetails = getByText('Verify third-party details'); fireEvent.click(verifyThirdPartyDetails); expect(getByText('Third-party details')).toBeInTheDocument(); const gotIt = getByText('Got it'); fireEvent.click(gotIt); expect(gotIt).not.toBeInTheDocument(); }); it('should show ledger info text if the sending address is ledger', () => { const { queryByText, getByText } = renderWithProvider( , store, ); expect(queryByText('Prior to clicking confirm:')).toBeNull(); const nextButton = getByText('Next'); fireEvent.click(nextButton); expect(queryByText('Prior to clicking confirm:')).toBeInTheDocument(); }); it('should not show ledger info text if the sending address is not ledger', () => { const { queryByText, getByText } = renderWithProvider( , store, ); expect(queryByText('Prior to clicking confirm:')).toBeNull(); const nextButton = getByText('Next'); fireEvent.click(nextButton); expect(queryByText('Prior to clicking confirm:')).toBeNull(); }); it('should render security provider response if transaction is malicious', () => { const securityProviderResponse = { flagAsDangerous: 1, reason: 'This has been flagged as potentially suspicious. If you sign, you could lose access to all of your NFTs and any funds or other assets in your wallet.', reason_header: 'Warning', }; const { getByText } = renderWithProvider( , store, ); expect(getByText(securityProviderResponse.reason)).toBeInTheDocument(); }); it('should render from account name in header', () => { const { getByText } = renderWithProvider( , store, ); expect(getByText('Account 1')).toBeInTheDocument(); }); it('should account name from transaction even if currently selected account is different', () => { const newState = { ...state, metamask: { ...state.metamask, selectedAddress: '0xc42edfcc21ed14dda456aa0756c153f7985d8813', }, }; const newStore = configureMockStore([thunk])(newState); const { queryByText } = renderWithProvider( , newStore, ); expect(queryByText('Account 1')).toBeInTheDocument(); expect(queryByText('Account 2')).not.toBeInTheDocument(); }); it('should display security alert if present', () => { const { getByText } = renderWithProvider( , store, ); expect(getByText('This is a deceptive request')).toBeInTheDocument(); }); });