Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add error handling for setCorrectChain #28740

Merged
merged 21 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
63cd7bf
chore: Add error handling and unit test for setCorrectChain
gambinish Nov 26, 2024
bd04656
fix: Mock react router correctly in token-buttons-test
gambinish Nov 26, 2024
1995a8d
fix: Expand test suite to include swap button click
gambinish Nov 26, 2024
fd633a4
fix: Lint and update snapshots
gambinish Nov 26, 2024
76de6cc
fix: Lint file import order
gambinish Nov 26, 2024
d920599
Merge branch 'develop' into chore/portfolio-view-swap-check
gambinish Nov 26, 2024
cf02585
chore: incorporate dapp network switching into setCorrectChain
gambinish Nov 26, 2024
d9a858e
Merge branch 'chore/portfolio-view-swap-check' of github.com:MetaMask…
gambinish Nov 26, 2024
e829c99
fix: Tweak asset-page-test state mock
gambinish Nov 26, 2024
d6a3682
fix: Add if condition to ensure that navigation doesnt happen on chai…
gambinish Nov 27, 2024
bc9a520
fix: Active tab no longer needs to be mocked in unit test suite for a…
gambinish Nov 27, 2024
a6e695e
fix: Add unit test coverage for coin-buttons
gambinish Nov 27, 2024
da5872e
fix: Lint
gambinish Nov 27, 2024
53d1026
fix: Throw error in setCurrentChain, catch and stop execution on failure
gambinish Nov 27, 2024
f31edb0
fix: Cleanup
gambinish Nov 27, 2024
1b42ecb
fix: Cleanup
gambinish Nov 27, 2024
ddb158e
fix: Move the network switch action outside of codefence
gambinish Nov 27, 2024
30b5bb9
chore: Revert unecessary snapshot update
gambinish Nov 27, 2024
8d8272d
chore: Remove mocked error
gambinish Nov 27, 2024
954652b
Merge branch 'develop' into chore/portfolio-view-swap-check
gambinish Nov 27, 2024
8a8f65c
Merge branch 'develop' into chore/portfolio-view-swap-check
gambinish Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions ui/components/app/wallet-overview/coin-buttons.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import { useHistory } from 'react-router-dom';
import thunk from 'redux-thunk';
import { mockNetworkState } from '../../../../test/stub/networks';
import { renderWithProvider } from '../../../../test/jest/rendering';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import mockState from '../../../../test/data/mock-state.json';
import * as actions from '../../../store/actions';
import {
PREPARE_SWAP_ROUTE,
SEND_ROUTE,
} from '../../../helpers/constants/routes';
import CoinButtons from './coin-buttons';
import { InternalEthEoaAccount } from '@metamask/keyring-api/dist/internal/types';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
}));

jest.mock('../../../ducks/send', () => ({
...jest.requireActual('../../../ducks/send'),
startNewDraftTransaction: jest.fn(() => ({
type: 'MOCK_START_NEW_DRAFT_TRANSACTION',
})),
}));

const selectedAccountMock: InternalEthEoaAccount = {
id: 'b39bc837-4c0f-4692-9e24-f2aef2eaefad',
address: '0x0521797e19b8e274e4ed3bfe5254faf6fac96f09',
options: {},
methods: [
'personal_sign',
'eth_sign',
'eth_signTransaction',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
],
type: 'eip155:eoa',
metadata: {
name: 'Account 2',
importTime: 1732668178048,
lastSelected: 1732670357591,
keyring: {
type: 'Simple Key Pair',
},
},
};

describe('CoinButtons Component', () => {
let mockPush: jest.Mock;
const mockStore = {
...mockState,
metamask: {
...mockState.metamask,
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
useExternalServices: true,
},
};

beforeEach(() => {
mockPush = jest.fn();
(useHistory as jest.Mock).mockReturnValue({ push: mockPush });
});

afterEach(() => {
jest.restoreAllMocks();
});

it('does not redirect to swap when there is a mismatched chainId between account and network', async () => {
const store = configureMockStore([thunk])(mockStore);

jest.spyOn(actions, 'setActiveNetwork').mockImplementation(() => {
throw new Error('setActiveNetwork mock failure');
});

const consoleErrorSpy = jest.spyOn(console, 'error');

const { getByTestId } = renderWithProvider(
<CoinButtons
account={selectedAccountMock}
chainId={CHAIN_IDS.POLYGON}
trackingLocation="Home"
isSwapsChain={true}
isSigningEnabled={true}
isBridgeChain={true}
isBuyableChain={true}
/>,
store,
);

const swapButton = getByTestId('token-overview-button-swap');

fireEvent.click(swapButton);

await waitFor(() => {
expect(mockPush).not.toHaveBeenCalled();
});

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(`Failed to switch chains.`),
);
consoleErrorSpy.mockRestore();
});

it('does redirect to swap when there is a matched chainId between account and network', async () => {
const store = configureMockStore([thunk])(mockStore);

const consoleErrorSpy = jest.spyOn(console, 'error');

const { getByTestId } = renderWithProvider(
<CoinButtons
account={selectedAccountMock}
chainId={CHAIN_IDS.MAINNET}
trackingLocation="Home"
isSwapsChain={true}
isSigningEnabled={true}
isBridgeChain={true}
isBuyableChain={true}
/>,
store,
);

const swapButton = getByTestId('token-overview-button-swap');

fireEvent.click(swapButton);

await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(PREPARE_SWAP_ROUTE);
});

expect(consoleErrorSpy).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});

it('does not redirect to send when there is a mismatched chainId between account and network', async () => {
const store = configureMockStore([thunk])(mockStore);

jest.spyOn(actions, 'setActiveNetwork').mockImplementation(() => {
throw new Error('setActiveNetwork mock failure');
});

const consoleErrorSpy = jest.spyOn(console, 'error');

const { getByTestId } = renderWithProvider(
<CoinButtons
account={selectedAccountMock}
chainId={CHAIN_IDS.POLYGON}
trackingLocation="Home"
isSwapsChain={true}
isSigningEnabled={true}
isBridgeChain={true}
isBuyableChain={true}
/>,
store,
);

const sendButton = getByTestId('coin-overview-send');

fireEvent.click(sendButton);

await waitFor(() => {
expect(mockPush).not.toHaveBeenCalled();
});

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(`Failed to switch chains.`),
);
consoleErrorSpy.mockRestore();
});

it('does redirect to send when there is a matched chainId between account and network', async () => {
const store = configureMockStore([thunk])(mockStore);

const consoleErrorSpy = jest.spyOn(console, 'error');

const { getByTestId } = renderWithProvider(
<CoinButtons
account={selectedAccountMock}
chainId={CHAIN_IDS.MAINNET}
trackingLocation="Home"
isSwapsChain={true}
isSigningEnabled={true}
isBridgeChain={true}
isBuyableChain={true}
/>,
store,
);

const sendButton = getByTestId('coin-overview-send');

fireEvent.click(sendButton);

await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(SEND_ROUTE);
});

expect(consoleErrorSpy).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
36 changes: 28 additions & 8 deletions ui/components/app/wallet-overview/coin-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,13 +328,19 @@ const CoinButtons = ({

const setCorrectChain = useCallback(async () => {
if (currentChainId !== chainId) {
const networkConfigurationId = networks[chainId];
await dispatch(setActiveNetwork(networkConfigurationId));
await dispatch(
setSwitchedNetworkDetails({
networkClientId: networkConfigurationId,
}),
);
try {
const networkConfigurationId = networks[chainId];
await dispatch(setActiveNetwork(networkConfigurationId));
await dispatch(
setSwitchedNetworkDetails({
networkClientId: networkConfigurationId,
}),
);
} catch (err) {
console.error(`Failed to switch chains.
Target chainId: ${chainId}, Current chainId: ${currentChainId}.
${err}`);
}
}
}, [currentChainId, chainId, networks, dispatch]);

Expand Down Expand Up @@ -389,15 +395,29 @@ const CoinButtons = ({
{ excludeMetaMetricsId: false },
);
await setCorrectChain();
if (chainId !== currentChainId) {
// TODO: Properly handle this case, for now we are stopping execution to prevent users from inadvertendly navigating to incompatible chains
console.error(
`Token/Network mismatch token: ${chainId} network: ${currentChainId}`,
);
return;
}
await dispatch(startNewDraftTransaction({ type: AssetType.native }));
history.push(SEND_ROUTE);
}
}
}, [chainId, account, setCorrectChain]);

const handleSwapOnClick = useCallback(async () => {
console.log('swap');
await setCorrectChain();

if (chainId !== currentChainId) {
// TODO: Properly handle this case, for now we are stopping execution to prevent users from inadvertendly navigating to incompatible chains
console.error(
`Token/Network mismatch token: ${chainId} network: ${currentChainId}`,
);
return;
}
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
global.platform.openTab({
url: `${mmiPortfolioUrl}/swap`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = `
</button>
<button
class="icon-button token-overview__button"
data-testid="prepare-swap"
>
<div
class="icon-button__circle"
Expand Down Expand Up @@ -967,6 +968,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `
</button>
<button
class="icon-button token-overview__button"
data-testid="prepare-swap"
>
<div
class="icon-button__circle"
Expand Down
Loading
Loading