Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

This file was deleted.

1 change: 1 addition & 0 deletions app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export const mockBridgeReducerState: BridgeState = {
isGasIncluded7702Supported: false,
bridgeViewMode: BridgeViewMode.Bridge,
isSelectingRecipient: false,
isDestTokenManuallySet: false,
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Routes from '../../../../../constants/navigation/Routes';
import {
selectBridgeViewMode,
setDestToken,
setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { cloneDeep } from 'lodash';
import { BridgeViewMode } from '../../types';
Expand Down Expand Up @@ -42,6 +43,7 @@ jest.mock('../../../../../core/redux/slices/bridge', () => {
...actual,
default: actual.default,
setDestToken: jest.fn(actual.setDestToken),
setIsDestTokenManuallySet: jest.fn(actual.setIsDestTokenManuallySet),
selectBridgeViewMode: jest.fn().mockReturnValue('Bridge'),
};
});
Expand Down Expand Up @@ -304,7 +306,8 @@ describe('BridgeDestTokenSelector', () => {

// TODO: Fix flaky test - timing issue with debounced token selection (500ms)
// Test fails intermittently due to race condition between waitFor and debounce
it.skip('handles token selection correctly', async () => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('handles token selection correctly and marks dest token as manually set', async () => {
// Arrange
const { getByText } = renderScreen(
BridgeDestTokenSelector,
Expand All @@ -325,7 +328,7 @@ describe('BridgeDestTokenSelector', () => {
jest.advanceTimersByTime(500);
});

// Assert - check that actions were called
// Assert - check that setDestToken was called with the selected token
expect(setDestToken).toHaveBeenCalledWith(
expect.objectContaining({
address: ethToken2Address,
Expand All @@ -337,6 +340,8 @@ describe('BridgeDestTokenSelector', () => {
symbol: 'HELLO',
}),
);
// Also verify the manual flag was set
expect(setIsDestTokenManuallySet).toHaveBeenCalledWith(true);
expect(mockGoBack).toHaveBeenCalled();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
selectSelectedDestChainId,
selectSourceToken,
setDestToken,
setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { getNetworkImageSource } from '../../../../../util/networks';
import { TokenSelectorItem } from '../TokenSelectorItem';
Expand Down Expand Up @@ -78,7 +79,9 @@ export const BridgeDestTokenSelector: React.FC = React.memo(() => {

const handleTokenPress = useCallback(
(token: BridgeToken) => {
// Mark as manually set to prevent auto-updating dest when source chain changes
dispatch(setDestToken(token));
dispatch(setIsDestTokenManuallySet(true));
navigation.goBack();
},
[dispatch, navigation],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { BridgeSourceNetworkSelector } from '.';
import Routes from '../../../../../constants/navigation/Routes';
import { strings } from '../../../../../../locales/i18n';
import { Hex } from '@metamask/utils';
import { setSelectedSourceChainIds } from '../../../../../core/redux/slices/bridge';
import {
setSelectedSourceChainIds,
setSourceToken,
} from '../../../../../core/redux/slices/bridge';
import { BridgeSourceNetworkSelectorSelectorsIDs } from '../../../../../../e2e/selectors/Bridge/BridgeSourceNetworkSelector.selectors';
import { cloneDeep } from 'lodash';
import { MultichainNetwork } from '@metamask/multichain-transactions-controller';
Expand Down Expand Up @@ -35,19 +38,34 @@ jest.mock('../../../../../core/redux/slices/bridge', () => {
};
});

const mockOnSetRpcTarget = jest.fn().mockResolvedValue(undefined);
const mockOnNonEvmNetworkChange = jest.fn().mockResolvedValue(undefined);

jest.mock('../../../../Views/NetworkSelector/useSwitchNetworks', () => ({
useSwitchNetworks: jest.fn(() => ({
onSetRpcTarget: jest.fn().mockResolvedValue(undefined),
onSetRpcTarget: mockOnSetRpcTarget,
onNetworkChange: jest.fn(),
onNonEvmNetworkChange: mockOnNonEvmNetworkChange,
})),
}));

const mockAutoUpdateDestToken = jest.fn();

jest.mock('../../hooks/useAutoUpdateDestToken', () => ({
useAutoUpdateDestToken: () => ({
autoUpdateDestToken: mockAutoUpdateDestToken,
}),
}));

describe('BridgeSourceNetworkSelector', () => {
const mockChainId = '0x1' as Hex;
const optimismChainId = '0xa' as Hex;

beforeEach(() => {
jest.clearAllMocks();
mockOnSetRpcTarget.mockResolvedValue(undefined);
mockOnNonEvmNetworkChange.mockResolvedValue(undefined);
mockAutoUpdateDestToken.mockClear();
});

it('renders with initial state and displays networks', async () => {
Expand Down Expand Up @@ -223,6 +241,183 @@ describe('BridgeSourceNetworkSelector', () => {
expect(applyButton.props.disabled).toBe(true);
});

describe('handleApply', () => {
it('calls onApply callback when provided instead of dispatching actions', async () => {
const mockOnApply = jest.fn();
const { getAllByTestId, getByText } = renderScreen(
() => <BridgeSourceNetworkSelector onApply={mockOnApply} />,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Uncheck Ethereum to have only Optimism selected
const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
fireEvent.press(ethereumCheckbox);

// Click Apply button
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(mockOnApply).toHaveBeenCalledWith([
optimismChainId,
SolScope.Mainnet,
BtcScope.Mainnet,
TrxScope.Mainnet,
]);
expect(mockGoBack).not.toHaveBeenCalled();
expect(setSelectedSourceChainIds).not.toHaveBeenCalled();
});
});

it('sets source token to native token when single EVM network is selected', async () => {
const { getAllByTestId, getByText } = renderScreen(
BridgeSourceNetworkSelector,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Deselect all first
const deselectAllButton = getByText('Deselect all');
fireEvent.press(deselectAllButton);

// Select only Ethereum
const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
fireEvent.press(ethereumCheckbox);

// Click Apply
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(setSourceToken).toHaveBeenCalledWith(
expect.objectContaining({
chainId: mockChainId,
symbol: 'ETH',
}),
);
});
});

it('calls autoUpdateDestToken when single network is selected', async () => {
const { getAllByTestId, getByText } = renderScreen(
BridgeSourceNetworkSelector,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Deselect all first
const deselectAllButton = getByText('Deselect all');
fireEvent.press(deselectAllButton);

// Select only Optimism
const optimismCheckbox = getAllByTestId(`checkbox-${optimismChainId}`)[0];
fireEvent.press(optimismCheckbox);

// Click Apply
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(mockAutoUpdateDestToken).toHaveBeenCalledWith(
expect.objectContaining({
chainId: optimismChainId,
}),
);
});
});

it('calls onSetRpcTarget when single EVM network is selected', async () => {
const { getAllByTestId, getByText } = renderScreen(
BridgeSourceNetworkSelector,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Deselect all first
const deselectAllButton = getByText('Deselect all');
fireEvent.press(deselectAllButton);

// Select only Ethereum
const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
fireEvent.press(ethereumCheckbox);

// Click Apply
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(mockOnSetRpcTarget).toHaveBeenCalledWith(
expect.objectContaining({
chainId: mockChainId,
}),
);
});
});

it('calls onNonEvmNetworkChange when single non-EVM network is selected', async () => {
const { getAllByTestId, getByText } = renderScreen(
BridgeSourceNetworkSelector,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Deselect all first
const deselectAllButton = getByText('Deselect all');
fireEvent.press(deselectAllButton);

// Select only Solana
const solanaCheckbox = getAllByTestId(`checkbox-${SolScope.Mainnet}`)[0];
fireEvent.press(solanaCheckbox);

// Click Apply
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(mockOnNonEvmNetworkChange).toHaveBeenCalledWith(
SolScope.Mainnet,
);
expect(mockOnSetRpcTarget).not.toHaveBeenCalled();
});
});

it('does not set source token or switch network when multiple networks are selected', async () => {
const { getByText } = renderScreen(
BridgeSourceNetworkSelector,
{
name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
},
{ state: initialState },
);

// Keep all networks selected and click Apply
const applyButton = getByText('Apply');
fireEvent.press(applyButton);

await waitFor(() => {
expect(setSelectedSourceChainIds).toHaveBeenCalled();
expect(mockGoBack).toHaveBeenCalled();
});

// These should NOT be called when multiple networks are selected
expect(setSourceToken).not.toHaveBeenCalled();
expect(mockAutoUpdateDestToken).not.toHaveBeenCalled();
expect(mockOnSetRpcTarget).not.toHaveBeenCalled();
expect(mockOnNonEvmNetworkChange).not.toHaveBeenCalled();
});
});

it('networks should be sorted by fiat value in descending order', async () => {
const { getAllByTestId } = renderScreen(
BridgeSourceNetworkSelector,
Expand Down
Loading
Loading