Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import type { TrendingAsset } from '@metamask/assets-controllers';
import TrendingQuickBuy from './TrendingQuickBuy';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { SocialLeaderboardEventProperties } from '../../../../Views/SocialLeaderboard/analytics';

const mockTrack = jest.fn();
jest.mock(
'../../../../Views/SocialLeaderboard/analytics/useSocialLeaderboardAnalytics',
() => ({
useSocialLeaderboardAnalytics: () => ({ track: mockTrack }),
}),
);

const mockQuickBuyRoot = jest.fn();
jest.mock(
'../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy',
() => ({
QuickBuy: {
Root: (props: Record<string, unknown>) => {
mockQuickBuyRoot(props);
return null;
},
},
}),
);

jest.mock(
'../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features',
() => ({
TOP_TRADERS_QUICK_BUY_FEATURES: { tradeModes: ['buy', 'sell'] },
}),
);

// ─── Helpers ─────────────────────────────────────────────────────────────────

const makeToken = (overrides: Partial<TrendingAsset> = {}): TrendingAsset => ({
assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
price: '1.00',
marketCap: 75_000_000_000,
aggregatedUsdVolume: 900_000_000,
...overrides,
});

describe('TrendingQuickBuy', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders QuickBuy.Root with isVisible=false when token is null', () => {
render(<TrendingQuickBuy token={null} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ isVisible: false }),
);
});

it('renders QuickBuy.Root with isVisible=true when token is provided', () => {
render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ isVisible: true }),
);
});

it('maps an ERC-20 TrendingAsset to the correct QuickBuyTarget', () => {
const token = makeToken({
assetId: 'eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
name: 'Synthetix',
});

render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
target: {
chain: 'eip155:1',
tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
tokenSymbol: 'SNX',
tokenName: 'Synthetix',
},
}),
);
});

it('maps a native (slip44) TrendingAsset to the zero address', () => {
const token = makeToken({
assetId: 'eip155:1/slip44:60',
symbol: 'ETH',
name: 'Ethereum',
});

render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
tokenAddress: '0x0000000000000000000000000000000000000000',
tokenSymbol: 'ETH',
}),
}),
);
});

it('passes analyticsContext with source=explore_search to QuickBuy.Root', () => {
render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
analyticsContext: expect.objectContaining({ source: 'explore_search' }),
}),
);
});

it('fires SOCIAL_QUICK_BUY_SHEET_VIEWED when token transitions from null to non-null', () => {
const token = makeToken();
render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(1);
expect(mockTrack).toHaveBeenCalledWith(
MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED,
expect.objectContaining({
[SocialLeaderboardEventProperties.SOURCE]: 'explore_search',
[SocialLeaderboardEventProperties.CAIP19]: token.assetId,
[SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap,
}),
);
});

it('does not fire SOCIAL_QUICK_BUY_SHEET_VIEWED when token is null', () => {
render(<TrendingQuickBuy token={null} onClose={jest.fn()} />);
expect(mockTrack).not.toHaveBeenCalled();
});

it('does not re-fire the event while the same token stays visible', () => {
const token = makeToken();
const { rerender } = render(
<TrendingQuickBuy token={token} onClose={jest.fn()} />,
);

// Simulate a re-render with the same token (e.g. parent re-renders)
rerender(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(1);
});

it('fires the event again when a new token is opened after closing', () => {
const tokenA = makeToken({
assetId: 'eip155:1/erc20:0xaaa',
symbol: 'AAA',
name: 'Token A',
});
const tokenB = makeToken({
assetId: 'eip155:1/erc20:0xbbb',
symbol: 'BBB',
name: 'Token B',
});

const { rerender } = render(
<TrendingQuickBuy token={tokenA} onClose={jest.fn()} />,
);
// Close sheet
rerender(<TrendingQuickBuy token={null} onClose={jest.fn()} />);
// Open with a new token
rerender(<TrendingQuickBuy token={tokenB} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(2);
expect(mockTrack).toHaveBeenNthCalledWith(
2,
MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED,
expect.objectContaining({
[SocialLeaderboardEventProperties.CAIP19]: tokenB.assetId,
}),
);
});

it('forwards onClose to QuickBuy.Root', () => {
const onClose = jest.fn();
render(<TrendingQuickBuy token={makeToken()} onClose={onClose} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ onClose }),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { TrendingAsset } from '@metamask/assets-controllers';
import React, { useEffect, useMemo, useRef } from 'react';
import { QuickBuy } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy';
import { TOP_TRADERS_QUICK_BUY_FEATURES } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features';
import type { QuickBuyTarget } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/types';
import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../../../../constants/bridge';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import {
useSocialLeaderboardAnalytics,
SocialLeaderboardEventProperties,
} from '../../../../Views/SocialLeaderboard/analytics';

interface TrendingQuickBuyProps {
token: TrendingAsset | null;
onClose: () => void;
}

const TrendingQuickBuy: React.FC<TrendingQuickBuyProps> = ({
token,
onClose,
}) => {
const { track } = useSocialLeaderboardAnalytics();
const prevTokenRef = useRef<TrendingAsset | null>(null);

const target = useMemo((): QuickBuyTarget | null => {
if (!token) return null;
const [caipChainId, assetIdentifier] = token.assetId.split('/');
if (!caipChainId) return null;
const isNative = assetIdentifier?.startsWith('slip44:');
const tokenAddress = isNative
? NATIVE_SWAPS_TOKEN_ADDRESS
: (assetIdentifier?.split(':')[1] ?? '');
return {
chain: caipChainId as QuickBuyTarget['chain'],
tokenAddress,
tokenSymbol: token.symbol ?? '',
tokenName: token.name ?? '',
};
}, [token]);

useEffect(() => {
if (token && !prevTokenRef.current) {
track(MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, {
[SocialLeaderboardEventProperties.SOURCE]: 'explore_search',
[SocialLeaderboardEventProperties.CAIP19]: token.assetId,
...(typeof token.marketCap === 'number'
? { [SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap }
: {}),
});
}
prevTokenRef.current = token;
Comment thread
salimtb marked this conversation as resolved.
}, [token, track]);

return (
<QuickBuy.Root
isVisible={!!token}
target={target}
onClose={onClose}
features={TOP_TRADERS_QUICK_BUY_FEATURES}
analyticsContext={{ source: 'explore_search' }}
/>
);
};

export default TrendingQuickBuy;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../../util/theme/models';

const styleSheet = (_params: { theme: Theme }) =>
StyleSheet.create({
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
return StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
Expand Down Expand Up @@ -38,6 +39,16 @@ const styleSheet = (_params: { theme: Theme }) =>
stockBadgeWrapper: {
marginTop: 4,
},
quickTradeButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: theme.colors.background.muted,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
},
});
};

export default styleSheet;
Original file line number Diff line number Diff line change
Expand Up @@ -1815,4 +1815,65 @@ describe('TrendingTokenRowItem', () => {
expect(queryByText('Malicious')).toBeNull();
});
});

describe('Quick Trade button (onQuickTrade)', () => {
it('does not render the quick trade button when onQuickTrade is not provided', () => {
const token = createMockToken();

const { queryByTestId } = renderWithProvider(
<TrendingTokenRowItem token={token} />,
{ state: mockState },
false,
);

expect(queryByTestId('quick-trade-button')).toBeNull();
});

it('renders the quick trade button when onQuickTrade is provided', () => {
const token = createMockToken();
const onQuickTrade = jest.fn();

const { getByTestId } = renderWithProvider(
<TrendingTokenRowItem token={token} onQuickTrade={onQuickTrade} />,
{ state: mockState },
false,
);

expect(getByTestId('quick-trade-button')).toBeOnTheScreen();
});

it('calls onQuickTrade with the token when the button is pressed', () => {
const token = createMockToken();
const onQuickTrade = jest.fn();

const { getByTestId } = renderWithProvider(
<TrendingTokenRowItem token={token} onQuickTrade={onQuickTrade} />,
{ state: mockState },
false,
);

fireEvent.press(getByTestId('quick-trade-button'));

expect(onQuickTrade).toHaveBeenCalledTimes(1);
expect(onQuickTrade).toHaveBeenCalledWith(token);
});

it('does not trigger row navigation when the quick trade button is pressed', async () => {
const token = createMockToken();
const onQuickTrade = jest.fn();

const { getByTestId } = renderWithProvider(
<TrendingTokenRowItem token={token} onQuickTrade={onQuickTrade} />,
{ state: mockState },
false,
);

fireEvent.press(getByTestId('quick-trade-button'));

await waitFor(() => {
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockDispatch).not.toHaveBeenCalled();
});
});
});
});
Loading
Loading