diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 501028977b4e..32ccdbdd8fcb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2941,6 +2941,10 @@ export default class MetamaskController extends EventEmitter { this.networkController.getEIP1559Compatibility.bind( this.networkController, ), + getNetworkConfigurationByNetworkClientId: + this.networkController.getNetworkConfigurationByNetworkClientId.bind( + this.networkController, + ), // PreferencesController setSelectedAddress: (address) => { const account = this.accountsController.getAccountByAddress(address); @@ -3421,6 +3425,12 @@ export default class MetamaskController extends EventEmitter { ), // GasFeeController + gasFeeStartPollingByNetworkClientId: + gasFeeController.startPollingByNetworkClientId.bind(gasFeeController), + + gasFeeStopPollingByPollingToken: + gasFeeController.stopPollingByPollingToken.bind(gasFeeController), + getGasFeeEstimatesAndStartPolling: gasFeeController.getGasFeeEstimatesAndStartPolling.bind( gasFeeController, diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 0a11ad6fba86..be5e60a6a97f 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -1130,7 +1130,6 @@ "ui/hooks/useIncrementedGasFees.js", "ui/hooks/useOriginMetadata.js", "ui/hooks/usePrevious.js", - "ui/hooks/useSafeGasEstimatePolling.js", "ui/hooks/useSegmentContext.js", "ui/hooks/useShouldAnimateGasEstimations.js", "ui/hooks/useShouldShowSpeedUp.js", diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 02377c763766..badc99d28341 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -14,6 +14,7 @@ "importNftsModal": { "open": false }, "gasIsLoading": false, "isLoading": false, + "importTokensModalOpen": false, "modal": { "open": false, "modalState": { @@ -24,6 +25,9 @@ "name": null } }, + "showIpfsModalOpen": false, + "showKeyringRemovalSnapModal": false, + "showWhatsNewPopup": false, "warning": null, "alertOpen": false }, @@ -90,6 +94,38 @@ "priorityFeeTrend": "down", "networkCongestion": 0.90625 }, + "gasFeeEstimatesByChainId": { + "0x5": { + "gasEstimateType": "fee-market", + "gasFeeEstimates": { + "low": { + "minWaitTimeEstimate": 180000, + "maxWaitTimeEstimate": 300000, + "suggestedMaxPriorityFeePerGas": "3", + "suggestedMaxFeePerGas": "53" + }, + "medium": { + "minWaitTimeEstimate": 15000, + "maxWaitTimeEstimate": 60000, + "suggestedMaxPriorityFeePerGas": "7", + "suggestedMaxFeePerGas": "70" + }, + "high": { + "minWaitTimeEstimate": 0, + "maxWaitTimeEstimate": 15000, + "suggestedMaxPriorityFeePerGas": "10", + "suggestedMaxFeePerGas": "100" + }, + "estimatedBaseFee": "50", + "historicalBaseFeeRange": ["28.533098435", "70.351148354"], + "baseFeeTrend": "up", + "latestPriorityFeeRange": ["1", "40"], + "historicalPriorityFeeRange": ["0.1458417", "700.156384646"], + "priorityFeeTrend": "down", + "networkCongestion": 0.90625 + } + } + }, "snaps": [{}], "preferences": { "hideZeroBalanceTokens": false, @@ -97,6 +133,7 @@ "showTestNetworks": true, "useNativeCurrencyAsPrimaryCurrency": true }, + "seedPhraseBackedUp": null, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, "isUnlocked": true, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 82d3c1f69081..6cc1d244740c 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -50,6 +50,38 @@ "participateInMetaMetrics": false, "gasEstimateType": "fee-market", "showBetaHeader": false, + "gasFeeEstimatesByChainId": { + "0x5": { + "gasEstimateType": "fee-market", + "gasFeeEstimates": { + "low": { + "minWaitTimeEstimate": 180000, + "maxWaitTimeEstimate": 300000, + "suggestedMaxPriorityFeePerGas": "3", + "suggestedMaxFeePerGas": "53" + }, + "medium": { + "minWaitTimeEstimate": 15000, + "maxWaitTimeEstimate": 60000, + "suggestedMaxPriorityFeePerGas": "7", + "suggestedMaxFeePerGas": "70" + }, + "high": { + "minWaitTimeEstimate": 0, + "maxWaitTimeEstimate": 15000, + "suggestedMaxPriorityFeePerGas": "10", + "suggestedMaxFeePerGas": "100" + }, + "estimatedBaseFee": "50", + "historicalBaseFeeRange": ["28.533098435", "70.351148354"], + "baseFeeTrend": "up", + "latestPriorityFeeRange": ["1", "40"], + "historicalPriorityFeeRange": ["0.1458417", "700.156384646"], + "priorityFeeTrend": "down", + "networkCongestion": 0.90625 + } + } + }, "permissionHistory": {}, "gasFeeEstimates": { "low": { diff --git a/test/e2e/snaps/test-snap-txinsights-v2.spec.js b/test/e2e/snaps/test-snap-txinsights-v2.spec.js index 34617cd2a9f1..de8b9a148e1c 100644 --- a/test/e2e/snaps/test-snap-txinsights-v2.spec.js +++ b/test/e2e/snaps/test-snap-txinsights-v2.spec.js @@ -99,6 +99,12 @@ describe('Test Snap TxInsights-v2', function () { WINDOW_TITLES.Dialog, windowHandles, ); + + await driver.findClickableElement({ + text: 'Confirm', + tag: 'button', + }); + await driver.waitForSelector({ text: 'Insights Example Snap', tag: 'button', diff --git a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js index 1512b8992a09..2ef329652cf3 100644 --- a/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js +++ b/ui/components/app/cancel-speedup-popover/cancel-speedup-popover.test.js @@ -48,6 +48,15 @@ const MOCK_SUGGESTED_MEDIUM_MAXFEEPERGAS_HEX_WEI = MOCK_SUGGESTED_MEDIUM_MAXFEEPERGAS_BN_WEI.toString(16); jest.mock('../../../store/actions', () => ({ + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => + Promise.resolve({ + chainId: '0x5', + }), + ), disconnectGasFeeEstimatePoller: jest.fn(), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), getGasFeeEstimatesAndStartPolling: jest @@ -85,6 +94,14 @@ const render = ( featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js index 793dec26db85..092464decc55 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js @@ -1,7 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { waitFor } from '@testing-library/react'; +import { act, waitFor } from '@testing-library/react'; import { TransactionStatus } from '@metamask/transaction-controller'; import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; @@ -10,8 +10,13 @@ import TransactionListItemDetails from '.'; jest.mock('../../../store/actions.ts', () => ({ tryReverseResolveAddress: () => jest.fn(), - getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(), - addPollingTokenToAppState: jest.fn(), + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), })); let mockGetCustodianTransactionDeepLink = jest.fn(); @@ -22,34 +27,34 @@ jest.mock('../../../store/institutional/institution-background', () => ({ }), })); -describe('TransactionListItemDetails Component', () => { - const transaction = { - history: [], - id: 1, - status: TransactionStatus.confirmed, - txParams: { - from: '0x1', - gas: GAS_LIMITS.SIMPLE, - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - metadata: { - note: 'some note', - }, - custodyId: '1', - }; - - const transactionGroup = { - transactions: [transaction], - primaryTransaction: transaction, - initialTransaction: transaction, +const transaction = { + history: [], + id: 1, + status: TransactionStatus.confirmed, + txParams: { + from: '0x1', + gas: GAS_LIMITS.SIMPLE, + gasPrice: '0x3b9aca00', nonce: '0xa4', - hasRetried: false, - hasCancelled: false, - }; - + to: '0x2', + value: '0x2386f26fc10000', + }, + metadata: { + note: 'some note', + }, + custodyId: '1', +}; + +const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + nonce: '0xa4', + hasRetried: false, + hasCancelled: false, +}; + +const render = async (overrideProps) => { const rpcPrefs = { blockExplorerUrl: 'https://customblockexplorer.com/', }; @@ -69,70 +74,52 @@ describe('TransactionListItemDetails Component', () => { transactionStatus: () =>
, blockExplorerLinkText, rpcPrefs, + ...overrideProps, }; - it('should render title with title prop', async () => { - const mockStore = configureMockStore([thunk])(mockState); + const mockStore = configureMockStore([thunk])(mockState); + + let result; - const { queryByText } = renderWithProvider( - , - mockStore, - ); + await act( + async () => + (result = renderWithProvider( + , + mockStore, + )), + ); + + return result; +}; + +describe('TransactionListItemDetails Component', () => { + it('should render title with title prop', async () => { + const { queryByText } = await render(); await waitFor(() => { - expect(queryByText(props.title)).toBeInTheDocument(); + expect(queryByText('Test Transaction Details')).toBeInTheDocument(); }); }); describe('Retry button', () => { - it('should render retry button with showRetry prop', () => { - const retryProps = { - ...props, - showRetry: true, - }; - - const mockStore = configureMockStore([thunk])(mockState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); + it('should render retry button with showRetry prop', async () => { + const { queryByTestId } = await render({ showRetry: true }); expect(queryByTestId('rety-button')).toBeInTheDocument(); }); }); describe('Cancel button', () => { - it('should render cancel button with showCancel prop', () => { - const retryProps = { - ...props, - showCancel: true, - }; - - const mockStore = configureMockStore([thunk])(mockState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); + it('should render cancel button with showCancel prop', async () => { + const { queryByTestId } = await render({ showCancel: true }); expect(queryByTestId('cancel-button')).toBeInTheDocument(); }); }); describe('Speedup button', () => { - it('should render speedup button with showSpeedUp prop', () => { - const retryProps = { - ...props, - showSpeedUp: true, - }; - - const mockStore = configureMockStore([thunk])(mockState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); + it('should render speedup button with showSpeedUp prop', async () => { + const { queryByTestId } = await render({ showSpeedUp: true }); expect(queryByTestId('speedup-button')).toBeInTheDocument(); }); @@ -144,9 +131,7 @@ describe('TransactionListItemDetails Component', () => { .fn() .mockReturnValue({ url: 'https://url.com' }); - const mockStore = configureMockStore([thunk])(mockState); - - renderWithProvider(, mockStore); + await render({ showCancel: true }); await waitFor(() => { const custodianViewButton = document.querySelector( @@ -173,15 +158,10 @@ describe('TransactionListItemDetails Component', () => { primaryTransaction: newTransaction, initialTransaction: newTransaction, }; - const mockStore = configureMockStore([thunk])(mockState); - const { queryByText } = renderWithProvider( - , - mockStore, - ); + const { queryByText } = await render({ + transactionGroup: newTransactionGroup, + }); await waitFor(() => { expect(queryByText('some note')).toBeInTheDocument(); diff --git a/ui/components/multichain/pages/send/send.test.js b/ui/components/multichain/pages/send/send.test.js index a6c5bf673009..8682d07c6907 100644 --- a/ui/components/multichain/pages/send/send.test.js +++ b/ui/components/multichain/pages/send/send.test.js @@ -3,6 +3,7 @@ import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; import { NetworkType } from '@metamask/controller-utils'; import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { act } from '@testing-library/react'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider, @@ -12,7 +13,6 @@ import { import { domainInitialState } from '../../../../ducks/domains'; import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../../../test/jest/mocks'; import { GasEstimateTypes } from '../../../../../shared/constants/gas'; -import { KeyringType } from '../../../../../shared/constants/keyring'; import { SEND_STAGES, startNewDraftTransaction } from '../../../../ducks/send'; import { AssetType } from '../../../../../shared/constants/transaction'; import { @@ -22,6 +22,7 @@ import { } from '../../../../../shared/constants/network'; import mockSendState from '../../../../../test/data/mock-send-state.json'; import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { KeyringType } from '../../../../../shared/constants/keyring'; import { SendPage } from '.'; jest.mock('@ethersproject/providers', () => { @@ -45,12 +46,13 @@ jest.mock('../../../../store/actions.ts', () => { const originalModule = jest.requireActual('../../../../store/actions.ts'); return { ...originalModule, - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), getTokenSymbol: jest.fn().mockResolvedValue('ETH'), getGasFeeTimeEstimate: jest .fn() @@ -75,112 +77,70 @@ jest.mock('../../../../ducks/send/send', () => { }; }); -describe('SendPage', () => { - describe('render and initialization', () => { - const middleware = [thunk]; - - const baseStore = { - send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, - DNS: domainInitialState, - gas: { - customData: { limit: null, price: null }, +const baseStore = { + send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + DNS: domainInitialState, + gas: { + customData: { limit: null, price: null }, + }, + history: { mostRecentOverviewPage: 'activity' }, + confirmTransaction: { + txData: { + id: 1, + txParams: { + value: 'oldTxValue', }, - history: { mostRecentOverviewPage: 'activity' }, - metamask: { - transactions: [ - { - id: 1, - txParams: { - value: 'oldTxValue', - }, - }, - ], - currencyRates: { - ETH: { - conversionDate: 1620710825.03, - conversionRate: 3910.28, - usdConversionRate: 3910.28, - }, + }, + }, + metamask: { + permissionHistory: {}, + transactions: [ + { + id: 1, + txParams: { + value: 'oldTxValue', }, - gasEstimateType: GasEstimateTypes.legacy, + }, + ], + + currencyRates: { + ETH: { + conversionDate: 1620710825.03, + conversionRate: 3910.28, + usdConversionRate: 3910.28, + }, + }, + gasEstimateType: GasEstimateTypes.legacy, + gasFeeEstimates: { + low: '0', + medium: '1', + fast: '2', + }, + gasFeeEstimatesByChainId: { + '0x5': { gasFeeEstimates: { low: '0', medium: '1', fast: '2', }, - selectedAddress: '0x0', - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [...Object.values(EthMethod)], - type: EthAccountType.Eoa, + gasEstimateType: GasEstimateTypes.legacy, + }, + }, + selectedAddress: '0x0', + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x0', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'HD Key Tree', }, }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: KeyringType.hdKeyTree, - accounts: ['0x0'], - }, - ], - selectedNetworkClientId: NetworkType.mainnet, - networksMetadata: { - [NetworkType.mainnet]: { - EIPS: {}, - status: 'available', - }, - }, - tokens: [], - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, - currentCurrency: 'USD', - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - }, - nativeCurrency: 'ETH', - featureFlags: { - sendHexData: false, - }, - addressBook: { - [CHAIN_IDS.GOERLI]: [], - }, - cachedBalances: { - [CHAIN_IDS.GOERLI]: {}, - }, - accounts: { - '0x0': { balance: '0x0', address: '0x0', name: 'Account 1' }, - }, - identities: { '0x0': { address: '0x0' } }, - tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', - tokenList: { - '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { - name: 'BitBase', - symbol: 'BTBS', - decimals: 18, - address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356', - iconUrl: 'BTBS.svg', - occurrences: null, - }, - '0x3fa400483487a489ec9b1db29c4129063eec4654': { - name: 'Cryptokek.com', - symbol: 'KEK', - decimals: 18, - address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654', - iconUrl: 'cryptokek.svg', - occurrences: null, - }, + options: {}, + methods: [...Object.values(EthMethod)], + type: EthAccountType.Eoa, }, permissionHistory: { 'https://uniswap.org/': { @@ -198,10 +158,88 @@ describe('SendPage', () => { appState: { sendInputCurrencySwitched: false, }, - }; + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, + keyrings: [ + { + type: KeyringType.hdKeyTree, + accounts: ['0x0'], + }, + ], + selectedNetworkClientId: NetworkType.goerli, + networksMetadata: { + [NetworkType.goerli]: { + EIPS: {}, + status: 'available', + }, + }, + tokens: [], + preferences: { + useNativeCurrencyAsPrimaryCurrency: false, + }, + currentCurrency: 'USD', + providerConfig: { + chainId: CHAIN_IDS.GOERLI, + nickname: GOERLI_DISPLAY_NAME, + }, + nativeCurrency: 'ETH', + featureFlags: { + sendHexData: false, + }, + addressBook: { + [CHAIN_IDS.GOERLI]: [], + }, + cachedBalances: { + [CHAIN_IDS.GOERLI]: {}, + }, + accounts: { + '0x0': { balance: '0x0', address: '0x0', name: 'Account 1' }, + }, + identities: { '0x0': { address: '0x0' } }, + tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', + tokenList: { + '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { + name: 'BitBase', + symbol: 'BTBS', + decimals: 18, + address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356', + iconUrl: 'BTBS.svg', + occurrences: null, + }, + '0x3fa400483487a489ec9b1db29c4129063eec4654': { + name: 'Cryptokek.com', + symbol: 'KEK', + decimals: 18, + address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654', + iconUrl: 'cryptokek.svg', + occurrences: null, + }, + }, + }, + activeTab: { + origin: 'https://uniswap.org/', + }, + appState: { + sendInputCurrencySwitched: false, + }, +}; + +const render = async (state) => { + const middleware = [thunk]; - it('should initialize the ENS slice on render', () => { - const store = configureMockStore(middleware)({ + const store = configureMockStore(middleware)(state); + + let result; + + await act(async () => (result = renderWithProvider(, store))); + + return { store, result }; +}; + +describe('SendPage', () => { + describe('render and initialization', () => { + it('should initialize the ENS slice on render', async () => { + const { store } = await render({ ...baseStore, metamask: { ...baseStore.metamask, @@ -212,7 +250,6 @@ describe('SendPage', () => { hiddenAccountList: [], }, }); - renderWithProvider(, store); const actions = store.getActions(); expect(actions).toStrictEqual( expect.arrayContaining([ @@ -223,8 +260,8 @@ describe('SendPage', () => { ); }); - it('should render correctly even when a draftTransaction does not exist', () => { - const modifiedStore = { + it('should render correctly even when a draftTransaction does not exist', async () => { + const modifiedState = { ...baseStore, metamask: { ...baseStore.metamask, @@ -239,9 +276,9 @@ describe('SendPage', () => { currentTransactionUUID: null, }, }; - const store = configureMockStore(middleware)(modifiedStore); - const { container, getByTestId, getByPlaceholderText } = - renderWithProvider(, store); + const { + result: { container, getByTestId, getByPlaceholderText }, + } = await render(modifiedState); // Ensure that the send flow renders on the add recipient screen when // there is no draft transaction. @@ -259,20 +296,22 @@ describe('SendPage', () => { }); describe('footer buttons', () => { - const mockStore = configureMockStore([thunk])(mockState); - describe('onCancel', () => { - it('should call reset send state and route to recent page without cancelling tx', () => { - const { queryByText } = renderWithProvider(, mockStore); + it('should call reset send state and route to recent page without cancelling tx', async () => { + const { + result: { queryByText }, + } = await render(mockState); const cancelText = queryByText('Cancel'); - fireEvent.click(cancelText); + await act(async () => { + fireEvent.click(cancelText); + }); expect(mockResetSendState).toHaveBeenCalled(); expect(mockCancelTx).not.toHaveBeenCalled(); }); - it('should reject/cancel tx when coming from tx editing and route to index', () => { + it('should reject/cancel tx when coming from tx editing and route to index', async () => { const sendDataState = { ...mockState, send: { @@ -294,15 +333,14 @@ describe('SendPage', () => { }, }; - const sendStateStore = configureMockStore([thunk])(sendDataState); - - const { queryByText } = renderWithProvider( - , - sendStateStore, - ); + const { + result: { queryByText }, + } = await render(sendDataState); const rejectText = queryByText('Reject'); - fireEvent.click(rejectText); + await act(async () => { + fireEvent.click(rejectText); + }); expect(mockResetSendState).toHaveBeenCalled(); expect(mockCancelTx).toHaveBeenCalled(); @@ -340,14 +378,9 @@ describe('SendPage', () => { }, }; - const sendStateStore = configureMockStore([thunk])( - knownRecipientWarningState, - ); - - const { queryByTestId } = renderWithProvider( - , - sendStateStore, - ); + const { + result: { queryByTestId }, + } = await render(knownRecipientWarningState); const sendWarning = queryByTestId('send-warning'); await waitFor(() => { diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 775491e252ea..d030610427aa 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -353,12 +353,14 @@ export function isNotEIP1559Network(state) { * Function returns true if network details are fetched and it is found to support EIP-1559 * * @param state + * @param networkClientId - The optional network client ID to check for EIP-1559 support. Defaults to the currently selected network. */ -export function isEIP1559Network(state) { +export function isEIP1559Network(state, networkClientId) { const selectedNetworkClientId = getSelectedNetworkClientId(state); return ( - state.metamask.networksMetadata?.[selectedNetworkClientId].EIPS[1559] === - true + state.metamask.networksMetadata?.[ + networkClientId ?? selectedNetworkClientId + ].EIPS[1559] === true ); } @@ -400,6 +402,19 @@ export function getEstimatedGasFeeTimeBounds(state) { return state.metamask.estimatedGasFeeTimeBounds; } +export function getGasEstimateTypeByChainId(state, chainId) { + return state.metamask.gasFeeEstimatesByChainId?.[chainId]?.gasEstimateType; +} + +export function getGasFeeEstimatesByChainId(state, chainId) { + return state.metamask.gasFeeEstimatesByChainId?.[chainId]?.gasFeeEstimates; +} + +export function getEstimatedGasFeeTimeBoundsByChainId(state, chainId) { + return state.metamask.gasFeeEstimatesByChainId?.[chainId] + ?.estimatedGasFeeTimeBounds; +} + export function getIsGasEstimatesLoading(state) { const networkAndAccountSupports1559 = checkNetworkAndAccountSupports1559(state); @@ -420,11 +435,41 @@ export function getIsGasEstimatesLoading(state) { return isGasEstimatesLoading; } +export function getIsGasEstimatesLoadingByChainId( + state, + { chainId, networkClientId }, +) { + const networkAndAccountSupports1559 = checkNetworkAndAccountSupports1559( + state, + networkClientId, + ); + const gasEstimateType = getGasEstimateTypeByChainId(state, chainId); + + // We consider the gas estimate to be loading if the gasEstimateType is + // 'NONE' or if the current gasEstimateType cannot be supported by the current + // network + const isEIP1559TolerableEstimateType = + gasEstimateType === GasEstimateTypes.feeMarket || + gasEstimateType === GasEstimateTypes.ethGasPrice; + const isGasEstimatesLoading = + gasEstimateType === GasEstimateTypes.none || + (networkAndAccountSupports1559 && !isEIP1559TolerableEstimateType) || + (!networkAndAccountSupports1559 && + gasEstimateType === GasEstimateTypes.feeMarket); + + return isGasEstimatesLoading; +} + export function getIsNetworkBusy(state) { const gasFeeEstimates = getGasFeeEstimates(state); return gasFeeEstimates?.networkCongestion >= NetworkCongestionThresholds.busy; } +export function getIsNetworkBusyByChainId(state, chainId) { + const gasFeeEstimates = getGasFeeEstimatesByChainId(state, chainId); + return gasFeeEstimates?.networkCongestion >= NetworkCongestionThresholds.busy; +} + export function getCompletedOnboarding(state) { return state.metamask.completedOnboarding; } diff --git a/ui/helpers/utils/gas.js b/ui/helpers/utils/gas.js index d5e7633078ca..65103972f3fb 100644 --- a/ui/helpers/utils/gas.js +++ b/ui/helpers/utils/gas.js @@ -25,7 +25,7 @@ export const gasEstimateGreaterThanGasUsedPlusTenPercent = ( ); const maxFeePerGasFromEstimate = - gasFeeEstimates[estimate]?.suggestedMaxFeePerGas; + gasFeeEstimates?.[estimate]?.suggestedMaxFeePerGas; return bnGreaterThan(maxFeePerGasFromEstimate, maxFeePerGasInTransaction); }; diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js index 1cd11ccc0727..5ad37925054b 100644 --- a/ui/hooks/useGasFeeEstimates.js +++ b/ui/hooks/useGasFeeEstimates.js @@ -1,12 +1,19 @@ import isEqual from 'lodash/isEqual'; import { useSelector } from 'react-redux'; +import { useEffect, useState } from 'react'; import { - getGasEstimateType, - getGasFeeEstimates, - getIsGasEstimatesLoading, - getIsNetworkBusy, + getGasEstimateTypeByChainId, + getGasFeeEstimatesByChainId, + getIsGasEstimatesLoadingByChainId, + getIsNetworkBusyByChainId, } from '../ducks/metamask/metamask'; -import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling'; +import { + gasFeeStartPollingByNetworkClientId, + gasFeeStopPollingByPollingToken, + getNetworkConfigurationByNetworkClientId, +} from '../store/actions'; +import { getSelectedNetworkClientId } from '../selectors'; +import usePolling from './usePolling'; /** * @typedef {object} GasEstimates @@ -25,14 +32,52 @@ import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling'; * GasFeeController that it is done requiring new gas estimates. Also checks * the returned gas estimate for validity on the current network. * + * @param _networkClientId - The optional network client ID to get gas fee estimates for. Defaults to the currently selected network. * @returns {GasEstimates} GasEstimates object */ -export function useGasFeeEstimates() { - const gasEstimateType = useSelector(getGasEstimateType); - const gasFeeEstimates = useSelector(getGasFeeEstimates, isEqual); - const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); - const isNetworkBusy = useSelector(getIsNetworkBusy); - useSafeGasEstimatePolling(); +export function useGasFeeEstimates(_networkClientId) { + const selectedNetworkClientId = useSelector(getSelectedNetworkClientId); + const networkClientId = _networkClientId ?? selectedNetworkClientId; + + const [chainId, setChainId] = useState(''); + + const gasEstimateType = useSelector((state) => + getGasEstimateTypeByChainId(state, chainId), + ); + const gasFeeEstimates = useSelector( + (state) => getGasFeeEstimatesByChainId(state, chainId), + isEqual, + ); + const isGasEstimatesLoading = useSelector((state) => + getIsGasEstimatesLoadingByChainId(state, { + chainId, + networkClientId, + }), + ); + const isNetworkBusy = useSelector((state) => + getIsNetworkBusyByChainId(state, chainId), + ); + + useEffect(() => { + let isMounted = true; + getNetworkConfigurationByNetworkClientId(networkClientId).then( + (networkConfig) => { + if (networkConfig && isMounted) { + setChainId(networkConfig.chainId); + } + }, + ); + + return () => { + isMounted = false; + }; + }, [networkClientId]); + + usePolling({ + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + stopPollingByPollingToken: gasFeeStopPollingByPollingToken, + networkClientId, + }); return { gasFeeEstimates, diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js index a76633c112a0..0187ac793bbe 100644 --- a/ui/hooks/useGasFeeEstimates.test.js +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -1,25 +1,49 @@ -import { cleanup, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; import { GasEstimateTypes } from '../../shared/constants/gas'; -import createRandomId from '../../shared/modules/random-id'; import { - getGasEstimateType, - getGasFeeEstimates, - getIsGasEstimatesLoading, + getGasEstimateTypeByChainId, + getGasFeeEstimatesByChainId, + getIsGasEstimatesLoadingByChainId, + getIsNetworkBusyByChainId, } from '../ducks/metamask/metamask'; -import { checkNetworkAndAccountSupports1559 } from '../selectors'; import { - disconnectGasFeeEstimatePoller, - getGasFeeEstimatesAndStartPolling, + gasFeeStartPollingByNetworkClientId, + gasFeeStopPollingByPollingToken, + getNetworkConfigurationByNetworkClientId, } from '../store/actions'; import { useGasFeeEstimates } from './useGasFeeEstimates'; +import usePolling from './usePolling'; + +jest.mock('./usePolling', () => jest.fn()); jest.mock('../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest.fn(), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn(), +})); + +jest.mock('../ducks/metamask/metamask', () => ({ + getGasEstimateTypeByChainId: jest + .fn() + .mockReturnValue('getGasEstimateTypeByChainId'), + getGasFeeEstimatesByChainId: jest + .fn() + .mockReturnValue('getGasFeeEstimatesByChainId'), + getIsGasEstimatesLoadingByChainId: jest + .fn() + .mockReturnValue('getIsGasEstimatesLoadingByChainId'), + getIsNetworkBusyByChainId: jest + .fn() + .mockReturnValue('getIsNetworkBusyByChainId'), +})); + +jest.mock('../selectors', () => ({ + checkNetworkAndAccountSupports1559: jest + .fn() + .mockReturnValue('checkNetworkAndAccountSupports1559'), + getSelectedNetworkClientId: jest + .fn() + .mockReturnValue('getSelectedNetworkClientId'), })); jest.mock('react-redux', () => { @@ -42,76 +66,105 @@ const DEFAULT_OPTS = { isGasEstimatesLoading: true, }; +const MOCK_STATE = {}; + const generateUseSelectorRouter = (opts = DEFAULT_OPTS) => (selector) => { - if (selector === checkNetworkAndAccountSupports1559) { + const selectorId = selector(MOCK_STATE); + if (selectorId === 'checkNetworkAndAccountSupports1559') { return ( opts.checkNetworkAndAccountSupports1559 ?? DEFAULT_OPTS.checkNetworkAndAccountSupports1559 ); } - if (selector === getGasEstimateType) { + if (selectorId === 'getSelectedNetworkClientId') { + return 'selectedNetworkClientId'; + } + if (selectorId === 'getGasEstimateTypeByChainId') { return opts.gasEstimateType ?? DEFAULT_OPTS.gasEstimateType; } - if (selector === getGasFeeEstimates) { + if (selectorId === 'getGasFeeEstimatesByChainId') { return opts.gasFeeEstimates ?? DEFAULT_OPTS.gasFeeEstimates; } - if (selector === getIsGasEstimatesLoading) { + if (selectorId === 'getIsGasEstimatesLoadingByChainId') { return opts.isGasEstimatesLoading ?? DEFAULT_OPTS.isGasEstimatesLoading; } return undefined; }; describe('useGasFeeEstimates', () => { - let tokens = []; beforeEach(() => { jest.clearAllMocks(); - tokens = []; - getGasFeeEstimatesAndStartPolling.mockImplementation(() => { - const token = createRandomId(); - tokens.push(token); - return Promise.resolve(token); + getNetworkConfigurationByNetworkClientId.mockImplementation( + (networkClientId) => { + if (!networkClientId) { + return Promise.resolve(undefined); + } + + return Promise.resolve({ + chainId: '0xa', + }); + }, + ); + }); + + it('polls the selected networkClientId by default', async () => { + useSelector.mockImplementation(generateUseSelectorRouter()); + await act(async () => { + renderHook(() => useGasFeeEstimates()); }); - disconnectGasFeeEstimatePoller.mockImplementation((token) => { - tokens = tokens.filter((tkn) => tkn !== token); + expect(usePolling).toHaveBeenCalledWith({ + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + stopPollingByPollingToken: gasFeeStopPollingByPollingToken, + networkClientId: 'selectedNetworkClientId', }); }); - it('registers with the controller', () => { + it('polls the passed in networkClientId when provided', async () => { useSelector.mockImplementation(generateUseSelectorRouter()); - renderHook(() => useGasFeeEstimates()); - expect(tokens).toHaveLength(1); + await act(async () => { + renderHook(() => useGasFeeEstimates('networkClientId1')); + }); + expect(usePolling).toHaveBeenCalledWith({ + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + stopPollingByPollingToken: gasFeeStopPollingByPollingToken, + networkClientId: 'networkClientId1', + }); }); - it('clears token with the controller on unmount', async () => { + it('reads state with the right chainId and networkClientId', async () => { useSelector.mockImplementation(generateUseSelectorRouter()); - renderHook(() => useGasFeeEstimates()); - expect(tokens).toHaveLength(1); - const expectedToken = tokens[0]; - await cleanup(); - expect(getGasFeeEstimatesAndStartPolling).toHaveBeenCalledTimes(1); - expect(disconnectGasFeeEstimatePoller).toHaveBeenCalledWith(expectedToken); - expect(tokens).toHaveLength(0); + + await act(async () => + renderHook(() => useGasFeeEstimates('networkClientId1')), + ); + expect(getGasEstimateTypeByChainId).toHaveBeenCalledWith(MOCK_STATE, '0xa'); + expect(getGasFeeEstimatesByChainId).toHaveBeenCalledWith(MOCK_STATE, '0xa'); + expect(getIsGasEstimatesLoadingByChainId).toHaveBeenCalledWith(MOCK_STATE, { + chainId: '0xa', + networkClientId: 'networkClientId1', + }); + expect(getIsNetworkBusyByChainId).toHaveBeenCalledWith(MOCK_STATE, '0xa'); }); - it('works with LEGACY gas prices', () => { + it('works with LEGACY gas prices', async () => { useSelector.mockImplementation( generateUseSelectorRouter({ isGasEstimatesLoading: false, }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates: DEFAULT_OPTS.gasFeeEstimates, gasEstimateType: GasEstimateTypes.legacy, isGasEstimatesLoading: false, }); }); - it('works with ETH_GASPRICE gas prices', () => { + it('works with ETH_GASPRICE gas prices', async () => { const gasFeeEstimates = { gasPrice: '10' }; useSelector.mockImplementation( generateUseSelectorRouter({ @@ -121,17 +174,16 @@ describe('useGasFeeEstimates', () => { }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates, gasEstimateType: GasEstimateTypes.ethGasPrice, isGasEstimatesLoading: false, }); }); - it('works with FEE_MARKET gas prices', () => { + it('works with FEE_MARKET gas prices', async () => { const gasFeeEstimates = { low: { minWaitTimeEstimate: 180000, @@ -162,17 +214,16 @@ describe('useGasFeeEstimates', () => { }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates, gasEstimateType: GasEstimateTypes.feeMarket, isGasEstimatesLoading: false, }); }); - it('indicates that gas estimates are loading when gasEstimateType is NONE', () => { + it('indicates that gas estimates are loading when gasEstimateType is NONE', async () => { useSelector.mockImplementation( generateUseSelectorRouter({ gasEstimateType: GasEstimateTypes.none, @@ -180,17 +231,16 @@ describe('useGasFeeEstimates', () => { }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates: {}, gasEstimateType: GasEstimateTypes.none, isGasEstimatesLoading: true, }); }); - it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET or ETH_GASPRICE, but network supports EIP-1559', () => { + it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET or ETH_GASPRICE, but network supports EIP-1559', async () => { useSelector.mockImplementation( generateUseSelectorRouter({ checkNetworkAndAccountSupports1559: true, @@ -201,17 +251,16 @@ describe('useGasFeeEstimates', () => { }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates: { gasPrice: '10' }, gasEstimateType: GasEstimateTypes.legacy, isGasEstimatesLoading: true, }); }); - it('indicates that gas estimates are loading when gasEstimateType is FEE_MARKET but network does not support EIP-1559', () => { + it('indicates that gas estimates are loading when gasEstimateType is FEE_MARKET but network does not support EIP-1559', async () => { const gasFeeEstimates = { low: { minWaitTimeEstimate: 180000, @@ -241,10 +290,9 @@ describe('useGasFeeEstimates', () => { }), ); - const { - result: { current }, - } = renderHook(() => useGasFeeEstimates()); - expect(current).toMatchObject({ + let hook; + await act(async () => (hook = renderHook(() => useGasFeeEstimates()))); + expect(hook.result.current).toMatchObject({ gasFeeEstimates, gasEstimateType: GasEstimateTypes.feeMarket, isGasEstimatesLoading: true, diff --git a/ui/hooks/usePolling.test.js b/ui/hooks/usePolling.test.js new file mode 100644 index 000000000000..9250257d3cbc --- /dev/null +++ b/ui/hooks/usePolling.test.js @@ -0,0 +1,66 @@ +import { cleanup } from '@testing-library/react-hooks'; +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import usePolling from './usePolling'; + +describe('usePolling', () => { + // eslint-disable-next-line jest/no-done-callback + it('calls startPollingByNetworkClientId and callback option args with polling token when component instantiating the hook mounts', (done) => { + const mockStart = jest.fn().mockImplementation(() => { + return Promise.resolve('pollingToken'); + }); + const mockStop = jest.fn(); + const networkClientId = 'mainnet'; + const options = {}; + const mockState = { + metamask: {}, + }; + + renderHookWithProvider(() => { + usePolling({ + callback: (pollingToken) => { + expect(mockStart).toHaveBeenCalledWith(networkClientId, options); + expect(pollingToken).toBeDefined(); + done(); + return (_pollingToken) => { + // noop + }; + }, + startPollingByNetworkClientId: mockStart, + stopPollingByPollingToken: mockStop, + networkClientId, + options, + }); + }, mockState); + }); + // eslint-disable-next-line jest/no-done-callback + it('calls the cleanup function with the correct pollingToken when unmounted', (done) => { + const mockStart = jest.fn().mockImplementation(() => { + return Promise.resolve('pollingToken'); + }); + const mockStop = jest.fn(); + const networkClientId = 'mainnet'; + const options = {}; + const mockState = { + metamask: {}, + }; + + renderHookWithProvider( + () => + usePolling({ + callback: () => { + return (_pollingToken) => { + expect(mockStop).toHaveBeenCalledWith(_pollingToken); + expect(_pollingToken).toBeDefined(); + done(); + }; + }, + startPollingByNetworkClientId: mockStart, + stopPollingByPollingToken: mockStop, + networkClientId, + options, + }), + mockState, + ); + cleanup(); + }); +}); diff --git a/ui/hooks/usePolling.ts b/ui/hooks/usePolling.ts new file mode 100644 index 000000000000..ae64d3c73bb6 --- /dev/null +++ b/ui/hooks/usePolling.ts @@ -0,0 +1,56 @@ +import { useEffect, useRef } from 'react'; + +type UsePollingOptions = { + callback?: (pollingToken: string) => (pollingToken: string) => void; + startPollingByNetworkClientId: ( + networkClientId: string, + options: any, + ) => Promise; + stopPollingByPollingToken: (pollingToken: string) => void; + networkClientId: string; + options?: any; +}; + +const usePolling = (usePollingOptions: UsePollingOptions) => { + const pollTokenRef = useRef(null); + const cleanupRef = useRef void)>(null); + let isMounted = false; + useEffect(() => { + isMounted = true; + + const cleanup = () => { + if (pollTokenRef.current) { + usePollingOptions.stopPollingByPollingToken(pollTokenRef.current); + cleanupRef.current?.(pollTokenRef.current); + } + }; + // Start polling when the component mounts + usePollingOptions + .startPollingByNetworkClientId( + usePollingOptions.networkClientId, + usePollingOptions.options, + ) + .then((pollToken) => { + pollTokenRef.current = pollToken; + cleanupRef.current = usePollingOptions.callback?.(pollToken) || null; + if (!isMounted) { + cleanup(); + } + }); + + // Return a cleanup function to stop polling when the component unmounts + return () => { + isMounted = false; + cleanup(); + }; + }, [ + usePollingOptions.networkClientId, + usePollingOptions.options && + JSON.stringify( + usePollingOptions.options, + Object.keys(usePollingOptions.options).sort(), + ), + ]); +}; + +export default usePolling; diff --git a/ui/hooks/useSafeGasEstimatePolling.js b/ui/hooks/useSafeGasEstimatePolling.js deleted file mode 100644 index 156f805e6737..000000000000 --- a/ui/hooks/useSafeGasEstimatePolling.js +++ /dev/null @@ -1,47 +0,0 @@ -import { useEffect } from 'react'; -import { - disconnectGasFeeEstimatePoller, - getGasFeeEstimatesAndStartPolling, - addPollingTokenToAppState, - removePollingTokenFromAppState, -} from '../store/actions'; - -/** - * Provides a reusable hook that can be used for safely updating the polling - * data in the gas fee controller. It makes a request to get estimates and - * begin polling, keeping track of the poll token for the lifetime of the hook. - * It then disconnects polling upon unmount. If the hook is unmounted while waiting - * for `getGasFeeEstimatesAndStartPolling` to resolve, the `active` flag ensures - * that a call to disconnect happens after promise resolution. - */ -export function useSafeGasEstimatePolling() { - useEffect(() => { - let active = true; - let pollToken; - - const cleanup = () => { - active = false; - if (pollToken) { - disconnectGasFeeEstimatePoller(pollToken); - removePollingTokenFromAppState(pollToken); - } - }; - - getGasFeeEstimatesAndStartPolling().then((newPollToken) => { - if (active) { - pollToken = newPollToken; - addPollingTokenToAppState(pollToken); - } else { - disconnectGasFeeEstimatePoller(newPollToken); - removePollingTokenFromAppState(pollToken); - } - }); - - window.addEventListener('beforeunload', cleanup); - - return () => { - cleanup(); - window.removeEventListener('beforeunload', cleanup); - }; - }, []); -} diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js index c6e8289965c9..1b7c258a3705 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { EditGasModes, @@ -21,18 +21,19 @@ import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults'; const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.'; jest.mock('../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), setAdvancedGasFee: jest.fn(), updateEventFragment: jest.fn(), createTransactionEventFragment: jest.fn(), })); -const render = (defaultGasParams, contextParams) => { +const render = async (defaultGasParams, contextParams) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -46,38 +47,55 @@ const render = (defaultGasParams, contextParams) => { featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - - - - , - store, + + let result; + + await act( + async () => + (result = renderWithProvider( + + + + + + , + store, + )), ); + + return result; }; + describe('AdvancedGasFeeDefaults', () => { - it('should renders correct message when the default is not set', () => { - render({ advancedGasFee: {} }); + it('should renders correct message when the default is not set', async () => { + await render({ advancedGasFee: {} }); expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); - it('should renders correct message when the default values are set', () => { - render({ + it('should renders correct message when the default values are set', async () => { + await render({ advancedGasFee: { [CHAIN_IDS.GOERLI]: { maxBaseFee: '50', priorityFee: '2' }, }, }); expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); - it('should renders correct message when the default values are set and the maxBaseFee values are updated', () => { - render({ + it('should renders correct message when the default values are set and the maxBaseFee values are updated', async () => { + await render({ advancedGasFee: { [CHAIN_IDS.GOERLI]: { maxBaseFee: '50', priorityFee: '2' }, }, @@ -91,8 +109,8 @@ describe('AdvancedGasFeeDefaults', () => { expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); - it('should renders correct message when the default values are set and the priorityFee values are updated', () => { - render({ + it('should renders correct message when the default values are set and the priorityFee values are updated', async () => { + await render({ advancedGasFee: { [CHAIN_IDS.GOERLI]: { maxBaseFee: '50', priorityFee: '2' }, }, @@ -107,8 +125,8 @@ describe('AdvancedGasFeeDefaults', () => { expect(screen.queryByText(TEXT_SELECTOR)).toBeInTheDocument(); }); - it('should call action setAdvancedGasFee when checkbox or label text is clicked', () => { - render({ + it('should call action setAdvancedGasFee when checkbox or label text is clicked', async () => { + await render({ advancedGasFee: { [CHAIN_IDS.GOERLI]: { maxBaseFee: 50, priorityFee: 2 }, }, @@ -124,8 +142,8 @@ describe('AdvancedGasFeeDefaults', () => { expect(mock).toHaveBeenCalledTimes(2); }); - it('should not render option to set default gas options in a swaps transaction', () => { - render({}, { editGasMode: EditGasModes.swaps }); + it('should not render option to set default gas options in a swaps transaction', async () => { + await render({}, { editGasMode: EditGasModes.swaps }); expect( document.querySelector('input[type=checkbox]'), ).not.toBeInTheDocument(); diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js index 51e0c5ed9c6d..bba6d4c5aba8 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-gas-limit/advanced-gas-fee-gas-limit.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { GasEstimateTypes } from '../../../../../../shared/constants/gas'; import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; @@ -13,15 +13,16 @@ import { AdvancedGasFeePopoverContextProvider } from '../context'; import AdvancedGasFeeGasLimit from './advanced-gas-fee-gas-limit'; jest.mock('../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), })); -const render = (contextProps) => { +const render = async (contextProps) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -31,44 +32,59 @@ const render = (contextProps) => { balance: '0x1F4', }, }, - advancedGasFee: { priorityFee: 100 }, + advancedGasFee: { '0x5': { priorityFee: 100 } }, featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + + + , + store, + )), ); + + return result; }; describe('AdvancedGasFeeGasLimit', () => { - it('should show GasLimit from transaction', () => { - render(); + it('should show GasLimit from transaction', async () => { + await render(); expect(screen.getByText('21000')).toBeInTheDocument(); }); - it('should show input when edit link is clicked', () => { - render(); + it('should show input when edit link is clicked', async () => { + await render(); expect(document.getElementsByTagName('input')).toHaveLength(0); fireEvent.click(screen.queryByText('Edit')); expect(document.getElementsByTagName('input')[0]).toHaveValue(21000); }); - it('should show error if gas limit is not in range', () => { - render(); + it('should show error if gas limit is not in range', async () => { + await render(); fireEvent.click(screen.queryByText('Edit')); fireEvent.change(document.getElementsByTagName('input')[0], { target: { value: 20000 }, @@ -96,8 +112,8 @@ describe('AdvancedGasFeeGasLimit', () => { ).not.toBeInTheDocument(); }); - it('should validate gas limit against minimumGasLimit it is passed to context', () => { - render({ minimumGasLimit: '0x7530' }); + it('should validate gas limit against minimumGasLimit it is passed to context', async () => { + await render({ minimumGasLimit: '0x7530' }); fireEvent.click(screen.queryByText('Edit')); fireEvent.change(document.getElementsByTagName('input')[0], { target: { value: 25000 }, diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js index 80e4d08f5ec8..32c5ed716e1f 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js @@ -1,24 +1,45 @@ import React from 'react'; +import { act } from '@testing-library/react'; import { renderWithProvider, screen } from '../../../../../../test/jest'; import configureStore from '../../../../../store/store'; import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext'; jest.mock('../../../../../store/actions', () => ({ + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => + Promise.resolve({ + chainId: '0x5', + }), + ), disconnectGasFeeEstimatePoller: jest.fn(), getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(null), addPollingTokenToAppState: jest.fn(), removePollingTokenFromAppState: jest.fn(), })); -const renderComponent = ({ props = {}, state = {} } = {}) => { +const renderComponent = async ({ props = {}, state = {} } = {}) => { const store = configureStore(state); - return renderWithProvider(, store); + + let result; + + await act( + async () => + (result = renderWithProvider( + , + store, + )), + ); + + return result; }; describe('AdvancedGasFeeInputSubtext', () => { describe('when "latest" is non-nullish', () => { - it('should render the latest fee if given a fee', () => { - renderComponent({ + it('should render the latest fee if given a fee', async () => { + await renderComponent({ props: { latest: '123.12345', }, @@ -27,8 +48,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByText('123.12 GWEI')).toBeInTheDocument(); }); - it('should render the latest fee range if given a fee range', () => { - renderComponent({ + it('should render the latest fee range if given a fee range', async () => { + await renderComponent({ props: { latest: ['123.456', '456.789'], }, @@ -37,8 +58,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByText('123.46 - 456.79 GWEI')).toBeInTheDocument(); }); - it('should render a fee trend arrow image if given "up" as the trend', () => { - renderComponent({ + it('should render a fee trend arrow image if given "up" as the trend', async () => { + await renderComponent({ props: { latest: '123.12345', trend: 'up', @@ -48,8 +69,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByTitle('up arrow')).toBeInTheDocument(); }); - it('should render a fee trend arrow image if given "down" as the trend', () => { - renderComponent({ + it('should render a fee trend arrow image if given "down" as the trend', async () => { + await renderComponent({ props: { latest: '123.12345', trend: 'down', @@ -59,8 +80,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByTitle('down arrow')).toBeInTheDocument(); }); - it('should render a fee trend arrow image if given "level" as the trend', () => { - renderComponent({ + it('should render a fee trend arrow image if given "level" as the trend', async () => { + await renderComponent({ props: { latest: '123.12345', trend: 'level', @@ -70,11 +91,11 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByTitle('level arrow')).toBeInTheDocument(); }); - it('should not render a fee trend arrow image if given an invalid trend', () => { + it('should not render a fee trend arrow image if given an invalid trend', async () => { // Suppress warning from PropTypes, which we expect jest.spyOn(console, 'error').mockImplementation(); - renderComponent({ + await renderComponent({ props: { latest: '123.12345', trend: 'whatever', @@ -84,8 +105,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.queryByTestId('fee-arrow')).not.toBeInTheDocument(); }); - it('should not render a fee trend arrow image if given a nullish trend', () => { - renderComponent({ + it('should not render a fee trend arrow image if given a nullish trend', async () => { + await renderComponent({ props: { latest: '123.12345', trend: null, @@ -97,8 +118,8 @@ describe('AdvancedGasFeeInputSubtext', () => { }); describe('when "latest" is nullish', () => { - it('should not render the container for the latest fee', () => { - renderComponent({ + it('should not render the container for the latest fee', async () => { + await renderComponent({ props: { latest: null, }, @@ -109,8 +130,8 @@ describe('AdvancedGasFeeInputSubtext', () => { }); describe('when "historical" is not nullish', () => { - it('should render the historical fee if given a fee', () => { - renderComponent({ + it('should render the historical fee if given a fee', async () => { + await renderComponent({ props: { historical: '123.12345', }, @@ -119,8 +140,8 @@ describe('AdvancedGasFeeInputSubtext', () => { expect(screen.getByText('123.12 GWEI')).toBeInTheDocument(); }); - it('should render the historical fee range if given a fee range', () => { - renderComponent({ + it('should render the historical fee range if given a fee range', async () => { + await renderComponent({ props: { historical: ['123.456', '456.789'], }, @@ -131,8 +152,8 @@ describe('AdvancedGasFeeInputSubtext', () => { }); describe('when "historical" is nullish', () => { - it('should not render the container for the historical fee', () => { - renderComponent({ + it('should not render the container for the historical fee', async () => { + await renderComponent({ props: { historical: null, }, diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js index 8be1b829fe97..fa0653251750 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.js @@ -62,23 +62,24 @@ const BaseFeeInput = () => { } = useAdvancedGasFeePopoverContext(); const { estimatedBaseFee, historicalBaseFeeRange, baseFeeTrend } = - gasFeeEstimates; + gasFeeEstimates ?? {}; + const [baseFeeError, setBaseFeeError] = useState(); const { currency, numberOfDecimals } = useUserPreferencedCurrency(PRIMARY); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); - const [baseFee, setBaseFee] = useState(() => { - if ( - estimateUsed !== PriorityLevels.custom && - advancedGasFeeValues?.maxBaseFee && - editGasMode !== EditGasModes.swaps - ) { - return advancedGasFeeValues.maxBaseFee; - } - - return maxFeePerGas; - }); + const defaultBaseFee = + estimateUsed !== PriorityLevels.custom && + advancedGasFeeValues?.maxBaseFee && + editGasMode !== EditGasModes.swaps + ? advancedGasFeeValues.maxBaseFee + : maxFeePerGas; + + const [baseFee, setBaseFee] = useState(defaultBaseFee); + useEffect(() => { + setBaseFee(defaultBaseFee); + }, [defaultBaseFee, setBaseFee]); const [baseFeeInPrimaryCurrency] = useCurrencyDisplay( decGWEIToHexWEI(baseFee * gasLimit), diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js index 2e3139869bf6..c3413ddc0a0b 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/base-fee-input/base-fee-input.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { EditGasModes, @@ -18,15 +18,16 @@ import BaseFeeInput from './base-fee-input'; const LOW_BASE_FEE = 0.000000001; jest.mock('../../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), })); -const render = (txProps, contextProps) => { +const render = async (txProps, contextProps) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -36,40 +37,55 @@ const render = (txProps, contextProps) => { balance: '0x1F4', }, }, - advancedGasFee: { maxBaseFee: 100 }, + advancedGasFee: { '0x5': { maxBaseFee: 100 } }, featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + + + + , + store, + )), ); + + return result; }; describe('BaseFeeInput', () => { - it('should renders advancedGasFee.baseFee value if current estimate used is not custom', () => { - render({ + it('should renders advancedGasFee.baseFee value if current estimate used is not custom', async () => { + await render({ userFeeLevel: 'high', }); expect(document.getElementsByTagName('input')[0]).toHaveValue(100); }); - it('should not use advancedGasFee.baseFee value for swaps', () => { - render( + it('should not use advancedGasFee.baseFee value for swaps', async () => { + await render( { userFeeLevel: 'high', }, @@ -84,8 +100,8 @@ describe('BaseFeeInput', () => { ); }); - it('should renders baseFee values from transaction if current estimate used is custom', () => { - render({ + it('should renders baseFee values from transaction if current estimate used is custom', async () => { + await render({ txParams: { maxFeePerGas: '0x2E90EDD000', }, @@ -107,20 +123,23 @@ describe('BaseFeeInput', () => { }, ]; - it.each(testCases)('$description', ({ maxFeePerGas, expectedValue }) => { - render({ - txParams: { - maxFeePerGas, - }, - }); - expect(document.getElementsByTagName('input')[0]).toHaveValue( - expectedValue, - ); - }); + it.each(testCases)( + '$description', + async ({ maxFeePerGas, expectedValue }) => { + await render({ + txParams: { + maxFeePerGas, + }, + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue( + expectedValue, + ); + }, + ); }); - it('should show current value of estimatedBaseFee in users primary currency in right side of input box', () => { - render({ + it('should show current value of estimatedBaseFee in users primary currency in right side of input box', async () => { + await render({ txParams: { gas: '0x5208', maxFeePerGas: '0x2E90EDD000', @@ -129,18 +148,18 @@ describe('BaseFeeInput', () => { expect(screen.queryByText('≈ 0.0042 ETH')).toBeInTheDocument(); }); - it('should show current value of estimatedBaseFee in subtext', () => { - render(); + it('should show current value of estimatedBaseFee in subtext', async () => { + await render(); expect(screen.queryByText('50 GWEI')).toBeInTheDocument(); }); - it('should show 12hr range value in subtext', () => { - render(); + it('should show 12hr range value in subtext', async () => { + await render(); expect(screen.queryByText('50 - 100 GWEI')).toBeInTheDocument(); }); - it('should show error if base fee is less than suggested low value', () => { - render({ + it('should show error if base fee is less than suggested low value', async () => { + await render({ txParams: { maxFeePerGas: '0x174876E800', }, @@ -156,8 +175,8 @@ describe('BaseFeeInput', () => { }); }); - it('should show error if base if is more than suggested high value', () => { - render({ + it('should show error if base if is more than suggested high value', async () => { + await render({ txParams: { maxFeePerGas: '0x174876E800', }, @@ -177,8 +196,8 @@ describe('BaseFeeInput', () => { }); describe('updateBaseFee', () => { - it('updates base fee correctly', () => { - const { getByTestId } = render(); + it('updates base fee correctly', async () => { + const { getByTestId } = await render(); const input = getByTestId('base-fee-input'); fireEvent.change(input, { target: { value: '1' } }); @@ -186,8 +205,8 @@ describe('BaseFeeInput', () => { expect(input.value).toBe('1'); }); - it('handles low numbers', () => { - const { getByTestId } = render(); + it('handles low numbers', async () => { + const { getByTestId } = await render(); const input = getByTestId('base-fee-input'); fireEvent.change(input, { target: { value: LOW_BASE_FEE } }); diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js index c9e70faa219c..6079955196f3 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.js @@ -66,19 +66,20 @@ const PriorityFeeInput = () => { latestPriorityFeeRange, historicalPriorityFeeRange, priorityFeeTrend, - } = gasFeeEstimates; + } = gasFeeEstimates ?? {}; const [priorityFeeError, setPriorityFeeError] = useState(); - const [priorityFee, setPriorityFee] = useState(() => { - if ( - estimateUsed !== PriorityLevels.custom && - advancedGasFeeValues?.priorityFee && - editGasMode !== EditGasModes.swaps - ) { - return advancedGasFeeValues.priorityFee; - } - return maxPriorityFeePerGas; - }); + const defaultPriorityFee = + estimateUsed !== PriorityLevels.custom && + advancedGasFeeValues?.priorityFee && + editGasMode !== EditGasModes.swaps + ? advancedGasFeeValues.priorityFee + : maxPriorityFeePerGas; + + const [priorityFee, setPriorityFee] = useState(defaultPriorityFee); + useEffect(() => { + setPriorityFee(defaultPriorityFee); + }, [defaultPriorityFee, setPriorityFee]); const { currency, numberOfDecimals } = useUserPreferencedCurrency(PRIMARY); diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js index 3053cde03521..1378e89c84b3 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-inputs/priority-fee-input/priority-fee-input.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { EditGasModes, @@ -19,15 +19,16 @@ import PriorityfeeInput from './priority-fee-input'; const LOW_PRIORITY_FEE = 0.000000001; jest.mock('../../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), })); -const render = (txProps, contextProps) => { +const render = async (txProps, contextProps) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -41,36 +42,51 @@ const render = (txProps, contextProps) => { featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + + + + , + store, + )), ); + + return result; }; describe('PriorityfeeInput', () => { - it('should renders advancedGasFee.priorityfee value if current estimate used is not custom', () => { - render({ + it('should renders advancedGasFee.priorityfee value if current estimate used is not custom', async () => { + await render({ userFeeLevel: 'high', }); expect(document.getElementsByTagName('input')[0]).toHaveValue(100); }); - it('should not use advancedGasFee.priorityfee value for swaps', () => { - render( + it('should not use advancedGasFee.priorityfee value for swaps', async () => { + await render( { userFeeLevel: 'high', }, @@ -101,8 +117,8 @@ describe('PriorityfeeInput', () => { it.each(testCases)( '$description', - ({ maxPriorityFeePerGas, expectedValue }) => { - render({ + async ({ maxPriorityFeePerGas, expectedValue }) => { + await render({ txParams: { maxPriorityFeePerGas, }, @@ -114,13 +130,13 @@ describe('PriorityfeeInput', () => { ); }); - it('should show current priority fee range in subtext', () => { - render(); + it('should show current priority fee range in subtext', async () => { + await render(); expect(screen.queryByText('1 - 20 GWEI')).toBeInTheDocument(); }); - it('should show current value of priority fee in users primary currency in right side of input box', () => { - render({ + it('should show current value of priority fee in users primary currency in right side of input box', async () => { + await render({ txParams: { gas: '0x5208', maxPriorityFeePerGas: '0x77359400', @@ -129,13 +145,13 @@ describe('PriorityfeeInput', () => { expect(screen.queryByText('≈ 0.000042 ETH')).toBeInTheDocument(); }); - it('should show 12hr range value in subtext', () => { - render(); + it('should show 12hr range value in subtext', async () => { + await render(); expect(screen.queryByText('2 - 125 GWEI')).toBeInTheDocument(); }); - it('should not show error if value entered is 0', () => { - render({ + it('should not show error if value entered is 0', async () => { + await render({ txParams: { maxPriorityFeePerGas: '0x174876E800', }, @@ -151,8 +167,8 @@ describe('PriorityfeeInput', () => { ).not.toBeInTheDocument(); }); - it('should not show the error if priority fee is 0', () => { - render({ + it('should not show the error if priority fee is 0', async () => { + await render({ txParams: { maxPriorityFeePerGas: '0x0', }, @@ -163,8 +179,8 @@ describe('PriorityfeeInput', () => { }); describe('updatePriorityFee', () => { - it('updates base fee correctly', () => { - const { getByTestId } = render(); + it('updates base fee correctly', async () => { + const { getByTestId } = await render(); const input = getByTestId('priority-fee-input'); fireEvent.change(input, { target: { value: '1' } }); @@ -172,8 +188,8 @@ describe('PriorityfeeInput', () => { expect(input.value).toBe('1'); }); - it('handles low numbers', () => { - const { getByTestId } = render(); + it('handles low numbers', async () => { + const { getByTestId } = await render(); const input = getByTestId('priority-fee-input'); fireEvent.change(input, { target: { value: LOW_PRIORITY_FEE } }); diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js index 09ca4ba261a4..488b044513c3 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-popover.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { GasEstimateTypes } from '../../../../../shared/constants/gas'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; @@ -12,12 +12,13 @@ import configureStore from '../../../../store/store'; import AdvancedGasFeePopover from './advanced-gas-fee-popover'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), createTransactionEventFragment: jest.fn(), })); @@ -28,7 +29,7 @@ jest.mock('../../../../contexts/transaction-modal', () => ({ }), })); -const render = () => { +const render = async () => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -41,46 +42,61 @@ const render = () => { featureFlags: { advancedInlineGas: true }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('AdvancedGasFeePopover', () => { - it('should renders save button enabled by default', () => { - render(); + it('should renders save button enabled by default', async () => { + await render(); expect(screen.queryByRole('button', { name: 'Save' })).not.toBeDisabled(); }); - it('should enable save button if priority fee 0 is entered', () => { - render(); + it('should enable save button if priority fee 0 is entered', async () => { + await render(); fireEvent.change(document.getElementsByTagName('input')[1], { target: { value: 0 }, }); expect(screen.queryByRole('button', { name: 'Save' })).toBeEnabled(); }); - it('should disable save button if priority fee entered is greater than base fee', () => { - render(); + it('should disable save button if priority fee entered is greater than base fee', async () => { + await render(); fireEvent.change(document.getElementsByTagName('input')[1], { target: { value: 100000 }, }); expect(screen.queryByRole('button', { name: 'Save' })).toBeDisabled(); }); - it('should disable save button if gas limit beyond range is entered', () => { - render(); + it('should disable save button if gas limit beyond range is entered', async () => { + await render(); fireEvent.click(screen.queryByText('Edit')); fireEvent.change(document.getElementsByTagName('input')[3], { target: { value: 0 }, diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js index bb97bb33ab1a..4cb9c499c01b 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; @@ -13,15 +13,17 @@ import { GasFeeContextProvider } from '../../../../contexts/gasFee'; import ConfirmGasDisplay from './confirm-gas-display'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), })); -const render = ({ transactionProp = {}, contextProps = {} } = {}) => { +const render = async ({ transactionProp = {}, contextProps = {} } = {}) => { const store = configureStore({ ...mockState, ...contextProps, @@ -38,20 +40,35 @@ const render = ({ transactionProp = {}, contextProps = {} } = {}) => { }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('ConfirmGasDisplay', () => { it('should match snapshot', async () => { - const { container } = render({ + const { container } = await render({ transactionProp: { txParams: { gas: '0x5208', @@ -61,8 +78,8 @@ describe('ConfirmGasDisplay', () => { }); expect(container).toMatchSnapshot(); }); - it('should render gas display labels for EIP1559 transcations', () => { - render({ + it('should render gas display labels for EIP1559 transcations', async () => { + await render({ transactionProp: { txParams: { gas: '0x5208', @@ -76,13 +93,13 @@ describe('ConfirmGasDisplay', () => { expect(screen.queryByText('Max fee:')).toBeInTheDocument(); expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0); }); - it('should render gas display labels for legacy transcations', () => { - render({ + it('should render gas display labels for legacy transcations', async () => { + await render({ contextProps: { metamask: { - selectedNetworkClientId: NetworkType.mainnet, + selectedNetworkClientId: NetworkType.goerli, networksMetadata: { - [NetworkType.mainnet]: { + [NetworkType.goerli]: { EIPS: { 1559: false, }, diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-container.test.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-container.test.js index 4a515fad6b45..eb38c48a510c 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-container.test.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container-container.test.js @@ -163,6 +163,13 @@ jest.mock('../../../../store/actions', () => ({ getGasFeeEstimatesAndStartPolling: jest .fn() .mockImplementation(() => Promise.resolve()), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => { + return Promise.resolve({ chainId: '0x5' }); + }), + gasFeeStartPollingByNetworkClientId: jest.fn().mockImplementation(() => { + return Promise.resolve('pollingToken'); + }), + gasFeeStopPollingByPollingToken: jest.fn(), addPollingTokenToAppState: jest.fn(), })); @@ -197,9 +204,11 @@ describe('Confirm Page Container Container Test', () => { }); describe('Render and simulate button clicks', () => { - beforeEach(() => { + beforeEach(async () => { const store = configureMockStore()(mockedState); - renderWithProvider(, store); + await act(async () => { + renderWithProvider(, store); + }); }); it('should render a confirm page container component', () => { @@ -244,7 +253,7 @@ describe('Confirm Page Container Container Test', () => { }); describe(`when type is '${TransactionType.tokenMethodSetApprovalForAll}'`, () => { - it('should display warning modal with total token balance', () => { + it('should display warning modal with total token balance', async () => { const mockValue12AsHexString = '0x0c'; // base-10 representation = 12 TokenUtil.fetchTokenBalance.mockImplementation(() => { @@ -253,16 +262,18 @@ describe('Confirm Page Container Container Test', () => { setMockedTransactionType(TransactionType.tokenMethodSetApprovalForAll); const store = configureMockStore()(mockedState); - renderWithProvider( - , - store, - ); + await act(async () => { + renderWithProvider( + , + store, + ); + }); - act(() => { + await act(async () => { const confirmButton = screen.getByTestId('page-container-footer-next'); fireEvent.click(confirmButton); }); @@ -281,25 +292,36 @@ describe('Confirm Page Container Container Test', () => { describe('Rendering NetworkAccountBalanceHeader', () => { const store = configureMockStore()(mockState); - it('should render NetworkAccountBalanceHeader if displayAccountBalanceHeader is true', () => { - const { getByText } = renderWithProvider( - , - store, - ); + it('should render NetworkAccountBalanceHeader if displayAccountBalanceHeader is true', async () => { + let result; + await act(async () => { + result = renderWithProvider( + , + store, + ); + }); + const { getByText } = result; expect(getByText('Balance')).toBeInTheDocument(); }); - it('should not render NetworkAccountBalanceHeader if displayAccountBalanceHeader is false', () => { - const { queryByText } = renderWithProvider( - , - store, - ); - expect(queryByText('Balance')).toBeNull(); + it('should not render NetworkAccountBalanceHeader if displayAccountBalanceHeader is false', async () => { + let result; + await act(async () => { + result = renderWithProvider( + , + store, + ); + }); + const { queryByText } = result; + expect(queryByText('Balance')).toBe(null); }); }); describe('Contact/AddressBook name should appear in recipient header', () => { - it('should not show add to address dialog if recipient is in contact list and should display contact name', () => { + it('should not show add to address dialog if recipient is in contact list and should display contact name', async () => { const addressBookName = 'test save name'; const addressBook = { @@ -320,7 +342,9 @@ describe('Confirm Page Container Container Test', () => { const store = configureMockStore()(mockState); - renderWithProvider(, store); + await act(async () => { + renderWithProvider(, store); + }); // Does not display new address dialog banner const newAccountDetectDialog = screen.queryByText( diff --git a/ui/pages/confirmations/components/edit-gas-fee-button/edit-gas-fee-button.test.js b/ui/pages/confirmations/components/edit-gas-fee-button/edit-gas-fee-button.test.js index d0c351809e64..4ad0a3d6c0a2 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-button/edit-gas-fee-button.test.js +++ b/ui/pages/confirmations/components/edit-gas-fee-button/edit-gas-fee-button.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { @@ -17,15 +17,17 @@ import configureStore from '../../../../store/store'; import EditGasFeeButton from './edit-gas-fee-button'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), createTransactionEventFragment: jest.fn(), })); -const render = ({ componentProps, contextProps } = {}) => { +const render = async ({ componentProps, contextProps } = {}) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -37,45 +39,60 @@ const render = ({ componentProps, contextProps } = {}) => { }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('EditGasFeeButton', () => { - it('should render edit link with text low if low gas estimates are selected', () => { - render({ contextProps: { transaction: { userFeeLevel: 'low' } } }); + it('should render edit link with text low if low gas estimates are selected', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'low' } } }); expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('Low')).toBeInTheDocument(); }); - it('should render edit link with text market if medium gas estimates are selected', () => { - render({ contextProps: { transaction: { userFeeLevel: 'medium' } } }); + it('should render edit link with text market if medium gas estimates are selected', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'medium' } } }); expect(screen.queryByText('🦊')).toBeInTheDocument(); expect(screen.queryByText('Market')).toBeInTheDocument(); }); - it('should render edit link with text aggressive if high gas estimates are selected', () => { - render({ contextProps: { transaction: { userFeeLevel: 'high' } } }); + it('should render edit link with text aggressive if high gas estimates are selected', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'high' } } }); expect(screen.queryByText('🦍')).toBeInTheDocument(); expect(screen.queryByText('Aggressive')).toBeInTheDocument(); }); - it('should render edit link with text 10% increase if tenPercentIncreased gas estimates are selected', () => { - render({ + it('should render edit link with text 10% increase if tenPercentIncreased gas estimates are selected', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'tenPercentIncreased' } }, }); expect(screen.queryByText('10% increase')).toBeInTheDocument(); }); - it('should render edit link with text Site suggested if site suggested estimated are used', () => { - render({ + it('should render edit link with text Site suggested if site suggested estimated are used', async () => { + await render({ contextProps: { transaction: { userFeeLevel: PriorityLevels.dAppSuggested, @@ -89,8 +106,8 @@ describe('EditGasFeeButton', () => { expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1); }); - it('should render edit link with text swap suggested if high gas estimates are selected for swaps', () => { - render({ + it('should render edit link with text swap suggested if high gas estimates are selected for swaps', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'high' }, editGasMode: EditGasModes.swaps, @@ -100,8 +117,8 @@ describe('EditGasFeeButton', () => { expect(screen.queryByText('Swap suggested')).toBeInTheDocument(); }); - it('should render edit link with text advance if custom gas estimates are used', () => { - render({ + it('should render edit link with text advance if custom gas estimates are used', async () => { + await render({ contextProps: { defaultEstimateToUse: 'custom', transaction: {}, @@ -112,8 +129,8 @@ describe('EditGasFeeButton', () => { expect(screen.queryByText('Edit')).toBeInTheDocument(); }); - it('should not render edit link if transaction has simulation error and prop userAcknowledgedGasMissing is false', () => { - render({ + it('should not render edit link if transaction has simulation error and prop userAcknowledgedGasMissing is false', async () => { + await render({ contextProps: { transaction: { simulationFails: true, @@ -126,8 +143,8 @@ describe('EditGasFeeButton', () => { expect(screen.queryByText('Low')).not.toBeInTheDocument(); }); - it('should render edit link if userAcknowledgedGasMissing is true even if transaction has simulation error', () => { - render({ + it('should render edit link if userAcknowledgedGasMissing is true even if transaction has simulation error', async () => { + await render({ contextProps: { transaction: { simulationFails: true, @@ -140,8 +157,8 @@ describe('EditGasFeeButton', () => { expect(screen.queryByText('Low')).toBeInTheDocument(); }); - it('should render null for legacy transactions', () => { - const { container } = render({ + it('should render null for legacy transactions', async () => { + const { container } = await render({ contextProps: { transaction: { userFeeLevel: 'low', diff --git a/ui/pages/confirmations/components/edit-gas-fee-icon/edit-gas-fee-icon.test.js b/ui/pages/confirmations/components/edit-gas-fee-icon/edit-gas-fee-icon.test.js index 4f86824e09af..1781314edbc6 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-icon/edit-gas-fee-icon.test.js +++ b/ui/pages/confirmations/components/edit-gas-fee-icon/edit-gas-fee-icon.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import { renderWithProvider } from '../../../../../test/jest'; import { GasFeeContextProvider } from '../../../../contexts/gasFee'; import configureStore from '../../../../store/store'; @@ -7,11 +7,13 @@ import mockState from '../../../../../test/data/mock-state.json'; import EditGasFeeIcon from './edit-gas-fee-icon'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), createTransactionEventFragment: jest.fn(), })); @@ -24,27 +26,36 @@ jest.mock('../../../../contexts/transaction-modal', () => ({ }), })); -const render = () => { +const render = async () => { const store = configureStore({ metamask: { ...mockState.metamask, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('EditGasFeeIcon', () => { - it('should render edit icon', () => { - render(); + it('should render edit icon', async () => { + await render(); const iconButton = screen.getByTestId('edit-gas-fee-icon'); expect(iconButton).toBeInTheDocument(); - fireEvent.click(iconButton); + await act(async () => { + fireEvent.click(iconButton); + }); expect(mockOpenModalFn).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-fee-popover.test.js b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-fee-popover.test.js index f8d6f06af6ef..51a996678119 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-fee-popover.test.js +++ b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-fee-popover.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { @@ -19,11 +19,15 @@ import { import EditGasFeePopover from './edit-gas-fee-popover'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => + Promise.resolve({ + chainId: '0x5', + }), + ), createTransactionEventFragment: jest.fn(), })); @@ -58,7 +62,7 @@ const MOCK_FEE_ESTIMATE = { networkCongestion: 0.7, }; -const render = ({ txProps, contextProps } = {}) => { +const render = async ({ txProps, contextProps } = {}) => { const store = configureStore({ metamask: { currencyRates: {}, @@ -67,6 +71,15 @@ const render = ({ txProps, contextProps } = {}) => { nickname: GOERLI_DISPLAY_NAME, type: NETWORK_TYPES.GOERLI, }, + selectedNetworkClientId: 'goerli', + networksMetadata: { + goerli: { + EIPS: { + 1559: true, + }, + status: 'available', + }, + }, accountsByChainId: { [CHAIN_IDS.GOERLI]: { '0xAddress': { address: '0xAddress', balance: '0x1F4' }, @@ -102,24 +115,38 @@ const render = ({ txProps, contextProps } = {}) => { selectedAddress: '0xAddress', featureFlags: { advancedInlineGas: true }, gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasFeeEstimatesByChainId: { + [CHAIN_IDS.GOERLI]: { + gasFeeEstimates: MOCK_FEE_ESTIMATE, + }, + }, advancedGasFee: {}, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('EditGasFeePopover', () => { - it('should renders low / medium / high options', () => { - render({ txProps: { dappSuggestedGasFees: { maxFeePerGas: '0x5208' } } }); + it('should renders low / medium / high options', async () => { + await render({ + txProps: { dappSuggestedGasFees: { maxFeePerGas: '0x5208' } }, + }); expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('🦊')).toBeInTheDocument(); @@ -133,21 +160,21 @@ describe('EditGasFeePopover', () => { expect(screen.queryByText('Advanced')).toBeInTheDocument(); }); - it('should show time estimates', () => { - render(); + it('should show time estimates', async () => { + await render(); expect(screen.queryAllByText('5 min')).toHaveLength(2); expect(screen.queryByText('15 sec')).toBeInTheDocument(); }); - it('should show gas fee estimates', () => { - render(); + it('should show gas fee estimates', async () => { + await render(); expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); }); - it('should not show insufficient balance message if transaction value is less than balance', () => { - render({ + it('should not show insufficient balance message if transaction value is less than balance', async () => { + await render({ txProps: { status: TransactionStatus.unapproved, type: TransactionType.simpleSend, @@ -158,8 +185,8 @@ describe('EditGasFeePopover', () => { expect(screen.queryByText('Insufficient funds.')).not.toBeInTheDocument(); }); - it('should show insufficient balance message if transaction value is more than balance', () => { - render({ + it('should show insufficient balance message if transaction value is more than balance', async () => { + await render({ txProps: { status: TransactionStatus.unapproved, type: TransactionType.simpleSend, @@ -170,8 +197,8 @@ describe('EditGasFeePopover', () => { expect(screen.queryByText('Insufficient funds.')).toBeInTheDocument(); }); - it('should not show low, aggressive and dapp-suggested options for swap', () => { - render({ + it('should not show low, aggressive and dapp-suggested options for swap', async () => { + await render({ contextProps: { editGasMode: EditGasModes.swaps }, }); expect(screen.queryByText('🐢')).not.toBeInTheDocument(); @@ -188,52 +215,52 @@ describe('EditGasFeePopover', () => { expect(screen.queryByText('Advanced')).toBeInTheDocument(); }); - it('should not show time estimates for swaps', () => { - render({ + it('should not show time estimates for swaps', async () => { + await render({ contextProps: { editGasMode: EditGasModes.swaps }, }); expect(screen.queryByText('Time')).not.toBeInTheDocument(); expect(screen.queryByText('Max fee')).toBeInTheDocument(); }); - it('should show correct header for edit gas mode', () => { - render({ + it('should show correct header for edit gas mode', async () => { + await render({ contextProps: { editGasMode: EditGasModes.swaps }, }); expect(screen.queryByText('Edit gas fee')).toBeInTheDocument(); - render({ + await render({ contextProps: { editGasMode: EditGasModes.cancel }, }); expect(screen.queryByText('Edit cancellation gas fee')).toBeInTheDocument(); - render({ + await render({ contextProps: { editGasMode: EditGasModes.speedUp }, }); expect(screen.queryByText('Edit speed up gas fee')).toBeInTheDocument(); }); - it('should not show low option for cancel mode', () => { - render({ + it('should not show low option for cancel mode', async () => { + await render({ contextProps: { editGasMode: EditGasModes.cancel }, }); expect(screen.queryByText('Low')).not.toBeInTheDocument(); }); - it('should not show low option for speedup mode', () => { - render({ + it('should not show low option for speedup mode', async () => { + await render({ contextProps: { editGasMode: EditGasModes.speedUp }, }); expect(screen.queryByText('Low')).not.toBeInTheDocument(); }); - it('should show tenPercentIncreased option for cancel gas mode', () => { - render({ + it('should show tenPercentIncreased option for cancel gas mode', async () => { + await render({ contextProps: { editGasMode: EditGasModes.cancel }, }); expect(screen.queryByText('10% increase')).toBeInTheDocument(); }); - it('should show tenPercentIncreased option for speedup gas mode', () => { - render({ + it('should show tenPercentIncreased option for speedup gas mode', async () => { + await render({ contextProps: { editGasMode: EditGasModes.speedUp }, }); expect(screen.queryByText('10% increase')).toBeInTheDocument(); diff --git a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js index b83f195a569c..6f793514cd1c 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js +++ b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { EditGasModes, @@ -17,11 +17,13 @@ import { import EditGasItem from './edit-gas-item'; jest.mock('../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), getGasFeeTimeEstimate: jest .fn() .mockImplementation(() => Promise.resolve('unknown')), @@ -55,7 +57,7 @@ const ESTIMATE_MOCK = { maxPriorityFeePerGas: '0x59682f00', }; -const renderComponent = ({ +const render = async ({ componentProps, transactionProps, contextProps, @@ -68,6 +70,15 @@ const renderComponent = ({ nickname: GOERLI_DISPLAY_NAME, type: NETWORK_TYPES.GOERLI, }, + selectedNetworkClientId: 'goerli', + networkConfigurations: { + goerli: { + type: 'rpc', + chainId: '0x5', + ticker: 'ETH', + id: 'goerli', + }, + }, accountsByChainId: { [CHAIN_IDS.GOERLI]: { '0xAddress': { @@ -114,6 +125,12 @@ const renderComponent = ({ featureFlags: { advancedInlineGas: true }, gasEstimateType: 'fee-market', gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasFeeEstimatesByChainId: { + [CHAIN_IDS.GOERLI]: { + gasFeeEstimates: MOCK_FEE_ESTIMATE, + gasEstimateType: 'fee-market', + }, + }, advancedGasFee: { [CHAIN_IDS.GOERLI]: { maxBaseFee: '100', @@ -123,20 +140,27 @@ const renderComponent = ({ }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('EditGasItem', () => { - it('should renders low gas estimate option for priorityLevel low', () => { - renderComponent({ componentProps: { priorityLevel: PriorityLevels.low } }); + it('should renders low gas estimate option for priorityLevel low', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.low } }); expect(screen.queryByRole('button', { name: 'low' })).toBeInTheDocument(); expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('Low')).toBeInTheDocument(); @@ -144,8 +168,8 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); }); - it('should renders market gas estimate option for priorityLevel medium', () => { - renderComponent({ + it('should renders market gas estimate option for priorityLevel medium', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.medium }, }); expect( @@ -157,8 +181,8 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); }); - it('should renders aggressive gas estimate option for priorityLevel high', () => { - renderComponent({ componentProps: { priorityLevel: PriorityLevels.high } }); + it('should renders aggressive gas estimate option for priorityLevel high', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.high } }); expect(screen.queryByRole('button', { name: 'high' })).toBeInTheDocument(); expect(screen.queryByText('🦍')).toBeInTheDocument(); expect(screen.queryByText('Aggressive')).toBeInTheDocument(); @@ -166,8 +190,8 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); }); - it('should render priorityLevel high as "Swap suggested" for swaps', () => { - renderComponent({ + it('should render priorityLevel high as "Swap suggested" for swaps', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.high }, contextProps: { editGasMode: EditGasModes.swaps }, }); @@ -178,8 +202,8 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); }); - it('should highlight option is priorityLevel is currently selected', () => { - renderComponent({ + it('should highlight option is priorityLevel is currently selected', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.high }, transactionProps: { userFeeLevel: 'high' }, }); @@ -188,8 +212,8 @@ describe('EditGasItem', () => { ).toHaveLength(1); }); - it('should renders site gas estimate option for priorityLevel dappSuggested', () => { - renderComponent({ + it('should renders site gas estimate option for priorityLevel dappSuggested', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.dAppSuggested }, transactionProps: { dappSuggestedGasFees: ESTIMATE_MOCK }, }); @@ -201,15 +225,15 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument(); }); - it('should not renders site gas estimate option for priorityLevel dappSuggested if site does not provided gas estimates', () => { - renderComponent({ + it('should not renders site gas estimate option for priorityLevel dappSuggested if site does not provided gas estimates', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.dAppSuggested }, transactionProps: {}, }); expect( screen.queryByRole('button', { name: 'dappSuggested' }), ).not.toBeInTheDocument(); - renderComponent({ + await render({ componentProps: { priorityLevel: PriorityLevels.dAppSuggested }, transactionProps: { dappSuggestedGasFees: { gas: '0x59682f10' } }, }); @@ -218,8 +242,8 @@ describe('EditGasItem', () => { ).not.toBeInTheDocument(); }); - it('should renders advance gas estimate option for priorityLevel custom', () => { - renderComponent({ + it('should renders advance gas estimate option for priorityLevel custom', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.custom }, transactionProps: { userFeeLevel: 'high' }, }); @@ -232,8 +256,8 @@ describe('EditGasItem', () => { expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); }); - it('should renders +10% gas estimate option for priorityLevel minimum', () => { - renderComponent({ + it('should renders +10% gas estimate option for priorityLevel minimum', async () => { + await render({ componentProps: { priorityLevel: PriorityLevels.tenPercentIncreased }, transactionProps: { userFeeLevel: 'tenPercentIncreased', diff --git a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/useGasItemFeeDetails.js b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/useGasItemFeeDetails.js index bfe35be4ad04..3e78d5ac05fc 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/useGasItemFeeDetails.js +++ b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-item/useGasItemFeeDetails.js @@ -79,7 +79,7 @@ export const useGasItemFeeDetails = (priorityLevel) => { maxPriorityFeePerGas, }); - if (gasFeeEstimates[priorityLevel]) { + if (gasFeeEstimates?.[priorityLevel]) { minWaitTime = priorityLevel === PriorityLevels.high ? gasFeeEstimates?.high.minWaitTimeEstimate diff --git a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js index 11a67066aea8..8e365ff9012f 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js +++ b/ui/pages/confirmations/components/edit-gas-fee-popover/edit-gas-tooltip/edit-gas-tooltip.test.js @@ -1,15 +1,18 @@ import React from 'react'; +import { act } from '@testing-library/react'; import configureStore from '../../../../../store/store'; import { renderWithProvider } from '../../../../../../test/jest'; import { GasFeeContextProvider } from '../../../../../contexts/gasFee'; import EditGasToolTip from './edit-gas-tooltip'; jest.mock('../../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), getGasFeeTimeEstimate: jest .fn() .mockImplementation(() => Promise.resolve('unknown')), @@ -30,7 +33,7 @@ const HIGH_GAS_OPTION = { maxPriorityFeePerGas: '2', }; -const renderComponent = (componentProps) => { +const render = async (componentProps) => { const mockStore = { metamask: { providerConfig: {}, @@ -81,17 +84,24 @@ const renderComponent = (componentProps) => { const store = configureStore(mockStore); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('EditGasToolTip', () => { - it('should render correct values for priorityLevel low', () => { - const { queryByText } = renderComponent({ + it('should render correct values for priorityLevel low', async () => { + const { queryByText } = await render({ priorityLevel: 'low', ...LOW_GAS_OPTION, }); @@ -101,8 +111,8 @@ describe('EditGasToolTip', () => { expect(queryByText('21000')).toBeInTheDocument(); }); - it('should render correct values for priorityLevel medium', () => { - const { queryByText } = renderComponent({ + it('should render correct values for priorityLevel medium', async () => { + const { queryByText } = await render({ priorityLevel: 'medium', ...MEDIUM_GAS_OPTION, }); @@ -111,8 +121,8 @@ describe('EditGasToolTip', () => { expect(queryByText('21000')).toBeInTheDocument(); }); - it('should render correct values for priorityLevel high', () => { - const { queryByText } = renderComponent({ + it('should render correct values for priorityLevel high', async () => { + const { queryByText } = await render({ priorityLevel: 'high', ...HIGH_GAS_OPTION, }); diff --git a/ui/pages/confirmations/components/fee-details-component/fee-details-component.test.js b/ui/pages/confirmations/components/fee-details-component/fee-details-component.test.js index f8199f0d90ee..ee3fd6b7a2fa 100644 --- a/ui/pages/confirmations/components/fee-details-component/fee-details-component.test.js +++ b/ui/pages/confirmations/components/fee-details-component/fee-details-component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import mockState from '../../../../../test/data/mock-state.json'; @@ -7,32 +7,44 @@ import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import FeeDetailsComponent from './fee-details-component'; jest.mock('../../../../store/actions', () => ({ - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), })); -const render = (state = {}) => { +const render = async (state = {}) => { const store = configureStore()({ ...mockState, ...state }); - return renderWithProvider(, store); + + let result; + + await act( + async () => (result = renderWithProvider(, store)), + ); + + return result; }; describe('FeeDetailsComponent', () => { - it('renders "Fee details"', () => { - render(); + it('renders "Fee details"', async () => { + await render(); expect(screen.queryByText('Fee details')).toBeInTheDocument(); }); - it('should expand when button is clicked', () => { - render(); + it('should expand when button is clicked', async () => { + await render(); expect(screen.queryByTitle('0 ETH')).not.toBeInTheDocument(); - screen.getByRole('button').click(); + await act(async () => { + screen.getByRole('button').click(); + }); expect(screen.queryByTitle('0 ETH')).toBeInTheDocument(); }); - it('should be displayed for even legacy network', () => { - render({ + it('should be displayed for even legacy network', async () => { + await render({ ...mockState, metamask: { ...mockState.metamask, @@ -41,6 +53,14 @@ describe('FeeDetailsComponent', () => { 1559: false, }, }, + networksMetadata: { + goerli: { + EIPS: { + 1559: false, + }, + status: 'available', + }, + }, }, }); expect(screen.queryByText('Fee details')).toBeInTheDocument(); diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js index 02071860835e..60f0cba4d7b5 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen, waitFor } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; import { GasEstimateTypes } from '../../../../../shared/constants/gas'; import mockEstimates from '../../../../../test/data/mock-estimates.json'; @@ -11,15 +11,17 @@ import configureStore from '../../../../store/store'; import GasDetailsItem from './gas-details-item'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), })); -const render = ({ contextProps } = {}) => { +const render = async ({ contextProps } = {}) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -34,29 +36,44 @@ const render = ({ contextProps } = {}) => { }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, ...contextProps, }, }); - return renderWithProvider( - - - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + + , + store, + )), ); + + return result; }; describe('GasDetailsItem', () => { it('should render label', async () => { - render(); + await render(); await waitFor(() => { expect(screen.queryAllByText('Market')[0]).toBeInTheDocument(); expect(screen.queryByText('Max fee:')).toBeInTheDocument(); @@ -65,7 +82,7 @@ describe('GasDetailsItem', () => { }); it('should show warning icon if estimates are high', async () => { - render({ + await render({ contextProps: { transaction: { txParams: {}, userFeeLevel: 'high' } }, }); await waitFor(() => { @@ -74,13 +91,23 @@ describe('GasDetailsItem', () => { }); it('should show warning icon if dapp estimates are high', async () => { - render({ + await render({ contextProps: { gasFeeEstimates: { high: { suggestedMaxPriorityFeePerGas: '1', }, }, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + gasFeeEstimates: { + high: { + suggestedMaxPriorityFeePerGas: '1', + }, + }, + }, + }, transaction: { txParams: { gas: '0x52081', @@ -100,7 +127,7 @@ describe('GasDetailsItem', () => { }); it('should not show warning icon if estimates are not high', async () => { - render({ + await render({ contextProps: { transaction: { txParams: {}, userFeeLevel: 'low' } }, }); await waitFor(() => { @@ -108,8 +135,8 @@ describe('GasDetailsItem', () => { }); }); - it('should return null if there is simulationError and user has not acknowledged gasMissing warning', () => { - const { container } = render({ + it('should return null if there is simulationError and user has not acknowledged gasMissing warning', async () => { + const { container } = await render({ contextProps: { transaction: { txParams: {}, @@ -122,7 +149,7 @@ describe('GasDetailsItem', () => { }); it('should not return null even if there is simulationError if user acknowledged gasMissing warning', async () => { - render(); + await render(); await waitFor(() => { expect(screen.queryAllByText('Market')[0]).toBeInTheDocument(); expect(screen.queryByText('Max fee:')).toBeInTheDocument(); @@ -131,7 +158,7 @@ describe('GasDetailsItem', () => { }); it('should render gas fee details', async () => { - render(); + await render(); await waitFor(() => { expect(screen.queryAllByTitle('0.00147 ETH').length).toBeGreaterThan(0); expect(screen.queryAllByText('ETH').length).toBeGreaterThan(0); @@ -139,7 +166,7 @@ describe('GasDetailsItem', () => { }); it('should render gas fee details if maxPriorityFeePerGas is 0', async () => { - render({ + await render({ contextProps: { transaction: { txParams: { @@ -159,7 +186,7 @@ describe('GasDetailsItem', () => { }); it('should render gas fee details if maxPriorityFeePerGas is undefined', async () => { - render({ + await render({ contextProps: { transaction: { txParams: { diff --git a/ui/pages/confirmations/components/gas-timing/gas-timing.component.js b/ui/pages/confirmations/components/gas-timing/gas-timing.component.js index 0b63f76a74a2..50a29591c391 100644 --- a/ui/pages/confirmations/components/gas-timing/gas-timing.component.js +++ b/ui/pages/confirmations/components/gas-timing/gas-timing.component.js @@ -65,6 +65,7 @@ export default function GasTiming({ const previousIsUnknownLow = usePrevious(isUnknownLow); useEffect(() => { + let isMounted = true; const priority = maxPriorityFeePerGas; const fee = maxFeePerGas; @@ -78,7 +79,11 @@ export default function GasTiming({ new BigNumber(priority, 10).toString(10), new BigNumber(fee, 10).toString(10), ).then((result) => { - if (maxFeePerGas === fee && maxPriorityFeePerGas === priority) { + if ( + maxFeePerGas === fee && + maxPriorityFeePerGas === priority && + isMounted + ) { setCustomEstimatedTime(result); } }); @@ -87,6 +92,10 @@ export default function GasTiming({ if (isUnknownLow !== false && previousIsUnknownLow === true) { setCustomEstimatedTime(null); } + + return () => { + isMounted = false; + }; }, [ maxPriorityFeePerGas, maxFeePerGas, diff --git a/ui/pages/confirmations/components/transaction-detail/transaction-detail.component.test.js b/ui/pages/confirmations/components/transaction-detail/transaction-detail.component.test.js index 1e1fa3804429..5a5d7431c2d0 100644 --- a/ui/pages/confirmations/components/transaction-detail/transaction-detail.component.test.js +++ b/ui/pages/confirmations/components/transaction-detail/transaction-detail.component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { GasEstimateTypes } from '../../../../../shared/constants/gas'; @@ -13,15 +13,17 @@ import configureStore from '../../../../store/store'; import TransactionDetail from './transaction-detail.component'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), createTransactionEventFragment: jest.fn(), })); -const render = ({ componentProps, contextProps } = {}) => { +const render = async ({ componentProps, contextProps } = {}) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -33,33 +35,48 @@ const render = ({ componentProps, contextProps } = {}) => { }, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + gasFeeEstimatesByChainId: { + ...mockState.metamask.gasFeeEstimatesByChainId, + '0x5': { + ...mockState.metamask.gasFeeEstimatesByChainId['0x5'], + gasFeeEstimates: + mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, + }, + }, }, }); - return renderWithProvider( - - { - console.log('on edit'); - }} - rows={[]} - userAcknowledgedGasMissing - {...componentProps} - /> - , - store, + let result; + + await act( + async () => + (result = renderWithProvider( + + { + console.log('on edit'); + }} + rows={[]} + userAcknowledgedGasMissing + {...componentProps} + /> + , + store, + )), ); + + return result; }; describe('TransactionDetail', () => { - it('should render edit link with text low if low gas estimates are selected', () => { - render({ contextProps: { transaction: { userFeeLevel: 'low' } } }); + it('should render edit link with text low if low gas estimates are selected', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'low' } } }); expect(screen.queryByText('🐢')).toBeInTheDocument(); expect(screen.queryByText('Low')).toBeInTheDocument(); }); - it('should render edit link with text edit for legacy transactions', () => { - render({ + it('should render edit link with text edit for legacy transactions', async () => { + await render({ contextProps: { transaction: { userFeeLevel: 'low', diff --git a/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap b/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap index 5a93d6e98cf0..dcbd37a4676f 100644 --- a/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap +++ b/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap @@ -316,23 +316,6 @@ exports[`ConfirmSendEther should render correct information for for confirm send
-
- -
-

- Network is busy. Gas prices are high and estimates are less accurate. -

-
-
@@ -398,12 +381,12 @@ exports[`ConfirmSendEther should render correct information for for confirm send
- 0.00021 + 0.000021
@@ -416,12 +399,12 @@ exports[`ConfirmSendEther should render correct information for for confirm send >
- 0.00021 + 0.000021 + Promise.resolve({ + chainId: '0x5', + }), + ), getGasFeeTimeEstimate: jest.fn(), getGasFeeEstimatesAndStartPolling: jest.fn(), promisifiedBackground: jest.fn(), diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index 55044842b445..89d44104ae06 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -32,6 +32,15 @@ import ConfirmTransactionBase from './confirm-transaction-base.container'; const middleware = [thunk]; setBackgroundConnection({ + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => + Promise.resolve({ + chainId: '0x5', + }), + ), getGasFeeTimeEstimate: jest.fn(), getGasFeeEstimatesAndStartPolling: jest.fn(), promisifiedBackground: jest.fn(), @@ -200,43 +209,87 @@ const baseStore = { }, }; -const mockedStore = jest.mocked(baseStore); - -const mockedStoreWithConfirmTxParams = (_mockTxParams = mockTxParams) => { - mockedStore.metamask.transactions[0].txParams = { ..._mockTxParams }; - mockedStore.confirmTransaction.txData.txParams = { ..._mockTxParams }; +const mockedStoreWithConfirmTxParams = ( + store, + _mockTxParams = mockTxParams, +) => { + const [firstTx, ...restTxs] = store.metamask.transactions; + + return { + ...store, + metamask: { + ...store.metamask, + transactions: [ + { + ...firstTx, + txParams: { + ..._mockTxParams, + }, + }, + ...restTxs, + ], + }, + confirmTransaction: { + ...store.confirmTransaction, + txData: { + ...store.confirmTransaction.txData, + txParams: { + ..._mockTxParams, + }, + }, + }, + }; }; const sendToRecipientSelector = '.sender-to-recipient__party--recipient .sender-to-recipient__name'; +const render = async ({ props, state } = {}) => { + const store = configureMockStore(middleware)({ + ...baseStore, + ...state, + }); + + const componentProps = { + actionKey: 'confirm', + ...props, + }; + + let result; + + await act( + async () => + (result = renderWithProvider( + , + store, + )), + ); + + return result; +}; + describe('Confirm Transaction Base', () => { - it('should match snapshot', () => { - const store = configureMockStore(middleware)(baseStore); - const { container } = renderWithProvider( - , - store, - ); + it('should match snapshot', async () => { + const { container } = await render(); expect(container).toMatchSnapshot(); }); - it('should not contain L1 L2 fee details for chains that are not optimism', () => { - const store = configureMockStore(middleware)(baseStore); - const { queryByText } = renderWithProvider( - , - store, - ); + it('should not contain L1 L2 fee details for chains that are not optimism', async () => { + const { queryByText } = await render(); + expect(queryByText('Layer 1 fees')).not.toBeInTheDocument(); expect(queryByText('Layer 2 gas fee')).not.toBeInTheDocument(); }); - it('should render only total fee details if simulation fails', () => { - mockedStore.send.hasSimulationError = true; - const store = configureMockStore(middleware)(mockedStore); - const { queryByText } = renderWithProvider( - , - store, - ); + it('should render only total fee details if simulation fails', async () => { + const state = { + send: { + ...baseStore.send, + hasSimulationError: true, + }, + }; + + const { queryByText } = await render({ state }); expect(queryByText('Total')).toBeInTheDocument(); expect(queryByText('Amount + gas fee')).toBeInTheDocument(); @@ -244,19 +297,18 @@ describe('Confirm Transaction Base', () => { expect(queryByText('Estimated fee')).not.toBeInTheDocument(); }); - it('renders blockaid security alert if recipient is a malicious address', () => { - const newMockedStore = { - ...mockedStore, + it('renders blockaid security alert if recipient is a malicious address', async () => { + const state = { send: { - ...mockedStore.send, + ...baseStore.send, hasSimulationError: false, }, confirmTransaction: { - ...mockedStore.confirmTransaction, + ...baseStore.confirmTransaction, txData: { - ...mockedStore.confirmTransaction.txData, + ...baseStore.confirmTransaction.txData, txParams: { - ...mockedStore.confirmTransaction.txData.txParams, + ...baseStore.confirmTransaction.txData.txParams, to: mockMaliciousToAddress, }, securityAlertResponse: { @@ -268,12 +320,7 @@ describe('Confirm Transaction Base', () => { }, }; - const store = configureMockStore(middleware)(newMockedStore); - - const { getByTestId } = renderWithProvider( - , - store, - ); + const { getByTestId } = await render({ state }); const securityProviderBanner = getByTestId( 'security-provider-banner-alert', @@ -281,63 +328,74 @@ describe('Confirm Transaction Base', () => { expect(securityProviderBanner).toBeInTheDocument(); }); - it('should contain L1 L2 fee details for optimism', () => { - mockedStore.metamask.providerConfig.chainId = CHAIN_IDS.OPTIMISM; - mockedStore.confirmTransaction.txData.chainId = CHAIN_IDS.OPTIMISM; - const store = configureMockStore(middleware)(mockedStore); - const { queryByText } = renderWithProvider( - , - store, - ); + it('should contain L1 L2 fee details for optimism', async () => { + const state = { + metamask: { + ...baseStore.metamask, + providerConfig: { + ...baseStore.metamask.providerConfig, + chainId: CHAIN_IDS.OPTIMISM, + }, + }, + confirmTransaction: { + ...baseStore.confirmTransaction, + txData: { + ...baseStore.confirmTransaction.txData, + chainId: CHAIN_IDS.OPTIMISM, + }, + }, + }; + + const { queryByText } = await render({ state }); + expect(queryByText('Layer 1 fees')).toBeInTheDocument(); expect(queryByText('Layer 2 gas fee')).toBeInTheDocument(); }); - it('should render NoteToTrader when isNoteToTraderSupported is true', () => { - mockedStore.metamask.custodyAccountDetails = { - [mockTxParamsFromAddress]: { - address: mockTxParamsFromAddress, - details: 'details', - custodyType: 'testCustody - Saturn', - custodianName: 'saturn-dev', - }, - }; - - mockedStore.metamask.mmiConfiguration = { - custodians: [ - { - envName: 'saturn-dev', - displayName: 'Saturn Custody', - isNoteToTraderSupported: true, + it('should render NoteToTrader when isNoteToTraderSupported is true', async () => { + const state = { + metamask: { + ...baseStore.metamask, + custodyAccountDetails: { + [mockTxParamsFromAddress]: { + address: mockTxParamsFromAddress, + details: 'details', + custodyType: 'testCustody - Saturn', + custodianName: 'saturn-dev', + }, }, - ], + mmiConfiguration: { + custodians: [ + { + envName: 'saturn-dev', + displayName: 'Saturn Custody', + isNoteToTraderSupported: true, + }, + ], + }, + }, }; - const store = configureMockStore(middleware)(mockedStore); - const { getByTestId } = renderWithProvider( - , - store, - ); + const { getByTestId } = await render({ state }); expect(getByTestId('note-tab')).toBeInTheDocument(); }); it('handleMMISubmit calls sendTransaction correctly when isNoteToTraderSupported is false', async () => { - const newMockedStore = { - ...mockedStore, + const state = { appState: { - ...mockedStore.appState, + ...baseStore.appState, gasLoadingAnimationIsShowing: false, }, confirmTransaction: { - ...mockedStore.confirmTransaction, + ...baseStore.confirmTransaction, txData: { - ...mockedStore.confirmTransaction.txData, + ...baseStore.confirmTransaction.txData, custodyStatus: true, }, }, metamask: { - ...mockedStore.metamask, + ...baseStore.metamask, accounts: { [mockTxParamsFromAddress]: { balance: '0x1000000000000000000', @@ -347,7 +405,7 @@ describe('Confirm Transaction Base', () => { gasEstimateType: GasEstimateTypes.feeMarket, selectedNetworkClientId: NetworkType.mainnet, networksMetadata: { - ...mockedStore.metamask.networksMetadata, + ...baseStore.metamask.networksMetadata, [NetworkType.mainnet]: { EIPS: { 1559: true, @@ -385,9 +443,9 @@ describe('Confirm Transaction Base', () => { }, }, send: { - ...mockedStore.send, + ...baseStore.send, gas: { - ...mockedStore.send.gas, + ...baseStore.send.gas, gasEstimateType: GasEstimateTypes.legacy, gasFeeEstimates: { low: '0', @@ -404,27 +462,24 @@ describe('Confirm Transaction Base', () => { }, }; - const store = configureMockStore(middleware)(newMockedStore); const sendTransaction = jest .fn() - .mockResolvedValue(newMockedStore.confirmTransaction.txData); + .mockResolvedValue(state.confirmTransaction.txData); const updateTransaction = jest.fn().mockResolvedValue(); const showCustodianDeepLink = jest.fn(); const setWaitForConfirmDeepLinkDialog = jest.fn(); - const { getByTestId } = renderWithProvider( - , - store, - ); + const props = { + sendTransaction, + updateTransaction, + showCustodianDeepLink, + setWaitForConfirmDeepLinkDialog, + toAddress: mockPropsToAddress, + toAccounts: [{ address: mockPropsToAddress }], + isMainBetaFlask: false, + }; + + const { getByTestId } = await render({ props, state }); const confirmButton = getByTestId('page-container-footer-next'); @@ -436,14 +491,13 @@ describe('Confirm Transaction Base', () => { }); it('handleMainSubmit calls sendTransaction correctly', async () => { - const newMockedStore = { - ...mockedStore, + const state = { appState: { - ...mockedStore.appState, + ...baseStore.appState, gasLoadingAnimationIsShowing: false, }, metamask: { - ...mockedStore.metamask, + ...baseStore.metamask, accounts: { [mockTxParamsFromAddress]: { balance: '0x1000000000000000000', @@ -453,7 +507,7 @@ describe('Confirm Transaction Base', () => { gasEstimateType: GasEstimateTypes.feeMarket, selectedNetworkClientId: NetworkType.mainnet, networksMetadata: { - ...mockedStore.metamask.networksMetadata, + ...baseStore.metamask.networksMetadata, [NetworkType.mainnet]: { EIPS: { 1559: true }, status: NetworkStatus.Available, @@ -466,9 +520,9 @@ describe('Confirm Transaction Base', () => { noGasPrice: false, }, send: { - ...mockedStore.send, + ...baseStore.send, gas: { - ...mockedStore.send.gas, + ...baseStore.send.gas, gasEstimateType: GasEstimateTypes.legacy, gasFeeEstimates: { low: '0', @@ -485,39 +539,38 @@ describe('Confirm Transaction Base', () => { }, }; - const store = configureMockStore(middleware)(newMockedStore); const sendTransaction = jest.fn().mockResolvedValue(); - const { getByTestId } = renderWithProvider( - , - store, - ); + const props = { + sendTransaction, + toAddress: mockPropsToAddress, + toAccounts: [{ address: mockPropsToAddress }], + }; + + const { getByTestId } = await render({ props, state }); + const confirmButton = getByTestId('page-container-footer-next'); - fireEvent.click(confirmButton); + await act(async () => { + fireEvent.click(confirmButton); + }); expect(sendTransaction).toHaveBeenCalled(); }); it('handleMMISubmit calls sendTransaction correctly and then showCustodianDeepLink', async () => { - const newMockedStore = { - ...mockedStore, + const state = { appState: { - ...mockedStore.appState, + ...baseStore.appState, gasLoadingAnimationIsShowing: false, }, confirmTransaction: { - ...mockedStore.confirmTransaction, + ...baseStore.confirmTransaction, txData: { - ...mockedStore.confirmTransaction.txData, + ...baseStore.confirmTransaction.txData, custodyStatus: true, }, }, metamask: { - ...mockedStore.metamask, + ...baseStore.metamask, accounts: { [mockTxParamsFromAddress]: { balance: '0x1000000000000000000', @@ -527,7 +580,7 @@ describe('Confirm Transaction Base', () => { gasEstimateType: GasEstimateTypes.feeMarket, selectedNetworkClientId: NetworkType.mainnet, networksMetadata: { - ...mockedStore.metamask.networksMetadata, + ...baseStore.metamask.networksMetadata, [NetworkType.mainnet]: { EIPS: { 1559: true, @@ -542,9 +595,9 @@ describe('Confirm Transaction Base', () => { noGasPrice: false, }, send: { - ...mockedStore.send, + ...baseStore.send, gas: { - ...mockedStore.send.gas, + ...baseStore.send.gas, gasEstimateType: GasEstimateTypes.legacy, gasFeeEstimates: { low: '0', @@ -561,27 +614,27 @@ describe('Confirm Transaction Base', () => { }, }; - const store = configureMockStore(middleware)(newMockedStore); const sendTransaction = jest .fn() - .mockResolvedValue(newMockedStore.confirmTransaction.txData); + .mockResolvedValue(state.confirmTransaction.txData); const showCustodianDeepLink = jest.fn(); const setWaitForConfirmDeepLinkDialog = jest.fn(); - const { getByTestId } = renderWithProvider( - , - store, - ); + const props = { + sendTransaction, + showCustodianDeepLink, + setWaitForConfirmDeepLinkDialog, + toAddress: mockPropsToAddress, + toAccounts: [{ address: mockPropsToAddress }], + isMainBetaFlask: false, + }; + + const { getByTestId } = await render({ props, state }); + const confirmButton = getByTestId('page-container-footer-next'); - fireEvent.click(confirmButton); + await act(async () => { + fireEvent.click(confirmButton); + }); expect(setWaitForConfirmDeepLinkDialog).toHaveBeenCalled(); await expect(sendTransaction).toHaveBeenCalled(); expect(showCustodianDeepLink).toHaveBeenCalled(); @@ -589,27 +642,19 @@ describe('Confirm Transaction Base', () => { describe('when rendering the recipient value', () => { describe(`when the transaction is a ${TransactionType.simpleSend} type`, () => { - it(`should use txParams.to address`, () => { - const store = configureMockStore(middleware)(mockedStore); - const { container } = renderWithProvider( - , - store, - ); + it(`should use txParams.to address`, async () => { + const { container } = await render(); const recipientElem = container.querySelector(sendToRecipientSelector); expect(recipientElem).toHaveTextContent(mockTxParamsToAddressConcat); }); - it(`should use txParams.to address even if there is no amount sent`, () => { - mockedStoreWithConfirmTxParams({ + it(`should use txParams.to address even if there is no amount sent`, async () => { + const state = mockedStoreWithConfirmTxParams(baseStore, { ...mockTxParams, value: '0x0', }); - const store = configureMockStore(middleware)(mockedStore); - const { container } = renderWithProvider( - , - store, - ); + const { container } = await render({ state }); const recipientElem = container.querySelector(sendToRecipientSelector); expect(recipientElem).toHaveTextContent(mockTxParamsToAddressConcat); @@ -617,21 +662,22 @@ describe('Confirm Transaction Base', () => { }); describe(`when the transaction is NOT a ${TransactionType.simpleSend} type`, () => { beforeEach(() => { - mockedStore.confirmTransaction.txData.type = + baseStore.confirmTransaction.txData.type = TransactionType.contractInteraction; }); describe('when there is an amount being sent (it should be treated as a general contract intereaction rather than custom one)', () => { - it('should use txParams.to address (contract address)', () => { - mockedStoreWithConfirmTxParams({ + it('should use txParams.to address (contract address)', async () => { + const state = mockedStoreWithConfirmTxParams(baseStore, { ...mockTxParams, value: '0x45666', }); - const store = configureMockStore(middleware)(mockedStore); - const { container } = renderWithProvider( - , - store, - ); + state.confirmTransaction.txData = { + ...state.confirmTransaction.txData, + type: TransactionType.contractInteraction, + }; + + const { container } = await render({ state }); const recipientElem = container.querySelector( sendToRecipientSelector, @@ -641,23 +687,24 @@ describe('Confirm Transaction Base', () => { }); describe(`when there is no amount being sent`, () => { - it('should use propToAddress (toAddress passed as prop)', () => { - mockedStoreWithConfirmTxParams({ + it('should use propToAddress (toAddress passed as prop)', async () => { + const state = mockedStoreWithConfirmTxParams(baseStore, { ...mockTxParams, value: '0x0', }); - const store = configureMockStore(middleware)(mockedStore); - - const { container } = renderWithProvider( - , - store, - ); + state.confirmTransaction.txData = { + ...state.confirmTransaction.txData, + type: TransactionType.contractInteraction, + }; + + const props = { + // we want to test toAddress provided by ownProps in mapStateToProps, but this + // currently overrides toAddress this should pan out fine when we refactor the + // component into a functional component and remove the container.js file + toAddress: mockPropsToAddress, + }; + + const { container } = await render({ props, state }); const recipientElem = container.querySelector( sendToRecipientSelector, @@ -665,16 +712,19 @@ describe('Confirm Transaction Base', () => { expect(recipientElem).toHaveTextContent(mockPropsToAddressConcat); }); - it('should use address parsed from transaction data if propToAddress is not provided', () => { - mockedStoreWithConfirmTxParams({ + it('should use address parsed from transaction data if propToAddress is not provided', async () => { + const state = mockedStoreWithConfirmTxParams(baseStore, { ...mockTxParams, value: '0x0', }); - const store = configureMockStore(middleware)(mockedStore); - const { container } = renderWithProvider( - , - store, - ); + state.confirmTransaction.txData = { + ...state.confirmTransaction.txData, + type: TransactionType.contractInteraction, + }; + + const props = {}; + + const { container } = await render({ props, state }); const recipientElem = container.querySelector( sendToRecipientSelector, @@ -682,17 +732,20 @@ describe('Confirm Transaction Base', () => { expect(recipientElem).toHaveTextContent(mockParsedTxDataToAddress); }); - it('should use txParams.to if neither propToAddress is not provided nor the transaction data to address were provided', () => { - mockedStoreWithConfirmTxParams({ + it('should use txParams.to if neither propToAddress is not provided nor the transaction data to address were provided', async () => { + const state = mockedStoreWithConfirmTxParams(baseStore, { ...mockTxParams, data: '0x', value: '0x0', }); - const store = configureMockStore(middleware)(mockedStore); - const { container } = renderWithProvider( - , - store, - ); + state.confirmTransaction.txData = { + ...state.confirmTransaction.txData, + type: TransactionType.contractInteraction, + }; + + const props = {}; + + const { container } = await render({ props, state }); const recipientElem = container.querySelector( sendToRecipientSelector, @@ -703,19 +756,19 @@ describe('Confirm Transaction Base', () => { }); }); describe('user op contract deploy attempt', () => { - it('should show error and disable Confirm button', () => { + it('should show error and disable Confirm button', async () => { const txParams = { ...mockTxParams, to: undefined, data: '0xa22cb46500000000000000', }; - const newMockedStore = { - ...mockedStore, + const state = { + ...baseStore, metamask: { - ...mockedStore.metamask, + ...baseStore.metamask, transactions: [ { - id: mockedStore.confirmTransaction.txData.id, + id: baseStore.confirmTransaction.txData.id, chainId: '0x5', status: 'unapproved', txParams, @@ -723,9 +776,9 @@ describe('Confirm Transaction Base', () => { ], }, confirmTransaction: { - ...mockedStore.confirmTransaction, + ...baseStore.confirmTransaction, txData: { - ...mockedStore.confirmTransaction.txData, + ...baseStore.confirmTransaction.txData, type: TransactionType.deployContract, value: '0x0', isUserOperation: true, @@ -734,11 +787,7 @@ describe('Confirm Transaction Base', () => { }, }; - const store = configureMockStore(middleware)(newMockedStore); - const { getByTestId } = renderWithProvider( - , - store, - ); + const { getByTestId } = await render({ state }); const banner = getByTestId( 'confirm-page-container-content-error-banner-2', diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js index f32867639b11..41f31a856301 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js @@ -37,19 +37,19 @@ import { unconfirmedTransactionsListSelector, unconfirmedTransactionsHashSelector, use4ByteResolutionSelector, + getSelectedNetworkClientId, } from '../../../selectors'; import { - disconnectGasFeeEstimatePoller, getContractMethodData, - getGasFeeEstimatesAndStartPolling, - addPollingTokenToAppState, - removePollingTokenFromAppState, setDefaultHomeActiveTabName, + gasFeeStartPollingByNetworkClientId, + gasFeeStopPollingByPollingToken, } from '../../../store/actions'; import ConfirmSignatureRequest from '../confirm-signature-request'; ///: BEGIN:ONLY_INCLUDE_IF(conf-redesign) import Confirm from '../confirm/confirm'; ///: END:ONLY_INCLUDE_IF +import usePolling from '../../../hooks/usePolling'; import ConfirmTokenTransactionSwitch from './confirm-token-transaction-switch'; const ConfirmTransaction = () => { @@ -57,14 +57,12 @@ const ConfirmTransaction = () => { const history = useHistory(); const { id: paramsTransactionId } = useParams(); - const [isMounted, setIsMounted] = useState(false); - const [pollingToken, setPollingToken] = useState(); - const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); const sendTo = useSelector(getSendTo); const unconfirmedTxsSorted = useSelector(unconfirmedTransactionsListSelector); const unconfirmedTxs = useSelector(unconfirmedTransactionsHashSelector); + const networkClientId = useSelector(getSelectedNetworkClientId); const totalUnapproved = unconfirmedTxsSorted.length || 0; const getTransaction = useCallback(() => { @@ -109,30 +107,13 @@ const ConfirmTransaction = () => { const prevParamsTransactionId = usePrevious(paramsTransactionId); const prevTransactionId = usePrevious(transactionId); - const _beforeUnload = useCallback(() => { - setIsMounted(false); - - if (pollingToken) { - disconnectGasFeeEstimatePoller(pollingToken); - removePollingTokenFromAppState(pollingToken); - } - }, [pollingToken]); + usePolling({ + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, + stopPollingByPollingToken: gasFeeStopPollingByPollingToken, + networkClientId: transaction.networkClientId ?? networkClientId, + }); useEffect(() => { - setIsMounted(true); - - getGasFeeEstimatesAndStartPolling().then((_pollingToken) => { - if (isMounted) { - setPollingToken(_pollingToken); - addPollingTokenToAppState(_pollingToken); - } else { - disconnectGasFeeEstimatePoller(_pollingToken); - removePollingTokenFromAppState(_pollingToken); - } - }); - - window.addEventListener('beforeunload', _beforeUnload); - if (!totalUnapproved && !sendTo) { history.replace(mostRecentOverviewPage); } else { @@ -148,10 +129,6 @@ const ConfirmTransaction = () => { } } - return () => { - _beforeUnload(); - window.removeEventListener('beforeunload', _beforeUnload); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js index c374316f320a..69d6206aeb09 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js @@ -135,6 +135,11 @@ jest.mock('../confirm-transaction-switch', () => { }); describe('Confirmation Transaction Page', () => { + beforeEach(() => { + jest + .spyOn(Actions, 'gasFeeStartPollingByNetworkClientId') + .mockResolvedValue(null); + }); it('should display the Loading component when the transaction is invalid', () => { const mockStore = configureMockStore(middleware)({ ...mockState, @@ -216,10 +221,9 @@ describe('Confirmation Transaction Page', () => { describe('initialization', () => { it('should poll for gas estimates', () => { const mockStore = configureMockStore(middleware)(mockState); - const gasEstimationPollingSpy = jest.spyOn( - Actions, - 'getGasFeeEstimatesAndStartPolling', - ); + const gasEstimationPollingSpy = jest + .spyOn(Actions, 'gasFeeStartPollingByNetworkClientId') + .mockResolvedValue(null); renderWithProvider(, mockStore); diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.transaction.test.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.transaction.test.js index 5be82bcb7588..4c3f9f20dc99 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.transaction.test.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.transaction.test.js @@ -1,7 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; - +import * as Actions from '../../../store/actions'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { setBackgroundConnection } from '../../../store/background-connection'; import mockState from '../../../../test/data/mock-state.json'; @@ -28,6 +28,9 @@ describe('Confirm Transaction', () => { mockState.metamask.transactions, )[0]; it('should render correct information for approve transaction with value', () => { + jest + .spyOn(Actions, 'gasFeeStartPollingByNetworkClientId') + .mockResolvedValue(null); const store = configureMockStore(middleware)({ ...mockState, confirmTransaction: { diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index a0805096dbf6..5805c56c8f93 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -6,7 +6,6 @@ import { getNativeCurrency, } from '../../../ducks/metamask/metamask'; import { - checkNetworkAndAccountSupports1559, getCurrentCurrency, getShouldShowFiat, getPreferences, @@ -132,7 +131,7 @@ export const generateUseSelectorRouter = if (selector === getCustomMaxPriorityFeePerGas) { return '0x5208'; } - if (selector === checkNetworkAndAccountSupports1559) { + if (selector.toString().includes('checkNetworkAndAccountSupports1559')) { return checkNetworkAndAccountSupports1559Response; } if (selector === getCurrentKeyring) { diff --git a/ui/pages/confirmations/hooks/useGasEstimates.js b/ui/pages/confirmations/hooks/useGasEstimates.js index a277bdff4d5d..083c084689a6 100644 --- a/ui/pages/confirmations/hooks/useGasEstimates.js +++ b/ui/pages/confirmations/hooks/useGasEstimates.js @@ -53,8 +53,9 @@ export function useGasEstimates({ transaction, }) { const supportsEIP1559 = - useSelector(checkNetworkAndAccountSupports1559) && - !isLegacyTransaction(transaction?.txParams); + useSelector((state) => + checkNetworkAndAccountSupports1559(state, transaction?.networkClientId), + ) && !isLegacyTransaction(transaction?.txParams); const { currency: primaryCurrency, @@ -76,7 +77,7 @@ export function useGasEstimates({ maxPriorityFeePerGas: decGWEIToHexWEI( maxPriorityFeePerGas || maxFeePerGas || gasPrice || '0', ), - baseFeePerGas: decGWEIToHexWEI(gasFeeEstimates.estimatedBaseFee ?? '0'), + baseFeePerGas: decGWEIToHexWEI(gasFeeEstimates?.estimatedBaseFee ?? '0'), }; } else { gasSettings = { diff --git a/ui/pages/confirmations/hooks/useGasFeeInputs.js b/ui/pages/confirmations/hooks/useGasFeeInputs.js index 90913f8f86c3..27a60ad5f043 100644 --- a/ui/pages/confirmations/hooks/useGasFeeInputs.js +++ b/ui/pages/confirmations/hooks/useGasFeeInputs.js @@ -130,7 +130,7 @@ export function useGasFeeInputs( gasFeeEstimates, isGasEstimatesLoading, isNetworkBusy, - } = useGasFeeEstimates(); + } = useGasFeeEstimates(transaction?.networkClientId); const userPrefersAdvancedGas = useSelector(getAdvancedInlineGasShown); diff --git a/ui/pages/confirmations/hooks/useIncrementedGasFees.js b/ui/pages/confirmations/hooks/useIncrementedGasFees.js index 319312e1b421..68556c216f53 100644 --- a/ui/pages/confirmations/hooks/useIncrementedGasFees.js +++ b/ui/pages/confirmations/hooks/useIncrementedGasFees.js @@ -39,7 +39,9 @@ function getHighestIncrementedFee(originalFee, currentEstimate) { * ).CustomGasSettings} Gas settings for cancellations/speed ups */ export function useIncrementedGasFees(transaction) { - const { gasFeeEstimates = {} } = useGasFeeEstimates(); + const { gasFeeEstimates = {} } = useGasFeeEstimates( + transaction.networkClientId, + ); // We memoize this value so that it can be relied upon in other hooks. const customGasSettings = useMemo(() => { diff --git a/ui/pages/confirmations/hooks/useTransactionFunctions.js b/ui/pages/confirmations/hooks/useTransactionFunctions.js index db947e08f337..17b5165da94e 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctions.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctions.js @@ -183,6 +183,9 @@ export const useTransactionFunctions = ({ ? CUSTOM_GAS_ESTIMATE : PriorityLevels.tenPercentIncreased; + if (!gasFeeEstimates) { + return; + } updateTransaction({ estimateSuggested: initTransaction ? defaultEstimateToUse diff --git a/ui/pages/confirmations/send/send-content/__snapshots__/send-content.component.test.js.snap b/ui/pages/confirmations/send/send-content/__snapshots__/send-content.component.test.js.snap index cb4204841623..55fc897b62f5 100644 --- a/ui/pages/confirmations/send/send-content/__snapshots__/send-content.component.test.js.snap +++ b/ui/pages/confirmations/send/send-content/__snapshots__/send-content.component.test.js.snap @@ -287,5 +287,3 @@ exports[`SendContent Component render should match snapshot 1`] = `
`; - -exports[`SendContent Component render should match snapshot 2`] = `
`; diff --git a/ui/pages/confirmations/send/send-content/send-content.component.test.js b/ui/pages/confirmations/send/send-content/send-content.component.test.js index 88c6fb270cce..ccfa8769c7f0 100644 --- a/ui/pages/confirmations/send/send-content/send-content.component.test.js +++ b/ui/pages/confirmations/send/send-content/send-content.component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { act, waitFor } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; @@ -13,10 +13,13 @@ import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalN import SendContent from '.'; jest.mock('../../../../store/actions', () => ({ - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest.fn().mockResolvedValue(), - addPollingTokenToAppState: jest.fn(), - removePollingTokenFromAppState: jest.fn(), + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), createTransactionEventFragment: jest.fn(), getGasFeeTimeEstimate: jest.fn().mockResolvedValue('unknown'), getTokenSymbol: jest.fn().mockResolvedValue('ETH'), @@ -28,31 +31,41 @@ jest.mock('../../../../hooks/useIsOriginalNativeTokenSymbol', () => { }; }); -describe('SendContent Component', () => { - useIsOriginalNativeTokenSymbol.mockReturnValue(true); - describe('render', () => { - const mockStore = configureMockStore()({ - ...mockSendState, - metamask: { - ...mockSendState.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, +const render = async (props, overrideStoreState) => { + const mockStore = configureMockStore()({ + ...mockSendState, + metamask: { + ...mockSendState.metamask, + providerConfig: { + chainId: CHAIN_IDS.GOERLI, + nickname: GOERLI_DISPLAY_NAME, + type: NETWORK_TYPES.GOERLI, }, - }); + }, + ...overrideStoreState, + }); + + let result; + await act( + async () => + (result = renderWithProvider(, mockStore)), + ); + + return result; +}; + +describe('SendContent Component', () => { + beforeEach(() => { + useIsOriginalNativeTokenSymbol.mockReturnValue(true); + }); + + describe('render', () => { it('should match snapshot', async () => { - const props = { + const { container } = await render({ gasIsExcessive: false, showHexData: true, - }; - - const { container } = renderWithProvider( - , - mockStore, - ); + }); await waitFor(() => { expect(container).toMatchSnapshot(); @@ -61,78 +74,47 @@ describe('SendContent Component', () => { }); describe('SendHexDataRow', () => { - const tokenAssetState = { - ...mockSendState, - send: { - ...mockSendState.send, - draftTransactions: { - '1-tx': { - ...mockSendState.send.draftTransactions['1-tx'], - asset: { - balance: '0x3635c9adc5dea00000', - details: { - address: '0xAddress', - decimals: 18, - symbol: 'TST', - balance: '1', - standard: 'ERC20', - }, - error: null, - type: 'TOKEN', - }, - }, - }, - }, - metamask: { - ...mockSendState.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, - }; - it('should not render the SendHexDataRow if props.showHexData is false', async () => { - const props = { + const { queryByText } = await render({ gasIsExcessive: false, showHexData: false, - }; - - const mockStore = configureMockStore()({ - ...mockSendState, - metamask: { - ...mockSendState.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, }); - const { queryByText } = renderWithProvider( - , - mockStore, - ); - await waitFor(() => { expect(queryByText('Hex data:')).not.toBeInTheDocument(); }); }); it('should not render the SendHexDataRow if the asset type is TOKEN (ERC-20)', async () => { - const props = { - gasIsExcessive: false, - showHexData: true, + const tokenAssetState = { + send: { + ...mockSendState.send, + draftTransactions: { + '1-tx': { + ...mockSendState.send.draftTransactions['1-tx'], + asset: { + balance: '0x3635c9adc5dea00000', + details: { + address: '0xAddress', + decimals: 18, + symbol: 'TST', + balance: '1', + standard: 'ERC20', + }, + error: null, + type: 'TOKEN', + }, + }, + }, + }, }; - // Use token draft transaction asset - const mockState = configureMockStore()(tokenAssetState); - - const { queryByText } = renderWithProvider( - , - mockState, + const { queryByText } = await render( + { + gasIsExcessive: false, + showHexData: true, + }, + tokenAssetState, ); await waitFor(() => { @@ -143,28 +125,11 @@ describe('SendContent Component', () => { describe('Gas Error', () => { it('should show gas warning when gasIsExcessive prop is true.', async () => { - const props = { + const { queryByTestId } = await render({ gasIsExcessive: true, showHexData: false, - }; - - const mockStore = configureMockStore()({ - ...mockSendState, - metamask: { - ...mockSendState.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, }); - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); - const gasWarning = queryByTestId('gas-warning-message'); await waitFor(() => { @@ -173,13 +138,7 @@ describe('SendContent Component', () => { }); it('should show gas warning for none gasEstimateType in state', async () => { - const props = { - gasIsExcessive: false, - showHexData: false, - }; - const noGasPriceState = { - ...mockSendState, metamask: { ...mockSendState.metamask, gasEstimateType: 'none', @@ -191,11 +150,12 @@ describe('SendContent Component', () => { }, }; - const mockStore = configureMockStore()(noGasPriceState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, + const { queryByTestId } = await render( + { + gasIsExcessive: false, + showHexData: false, + }, + noGasPriceState, ); const gasWarning = queryByTestId('gas-warning-message'); @@ -208,13 +168,7 @@ describe('SendContent Component', () => { describe('Recipient Warning', () => { it('should show recipient warning with knownAddressRecipient state in draft transaction state', async () => { - const props = { - gasIsExcessive: false, - showHexData: false, - }; - const knownRecipientWarningState = { - ...mockSendState, send: { ...mockSendState.send, draftTransactions: { @@ -238,11 +192,12 @@ describe('SendContent Component', () => { }, }; - const mockStore = configureMockStore()(knownRecipientWarningState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, + const { queryByTestId } = await render( + { + gasIsExcessive: false, + showHexData: false, + }, + knownRecipientWarningState, ); const sendWarning = queryByTestId('send-warning'); @@ -255,13 +210,7 @@ describe('SendContent Component', () => { describe('Assert Error', () => { it('should render dialog error with asset error in draft transaction state', async () => { - const props = { - gasIsExcessive: false, - showHexData: false, - }; - const assertErrorState = { - ...mockSendState, send: { ...mockSendState.send, draftTransactions: { @@ -285,11 +234,12 @@ describe('SendContent Component', () => { }, }; - const mockStore = configureMockStore()(assertErrorState); - - const { queryByTestId } = renderWithProvider( - , - mockStore, + const { queryByTestId } = await render( + { + gasIsExcessive: false, + showHexData: false, + }, + assertErrorState, ); const dialogMessage = queryByTestId('dialog-message'); @@ -302,29 +252,12 @@ describe('SendContent Component', () => { describe('Warning', () => { it('should display warning dialog message from warning prop', async () => { - const props = { + const { queryByTestId } = await render({ gasIsExcessive: false, showHexData: false, warning: 'warning', - }; - - const mockStore = configureMockStore()({ - ...mockSendState, - metamask: { - ...mockSendState.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, }); - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); - const dialogMessage = queryByTestId('dialog-message'); await waitFor(() => { diff --git a/ui/pages/confirmations/token-allowance/token-allowance.test.js b/ui/pages/confirmations/token-allowance/token-allowance.test.js index 881ebd59cb42..8665592d77d9 100644 --- a/ui/pages/confirmations/token-allowance/token-allowance.test.js +++ b/ui/pages/confirmations/token-allowance/token-allowance.test.js @@ -140,11 +140,20 @@ const mockedState = jest.mocked(state); jest.mock('../../../store/actions', () => ({ disconnectGasFeeEstimatePoller: jest.fn(), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), getGasFeeEstimatesAndStartPolling: jest .fn() .mockImplementation(() => Promise.resolve()), addPollingTokenToAppState: jest.fn(), removePollingTokenFromAppState: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest.fn().mockImplementation(() => + Promise.resolve({ + chainId: '0x5', + }), + ), updateTransactionGasFees: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), updatePreviousGasParams: () => ({ type: 'UPDATE_TRANSACTION_PARAMS' }), createTransactionEventFragment: jest.fn(), diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index fa2d5873138c..e684fedc32cb 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -1,6 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import { SEND_STAGES } from '../../ducks/send'; import { renderWithProvider } from '../../../test/jest'; @@ -28,10 +28,13 @@ jest.mock('webextension-polyfill', () => ({ jest.mock('../../store/actions', () => ({ getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), - getGasFeeEstimatesAndStartPolling: jest + gasFeeStartPollingByNetworkClientId: jest .fn() - .mockImplementation(() => Promise.resolve()), - addPollingTokenToAppState: jest.fn(), + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x5' }), showNetworkDropdown: () => mockShowNetworkDropdown, hideNetworkDropdown: () => mockHideNetworkDropdown, })); @@ -64,31 +67,49 @@ jest.mock( '../../components/app/metamask-template-renderer/safe-component-list', ); +const render = async (route, state) => { + const store = configureMockStore()({ + ...mockSendState, + ...state, + }); + + let result; + + await act( + async () => (result = renderWithProvider(, store, route)), + ); + + return result; +}; + describe('Routes Component', () => { useIsOriginalNativeTokenSymbol.mockImplementation(() => true); + afterEach(() => { mockShowNetworkDropdown.mockClear(); mockHideNetworkDropdown.mockClear(); }); + describe('render during send flow', () => { - it('should render with network change disabled while adding recipient for send flow', () => { - const store = configureMockStore()({ - ...mockSendState, + it('should render with network change disabled while adding recipient for send flow', async () => { + const state = { send: { ...mockSendState.send, stage: SEND_STAGES.ADD_RECIPIENT, }, - }); + }; - const { getByTestId } = renderWithProvider(, store, ['/send']); + const { getByTestId } = await render(['/send'], state); const networkDisplay = getByTestId('network-display'); - fireEvent.click(networkDisplay); + await act(async () => { + fireEvent.click(networkDisplay); + }); expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); }); - it('should render with network change disabled while user is in send page', () => { - const store = configureMockStore()({ - ...mockSendState, + + it('should render with network change disabled while user is in send page', async () => { + const state = { metamask: { ...mockSendState.metamask, providerConfig: { @@ -97,16 +118,18 @@ describe('Routes Component', () => { type: NETWORK_TYPES.GOERLI, }, }, - }); - const { getByTestId } = renderWithProvider(, store, ['/send']); + }; + const { getByTestId } = await render(['/send'], state); const networkDisplay = getByTestId('network-display'); - fireEvent.click(networkDisplay); + await act(async () => { + fireEvent.click(networkDisplay); + }); expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); }); - it('should render with network change disabled while editing a send transaction', () => { - const store = configureMockStore()({ - ...mockSendState, + + it('should render with network change disabled while editing a send transaction', async () => { + const state = { send: { ...mockSendState.send, stage: SEND_STAGES.EDIT, @@ -119,16 +142,18 @@ describe('Routes Component', () => { type: NETWORK_TYPES.GOERLI, }, }, - }); - const { getByTestId } = renderWithProvider(, store, ['/send']); + }; + const { getByTestId } = await render(['/send'], state); const networkDisplay = getByTestId('network-display'); - fireEvent.click(networkDisplay); + await act(async () => { + fireEvent.click(networkDisplay); + }); expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); }); - it('should render when send transaction is not active', () => { - const store = configureMockStore()({ - ...mockSendState, + + it('should render when send transaction is not active', async () => { + const state = { metamask: { ...mockSendState.metamask, swapsState: { @@ -151,8 +176,8 @@ describe('Routes Component', () => { localeMessages: { currentLocale: 'en', }, - }); - const { getByTestId } = renderWithProvider(, store); + }; + const { getByTestId } = await render(undefined, state); expect(getByTestId('account-menu-icon')).not.toBeDisabled(); }); }); diff --git a/ui/pages/swaps/index.test.js b/ui/pages/swaps/index.test.js index 3844631bc892..ddd867bc62d4 100644 --- a/ui/pages/swaps/index.test.js +++ b/ui/pages/swaps/index.test.js @@ -21,8 +21,13 @@ setBackgroundConnection({ setSwapsLiveness: jest.fn(() => true), setSwapsTokens: jest.fn(), setSwapsTxGasPrice: jest.fn(), - disconnectGasFeeEstimatePoller: jest.fn(), - getGasFeeEstimatesAndStartPolling: jest.fn(), + gasFeeStartPollingByNetworkClientId: jest + .fn() + .mockResolvedValue('pollingToken'), + gasFeeStopPollingByPollingToken: jest.fn(), + getNetworkConfigurationByNetworkClientId: jest + .fn() + .mockResolvedValue({ chainId: '0x1' }), }); describe('Swap', () => { @@ -49,6 +54,10 @@ describe('Swap', () => { .get('/networks/1/tokens') .reply(200, MOCKS.TOKENS_GET_RESPONSE); + nock(CONSTANTS.METASWAP_BASE_URL) + .get('/networks/1/tokens?includeBlockedTokens=true') + .reply(200, MOCKS.TOKENS_GET_RESPONSE); + featureFlagsNock = nock(CONSTANTS.METASWAP_BASE_URL) .get('/featureFlags') .reply(200, MOCKS.createFeatureFlagsResponse()); diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 080aae54dd87..4c3d2c4a51af 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -316,7 +316,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { const { maxFeePerGas: suggestedMaxFeePerGas, maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates: { estimatedBaseFee = '0' }, + gasFeeEstimates: { estimatedBaseFee = '0' } = {}, } = gasFeeInputs; maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); maxPriorityFeePerGas = diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 46460cf5c5c8..6b48b739785f 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -183,9 +183,10 @@ export function getCurrentKeyring(state) { * both of them support EIP-1559. * * @param state + * @param networkClientId - The optional network client ID to check network and account for EIP-1559 support */ -export function checkNetworkAndAccountSupports1559(state) { - const networkSupports1559 = isEIP1559Network(state); +export function checkNetworkAndAccountSupports1559(state, networkClientId) { + const networkSupports1559 = isEIP1559Network(state, networkClientId); return networkSupports1559; } diff --git a/ui/store/actions.ts b/ui/store/actions.ts index fc0a73dd5a00..6b24a9aa85df 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -31,7 +31,10 @@ import { TransactionParams, TransactionType, } from '@metamask/transaction-controller'; -import { NetworkClientId } from '@metamask/network-controller'; +import { + NetworkClientId, + NetworkConfiguration, +} from '@metamask/network-controller'; import { InterfaceState } from '@metamask/snaps-sdk'; import { getMethodDataAsync } from '../helpers/utils/transactions.util'; import switchDirection from '../../shared/lib/switch-direction'; @@ -4267,6 +4270,31 @@ export async function removePollingTokenFromAppState(pollingToken: string) { ]); } +/** + * Informs the GasFeeController that the UI requires gas fee polling + * + * @param networkClientId - unique identifier for the network client + * @returns polling token that can be used to stop polling + */ +export function gasFeeStartPollingByNetworkClientId(networkClientId: string) { + return submitRequestToBackground('gasFeeStartPollingByNetworkClientId', [ + networkClientId, + ]); +} + +/** + * Informs the GasFeeController that the UI no longer requires gas fee polling + * for the given network client. + * If all network clients unsubscribe, the controller stops polling. + * + * @param pollingToken - Poll token received from calling startPollingByNetworkClientId + */ +export function gasFeeStopPollingByPollingToken(pollingToken: string) { + return submitRequestToBackground('gasFeeStopPollingByPollingToken', [ + pollingToken, + ]); +} + export function getGasFeeTimeEstimate( maxPriorityFeePerGas: string, maxFeePerGas: string, @@ -4774,6 +4802,22 @@ export async function getCurrentNetworkEIP1559Compatibility(): Promise< return networkEIP1559Compatibility; } +export async function getNetworkConfigurationByNetworkClientId( + networkClientId: NetworkClientId, +): Promise { + let networkConfiguration; + try { + networkConfiguration = + await submitRequestToBackground( + 'getNetworkConfigurationByNetworkClientId', + [networkClientId], + ); + } catch (error) { + console.error(error); + } + return networkConfiguration; +} + export function updateProposedNames( request: UpdateProposedNamesRequest, ): ThunkAction<