Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
66 changes: 65 additions & 1 deletion app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { Hex } from '@metamask/utils';
import BridgeView from '.';
import type { BridgeRouteParams } from '../../hooks/useSwapBridgeNavigation';
import { createBridgeTestState } from '../../testUtils';
import { RequestStatus, type QuoteResponse } from '@metamask/bridge-controller';
import {
MetaMetricsSwapsEventSource,
RequestStatus,
type QuoteResponse,
} from '@metamask/bridge-controller';
import { SolScope } from '@metamask/keyring-api';
import { mockUseBridgeQuoteData } from '../../_mocks_/useBridgeQuoteData.mock';
import { useBridgeQuoteData } from '../../hooks/useBridgeQuoteData';
Expand All @@ -24,6 +28,7 @@ import { isHardwareAccount } from '../../../../../util/address';
import { MOCK_ENTROPY_SOURCE as mockEntropySource } from '../../../../../util/test/keyringControllerTestUtils';
import { RootState } from '../../../../../reducers';
import { mockQuoteWithMetadata } from '../../_mocks_/bridgeQuoteWithMetadata';
import { BridgeViewSelectorsIDs } from './BridgeView.testIds';

// Mock the account-tree-controller file that imports the problematic module
jest.mock(
Expand Down Expand Up @@ -1374,6 +1379,65 @@ describe('BridgeView', () => {
});
});

describe('location forwarding', () => {
it('forwards route.params.location to SwapsConfirmButton via price impact modal navigation', async () => {
mockRoute.params = {
sourcePage: 'test',
location: MetaMetricsSwapsEventSource.MainView,
} as BridgeRouteParams;

// A priceImpact above the error threshold (25) causes handleContinue to
// navigate to the PriceImpactModal — the location value is embedded in
// the navigation params, making this the easiest observable side-effect
// to assert for location forwarding.
jest
.mocked(useBridgeQuoteData as unknown as jest.Mock)
.mockImplementation(() => ({
...mockUseBridgeQuoteData,
activeQuote: mockQuoteWithMetadata,
formattedQuoteData: {
...mockUseBridgeQuoteData.formattedQuoteData,
priceImpact: '30%',
},
}));

const testState = createBridgeTestState(
{
bridgeControllerOverrides: {
quotesLoadingStatus: RequestStatus.FETCHED,
quotes: [mockQuoteWithMetadata as unknown as QuoteResponse],
quotesLastFetched: Date.now(),
},
bridgeReducerOverrides: {
sourceAmount: '1.0',
},
},
mockState,
);

const { getByTestId } = renderScreen(
BridgeView,
{ name: Routes.BRIDGE.ROOT },
{ state: testState },
);

await act(async () => {
fireEvent.press(getByTestId(BridgeViewSelectorsIDs.CONFIRM_BUTTON));
});

await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith(
Routes.BRIDGE.MODALS.ROOT,
expect.objectContaining({
params: expect.objectContaining({
location: MetaMetricsSwapsEventSource.MainView,
}),
}),
);
});
});
});

describe('gas included support hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
10 changes: 9 additions & 1 deletion app/components/UI/Bridge/Views/BridgeView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ const BridgeView = () => {
const isSolanaSourced = useSelector(selectIsSolanaSourced);
const isDestNetworkEnabled = useIsNetworkEnabled(destToken?.chainId);

/** The entry point location for analytics (e.g. Main View, Token View, Trending Explore) */
const location = route.params?.location;

// inputRef is used to programmatically blur the input field after a delay
// This gives users time to type before the keyboard disappears
// The ref is typed to only expose the blur method we need
Expand Down Expand Up @@ -380,7 +383,10 @@ const BridgeView = () => {
/>
)}

<SwapsConfirmButton latestSourceBalance={latestSourceBalance} />
<SwapsConfirmButton
location={location}
latestSourceBalance={latestSourceBalance}
/>
<Box flexDirection={FlexDirection.Row} alignItems={AlignItems.center}>
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
{hasFee
Expand Down Expand Up @@ -491,6 +497,7 @@ const BridgeView = () => {
{shouldDisplayQuoteDetails && (
<Box style={styles.quoteContainer}>
<QuoteDetailsCard
location={location}
hasInsufficientBalance={hasInsufficientBalance}
/>
</Box>
Expand All @@ -509,6 +516,7 @@ const BridgeView = () => {
>
{sourceAmount && sourceAmount !== '0' ? (
<SwapsConfirmButton
location={location}
latestSourceBalance={latestSourceBalance}
testID={BridgeViewSelectorsIDs.CONFIRM_BUTTON_KEYPAD}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { PriceImpactDescription } from './PriceImpactDescription';
import { PriceImpactModalType } from './constants';
import { strings } from '../../../../../../locales/i18n';

describe('PriceImpactDescription', () => {
describe('Execution type', () => {
it('renders the execution description with the given priceImpact', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Execution}
priceImpact="-30%"
/>,
);

expect(
getByText(
strings('bridge.price_impact_execution_description', {
priceImpact: '-30%',
}),
),
).toBeTruthy();
});

it('renders the execution description with "0" when priceImpact is undefined', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Execution}
priceImpact={undefined}
/>,
);

expect(
getByText(
strings('bridge.price_impact_execution_description', {
priceImpact: '0',
}),
),
).toBeTruthy();
});

it('does not render the info description', () => {
const { queryByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Execution}
priceImpact="-30%"
/>,
);

expect(
queryByText(strings('bridge.price_impact_info_description')),
).toBeNull();
});
});

describe('Info type — with priceImpact (warning state)', () => {
it('renders the warning description with the given priceImpact', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact="-10%"
/>,
);

expect(
getByText(
strings('bridge.price_impact_warning_description', {
priceImpact: '-10%',
}),
),
).toBeTruthy();
});

it('does not render the info description when priceImpact is provided', () => {
const { queryByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact="-10%"
/>,
);

expect(
queryByText(strings('bridge.price_impact_info_description')),
).toBeNull();
});

it('treats the string "0" as a truthy priceImpact and renders the warning description', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact="0"
/>,
);

expect(
getByText(
strings('bridge.price_impact_warning_description', {
priceImpact: '0',
}),
),
).toBeTruthy();
});
});

describe('Info type — without priceImpact (info state)', () => {
it('renders the info description when priceImpact is undefined', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact={undefined}
/>,
);

expect(
getByText(strings('bridge.price_impact_info_description')),
).toBeTruthy();
});

it('renders the info description when priceImpact is an empty string', () => {
const { getByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact=""
/>,
);

expect(
getByText(strings('bridge.price_impact_info_description')),
).toBeTruthy();
});

it('does not render the warning description when priceImpact is absent', () => {
const { queryByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Info}
priceImpact={undefined}
/>,
);

expect(
queryByText(
strings('bridge.price_impact_warning_description', {
priceImpact: undefined,
}),
),
).toBeNull();
});
});

describe('priority — Execution type takes precedence over warning state', () => {
it('renders the execution description rather than the warning description when type is Execution and priceImpact is provided', () => {
const { getByText, queryByText } = render(
<PriceImpactDescription
type={PriceImpactModalType.Execution}
priceImpact="-10%"
/>,
);

expect(
getByText(
strings('bridge.price_impact_execution_description', {
priceImpact: '-10%',
}),
),
).toBeTruthy();

expect(
queryByText(
strings('bridge.price_impact_warning_description', {
priceImpact: '-10%',
}),
),
).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useMemo } from 'react';
import { strings } from '../../../../../../locales/i18n';
import { Box, Text, TextColor } from '@metamask/design-system-react-native';
import { PriceImpactModalType } from './constants';

interface PriceImpactDescriptionProps {
type: PriceImpactModalType;
priceImpact?: string;
}

export function PriceImpactDescription({
type,
priceImpact,
}: PriceImpactDescriptionProps) {
const isWarning = Boolean(priceImpact);

const body = useMemo(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using memoizing callbacks for logic that's not expensive. For example, this one can simply become:

let body = strings('bridge.price_impact_info_description');

if (type === PriceImpactModalType.Execution) {
  body = strings('bridge.price_impact_execution_description', {
    priceImpact: priceImpact ?? '0',
  });
} else if (isWarning) {
  body =  strings('bridge.price_impact_warning_description', {
    priceImpact: priceImpact ?? '0',
  });
}

if (type === PriceImpactModalType.Execution) {
return strings('bridge.price_impact_execution_description', {
priceImpact: priceImpact ?? '0',
});
}
if (isWarning) {
return strings('bridge.price_impact_warning_description', {
priceImpact: priceImpact ?? '0',
});
}
return strings('bridge.price_impact_info_description');
}, [type, priceImpact, isWarning]);

return (
<Box paddingHorizontal={4} paddingVertical={2}>
<Text color={TextColor.TextAlternative}>{body}</Text>
</Box>
);
}
Loading
Loading