Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Expand Up @@ -31,6 +31,7 @@ import ErrorState from '../Homepage/components/ErrorState/ErrorState';
import WhatsHappeningExpandedCard from './components/WhatsHappeningExpandedCard';
import WhatsHappeningSourcesBottomSheet from './components/WhatsHappeningSourcesBottomSheet';
import PageIndicator from './components/PageIndicator';
import { PerpsStreamProvider } from '../../UI/Perps/providers/PerpsStreamManager';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics';

Expand Down Expand Up @@ -210,64 +211,66 @@ const WhatsHappeningDetailView = () => {
<Box twClassName="w-10" />
</Box>

<Box twClassName="flex-1">
{isLoading ? (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw.style('px-4 gap-3 items-stretch')}
testID="whats-happening-detail-skeleton"
>
{SKELETON_KEYS.map((key) => (
<WhatsHappeningCardSkeleton key={key} />
))}
</ScrollView>
) : hasError ? (
<ErrorState
title={strings('homepage.error.unable_to_load', {
section: strings(
'homepage.sections.whats_happening',
).toLowerCase(),
})}
onRetry={refresh}
/>
) : (
<>
<PerpsStreamProvider>
<Box twClassName="flex-1">
{isLoading ? (
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={SNAP_INTERVAL}
snapToAlignment="start"
style={tw`flex-1`}
contentContainerStyle={tw.style('px-4 gap-3')}
onLayout={handleCarouselLayout}
onContentSizeChange={handleContentSizeChange}
onScroll={handleScroll}
scrollEventThrottle={16}
onMomentumScrollEnd={handleScrollEnd}
testID="whats-happening-detail-carousel"
contentContainerStyle={tw.style('px-4 gap-3 items-stretch')}
testID="whats-happening-detail-skeleton"
>
{cardHeight > 0 &&
items.map((item, index) => (
<WhatsHappeningExpandedCard
key={item.id}
item={item}
cardIndex={index}
cardWidth={CARD_WIDTH}
cardHeight={cardHeight}
onSourcesPress={(articles) =>
handleSourcesPress(articles, item, index)
}
/>
))}
{SKELETON_KEYS.map((key) => (
<WhatsHappeningCardSkeleton key={key} />
))}
</ScrollView>
) : hasError ? (
<ErrorState
title={strings('homepage.error.unable_to_load', {
section: strings(
'homepage.sections.whats_happening',
).toLowerCase(),
})}
onRetry={refresh}
/>
) : (
<>
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={SNAP_INTERVAL}
snapToAlignment="start"
style={tw`flex-1`}
contentContainerStyle={tw.style('px-4 gap-3')}
onLayout={handleCarouselLayout}
onContentSizeChange={handleContentSizeChange}
onScroll={handleScroll}
scrollEventThrottle={16}
onMomentumScrollEnd={handleScrollEnd}
testID="whats-happening-detail-carousel"
>
{cardHeight > 0 &&
items.map((item, index) => (
<WhatsHappeningExpandedCard
key={item.id}
item={item}
cardIndex={index}
cardWidth={CARD_WIDTH}
cardHeight={cardHeight}
onSourcesPress={(articles) =>
handleSourcesPress(articles, item, index)
}
/>
))}
</ScrollView>

<PageIndicator count={items.length} activeIndex={currentIndex} />
</>
)}
</Box>
<PageIndicator count={items.length} activeIndex={currentIndex} />
</>
)}
</Box>
</PerpsStreamProvider>
{sourcesContext && (
<WhatsHappeningSourcesBottomSheet
onClose={handleSourcesClose}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react-native';
import { TextColor } from '@metamask/design-system-react-native';
import type { RelatedAsset } from '@metamask/ai-controllers';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import AssetRow from './AssetRow';

jest.mock('../utils/getRelatedAssetImageSource', () => ({
getRelatedAssetImageSource: jest.fn(() => undefined),
}));

jest.mock(
'../../../UI/Tokens/components/TokenListSecurityBadge/TokenListSecurityBadge',
() => 'TokenListSecurityBadge',
);

const btcAsset: RelatedAsset = {
sourceAssetId: 'bitcoin',
symbol: 'BTC',
name: 'Bitcoin',
caip19: ['eip155:1/slip44:0'],
};

const symbolOnlyAsset: RelatedAsset = {
sourceAssetId: 'unknown',
symbol: 'UNK',
name: '',
caip19: [],
};

describe('AssetRow', () => {
const onAction = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('displays asset.name when name is present', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
/>,
);
expect(screen.getByText('Bitcoin')).toBeOnTheScreen();
});

it('falls back to asset.symbol when name is empty', () => {
renderWithProvider(
<AssetRow
asset={symbolOnlyAsset}
actionLabel="Buy"
accessibilityLabel="Buy UNK"
onAction={onAction}
/>,
);
expect(screen.getByText('UNK')).toBeOnTheScreen();
});

it('does not render an action button when onAction is omitted', () => {
renderWithProvider(<AssetRow asset={btcAsset} />);
expect(screen.queryByText('Buy')).toBeNull();
expect(screen.queryByText('Trade')).toBeNull();
});

it('renders the action button with the provided label', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Trade"
accessibilityLabel="Trade BTC"
onAction={onAction}
/>,
);
expect(screen.getByText('Trade')).toBeOnTheScreen();
});

it('calls onAction when the button is pressed', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
/>,
);
fireEvent.press(screen.getByText('Buy'));
expect(onAction).toHaveBeenCalledTimes(1);
});

it('renders the TokenListSecurityBadge when caipAssetId is provided', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
caipAssetId="eip155:1/slip44:0"
/>,
);
expect(screen.getByTestId !== undefined).toBeTruthy();
Comment thread
zone-live marked this conversation as resolved.
Outdated
// The mocked component renders as 'TokenListSecurityBadge' native element
const badge = screen.UNSAFE_getByType(
'TokenListSecurityBadge' as unknown as React.ComponentType,
);
expect(badge).toBeTruthy();
expect(badge.props.caipAssetId).toBe('eip155:1/slip44:0');
});

it('does not render the security badge when caipAssetId is not provided', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
/>,
);
expect(
screen.UNSAFE_queryByType(
'TokenListSecurityBadge' as unknown as React.ComponentType,
),
).toBeNull();
});

it('renders the price secondary line when secondaryLine is provided', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
secondaryLine={{
priceText: '$95,000.00',
changeText: '+2.50%',
changeColor: TextColor.SuccessDefault,
}}
/>,
);
expect(screen.getByText('$95,000.00')).toBeOnTheScreen();
expect(screen.getByText('+2.50%')).toBeOnTheScreen();
});

it('renders just the price without change text when changeText is undefined', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
secondaryLine={{
priceText: '$95,000.00',
changeText: undefined,
changeColor: TextColor.TextAlternative,
}}
/>,
);
expect(screen.getByText('$95,000.00')).toBeOnTheScreen();
expect(screen.queryByText('%')).toBeNull();
});

it('does not render secondary line when secondaryLine is not provided', () => {
renderWithProvider(
<AssetRow
asset={btcAsset}
actionLabel="Buy"
accessibilityLabel="Buy BTC"
onAction={onAction}
/>,
);
expect(screen.queryByText('$')).toBeNull();
});
});
Loading
Loading