Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -3,8 +3,16 @@ import { screen, fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import WhatsHappeningSection from './WhatsHappeningSection';
import Routes from '../../../../../constants/navigation/Routes';
import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events';

const mockNavigate = jest.fn();
const mockTrackEvent = jest.fn();
const mockCreateEventBuilder = jest.fn((eventName: string) => ({
addProperties: jest.fn((properties: Record<string, unknown>) => ({
build: jest.fn(() => ({ category: eventName, properties })),
})),
build: jest.fn(() => ({ category: eventName })),
}));

jest.mock('@react-navigation/native', () => {
const actual = jest.requireActual('@react-navigation/native');
Expand All @@ -30,6 +38,13 @@ jest.mock('./hooks', () => ({
})),
}));

jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
useAnalytics: () => ({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
}),
}));

const mockUseWhatsHappening = jest.requireMock('./hooks').useWhatsHappening;
const mockSelectWhatsHappeningEnabled = jest.requireMock(
'../../../../../selectors/featureFlagController/whatsHappening',
Expand Down Expand Up @@ -175,4 +190,50 @@ describe('WhatsHappeningSection', () => {
initialIndex: 1,
});
});

it('tracks Whats Happening Opened with entry_point=card when a card is pressed', () => {
mockUseWhatsHappening.mockReturnValue({
items: [mockItem],
isLoading: false,
error: null,
refresh: jest.fn(),
});
renderWithProvider(<WhatsHappeningSection {...defaultProps} />);
fireEvent.press(screen.getByText(mockItem.title));
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
MetaMetricsEvents.WHATS_HAPPENING_OPENED,
);
expect(mockTrackEvent).toHaveBeenCalledWith(
expect.objectContaining({
category: MetaMetricsEvents.WHATS_HAPPENING_OPENED,
properties: expect.objectContaining({
entry_point: 'card',
event_id: mockItem.id,
card_index: 0,
category: 'macro',
impact: 'positive',
asset_symbols: [],
}),
}),
);
});

it('tracks Whats Happening Opened with entry_point=view_all when View More is pressed', () => {
mockUseWhatsHappening.mockReturnValue({
items: [mockItem],
isLoading: false,
error: null,
refresh: jest.fn(),
});
renderWithProvider(<WhatsHappeningSection {...defaultProps} />);
fireEvent.press(screen.getByText(/view more/i));
expect(mockTrackEvent).toHaveBeenCalledWith(
expect.objectContaining({
category: MetaMetricsEvents.WHATS_HAPPENING_OPENED,
properties: expect.objectContaining({
entry_point: 'view_all',
}),
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ import { SectionRefreshHandle } from '../../types';
import { selectWhatsHappeningEnabled } from '../../../../../selectors/featureFlagController/whatsHappening';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';
import { MAX_ITEMS_DISPLAYED } from './constants';
import { MAX_ITEMS_DISPLAYED, WhatsHappeningEntryPoint } from './constants';
import { useWhatsHappening } from './hooks';
import { WhatsHappeningCard, WhatsHappeningCardSkeleton } from './components';
import useHomeViewedEvent, {
HomeSectionNames,
} from '../../hooks/useHomeViewedEvent';
import { useSectionPerformance } from '../../hooks/useSectionPerformance';
import { WalletViewSelectorsIDs } from '../../../Wallet/WalletView.testIds';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
import { getWhatsHappeningEventProps } from './eventProperties';

const CARD_WIDTH = 280;
const GAP = 12;
Expand Down Expand Up @@ -56,6 +59,7 @@ const WhatsHappeningSection = forwardRef<
const navigation = useNavigation();
const isEnabled = useSelector(selectWhatsHappeningEnabled);
const title = strings('homepage.sections.whats_happening');
const { trackEvent, createEventBuilder } = useAnalytics();

const { items, isLoading, error, refresh } =
useWhatsHappening(MAX_ITEMS_DISPLAYED);
Expand Down Expand Up @@ -98,14 +102,30 @@ const WhatsHappeningSection = forwardRef<
);

const handleViewAll = useCallback(() => {
trackEvent(
createEventBuilder(MetaMetricsEvents.WHATS_HAPPENING_OPENED)
.addProperties({ entry_point: WhatsHappeningEntryPoint.ViewAll })
.build(),
);
navigateToDetail(0);
}, [navigateToDetail]);
}, [navigateToDetail, trackEvent, createEventBuilder]);

const handleCardPress = useCallback(
(index: number) => {
const item = items[index];
if (item) {
trackEvent(
createEventBuilder(MetaMetricsEvents.WHATS_HAPPENING_OPENED)
.addProperties({
...getWhatsHappeningEventProps(item, index),
entry_point: WhatsHappeningEntryPoint.Card,
})
.build(),
);
}
navigateToDetail(index);
},
[navigateToDetail],
[items, navigateToDetail, trackEvent, createEventBuilder],
);

if (!isEnabled) {
Expand Down Expand Up @@ -161,6 +181,7 @@ const WhatsHappeningSection = forwardRef<
<WhatsHappeningCard
key={item.id}
item={item}
cardIndex={index}
onPress={() => handleCardPress(index)}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ import { screen, fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import WhatsHappeningCard from './WhatsHappeningCard';
import type { WhatsHappeningItem } from '../types';
import { MetaMetricsEvents } from '../../../../../../core/Analytics/MetaMetrics.events';

const mockTrackEvent = jest.fn();
const mockCreateEventBuilder = jest.fn((eventName: string) => ({
addProperties: jest.fn((properties: Record<string, unknown>) => ({
build: jest.fn(() => ({ category: eventName, properties })),
})),
build: jest.fn(() => ({ category: eventName })),
}));

let capturedOnVisible: (() => void) | null = null;
jest.mock('../../../../../UI/MarketInsights/hooks/useViewportTracking', () => ({
useViewportTracking: (onVisible: () => void) => {
capturedOnVisible = onVisible;
return { ref: { current: null }, onLayout: jest.fn() };
},
}));

jest.mock('../../../../../hooks/useAnalytics/useAnalytics', () => ({
useAnalytics: () => ({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
}),
}));

const mockRelatedAsset = {
sourceAssetId: 'btc-mainnet',
Expand All @@ -23,63 +47,68 @@ const baseItem: WhatsHappeningItem = {
};

describe('WhatsHappeningCard', () => {
beforeEach(() => {
jest.clearAllMocks();
capturedOnVisible = null;
});

it('renders title and description', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(screen.getByText(baseItem.title)).toBeOnTheScreen();
expect(screen.getByText(baseItem.description)).toBeOnTheScreen();
});

it('renders category badge when category is provided', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(screen.getByText('Macro')).toBeOnTheScreen();
});

it('does not render category badge when category is absent', () => {
const item = { ...baseItem, category: undefined };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.queryByText('Macro')).toBeNull();
});

it('renders Bullish impact badge for positive impact', () => {
const item = { ...baseItem, impact: 'positive' as const };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.getByText('Bullish')).toBeOnTheScreen();
});

it('renders Bearish impact badge for negative impact', () => {
const item = { ...baseItem, impact: 'negative' as const };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.getByText('Bearish')).toBeOnTheScreen();
});

it('renders Neutral impact badge for neutral impact', () => {
const item = { ...baseItem, impact: 'neutral' as const };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.getByText('Neutral')).toBeOnTheScreen();
});

it('does not render impact badge when impact is absent', () => {
const item = { ...baseItem, impact: undefined };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.queryByText('Bullish')).toBeNull();
expect(screen.queryByText('Bearish')).toBeNull();
expect(screen.queryByText('Neutral')).toBeNull();
});

it('renders impact badge alongside category badge', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(screen.getByText('Bullish')).toBeOnTheScreen();
expect(screen.getByText('Macro')).toBeOnTheScreen();
});

it('renders related asset symbol pills', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(screen.getByText('BTC')).toBeOnTheScreen();
});

it('does not render asset pills when relatedAssets is empty', () => {
const item = { ...baseItem, relatedAssets: [] };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.queryByText('BTC')).toBeNull();
});

Expand All @@ -91,36 +120,57 @@ describe('WhatsHappeningCard', () => {
caip19: ['eip155:1/slip44:60'],
};
const item = { ...baseItem, relatedAssets: [mockRelatedAsset, ethAsset] };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.getByText('BTC')).toBeOnTheScreen();
expect(screen.getByText('ETH')).toBeOnTheScreen();
});

it('renders formatted date when date is valid', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(screen.getByText('Mar 15, 2026')).toBeOnTheScreen();
});

it('does not render date when date string is invalid', () => {
const item = { ...baseItem, date: 'not-a-date' };
renderWithProvider(<WhatsHappeningCard item={item} />);
renderWithProvider(<WhatsHappeningCard item={item} cardIndex={0} />);
expect(screen.queryByText('not-a-date')).toBeNull();
});

it('calls onPress with the item when tapped', () => {
const onPress = jest.fn();
renderWithProvider(
<WhatsHappeningCard item={baseItem} onPress={onPress} />,
<WhatsHappeningCard item={baseItem} cardIndex={0} onPress={onPress} />,
);
fireEvent.press(screen.getByText(baseItem.title));
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(baseItem);
});

it('does not throw when onPress is not provided', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} />);
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={0} />);
expect(() =>
fireEvent.press(screen.getByText(baseItem.title)),
).not.toThrow();
});

it('tracks Whats Happening Card Scrolled to View when card becomes visible', () => {
renderWithProvider(<WhatsHappeningCard item={baseItem} cardIndex={2} />);
expect(capturedOnVisible).not.toBeNull();
capturedOnVisible?.();
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
MetaMetricsEvents.WHATS_HAPPENING_CARD_SCROLLED_TO_VIEW,
);
expect(mockTrackEvent).toHaveBeenCalledWith(
expect.objectContaining({
category: MetaMetricsEvents.WHATS_HAPPENING_CARD_SCROLLED_TO_VIEW,
properties: expect.objectContaining({
event_id: 'trend-0',
card_index: 2,
category: 'macro',
impact: 'positive',
asset_symbols: ['BTC'],
}),
}),
);
});
});
Loading
Loading