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
2 changes: 1 addition & 1 deletion app/components/UI/Predict/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ The Predict feature enables users to participate in prediction markets within Me
├── /components # Reusable UI components
│ ├── /MarketListContent # Market list display component
│ ├── /MarketsWonCard # Won markets display card
│ ├── /PredictHome # Homepage components (positions, featured markets)
│ ├── /PredictMarket # Market wrapper component (routes to single/multiple)
│ ├── /PredictMarketSingle # Single outcome market card component
│ ├── /PredictMarketMultiple # Multiple outcome market selection component
│ ├── /PredictNewButton # New prediction creation button
│ ├── /PredictPosition # Position display component
│ ├── /PredictPositionEmpty # Empty state for positions
│ └── /SearchBox # Market search component
├── /controllers # Controllers for PredictMarket
│ └── PredictController.ts # Main controller with tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { forwardRef } from 'react';
import PredictPositionsHeader, {
PredictPositionsHeaderHandle,
} from '../PredictPositionsHeader';

interface PredictHomeAccountStateProps {
onError?: (error: string | null) => void;
}

const PredictHomeAccountState = forwardRef<
PredictPositionsHeaderHandle,
PredictHomeAccountStateProps
>(({ onError }, ref) => <PredictPositionsHeader ref={ref} onError={onError} />);

PredictHomeAccountState.displayName = 'PredictHomeAccountState';

export default PredictHomeAccountState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import PredictHomeFeatured from './PredictHomeFeatured';

// Mock the selector
const mockSelectPredictHomeFeaturedVariant = jest.fn();
jest.mock('../../selectors/featureFlags', () => ({
selectPredictHomeFeaturedVariant: (state: unknown) =>
mockSelectPredictHomeFeaturedVariant(state),
}));

// Mock react-redux
jest.mock('react-redux', () => ({
useSelector: (selector: (state: unknown) => unknown) => selector({}),
}));

// Mock child components
jest.mock('./PredictHomeFeaturedCarousel', () => {
const ReactNative = jest.requireActual('react-native');
return {
__esModule: true,
default: ({ testID }: { testID?: string }) => (
<ReactNative.View testID={testID || 'carousel'}>
<ReactNative.Text>Carousel</ReactNative.Text>
</ReactNative.View>
),
};
});

jest.mock('./PredictHomeFeaturedList', () => {
const ReactNative = jest.requireActual('react-native');
return {
__esModule: true,
default: ({ testID }: { testID?: string }) => (
<ReactNative.View testID={testID || 'list'}>
<ReactNative.Text>List</ReactNative.Text>
</ReactNative.View>
),
};
});

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

describe('variant routing', () => {
it('renders carousel component when variant is carousel', () => {
mockSelectPredictHomeFeaturedVariant.mockReturnValue('carousel');

render(<PredictHomeFeatured />);

expect(screen.getByText('Carousel')).toBeOnTheScreen();
});

it('renders list component when variant is list', () => {
mockSelectPredictHomeFeaturedVariant.mockReturnValue('list');

render(<PredictHomeFeatured />);

expect(screen.getByText('List')).toBeOnTheScreen();
});

it('renders carousel component when variant is undefined', () => {
mockSelectPredictHomeFeaturedVariant.mockReturnValue(undefined);

render(<PredictHomeFeatured />);

expect(screen.getByText('Carousel')).toBeOnTheScreen();
});
});

describe('testID prop', () => {
it('forwards testID to carousel component', () => {
mockSelectPredictHomeFeaturedVariant.mockReturnValue('carousel');

render(<PredictHomeFeatured testID="custom-test-id" />);

expect(screen.getByTestId('custom-test-id')).toBeOnTheScreen();
});

it('forwards testID to list component', () => {
mockSelectPredictHomeFeaturedVariant.mockReturnValue('list');

render(<PredictHomeFeatured testID="custom-test-id" />);

expect(screen.getByTestId('custom-test-id')).toBeOnTheScreen();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { selectPredictHomeFeaturedVariant } from '../../selectors/featureFlags';
import PredictHomeFeaturedCarousel from './PredictHomeFeaturedCarousel';
import PredictHomeFeaturedList from './PredictHomeFeaturedList';

interface PredictHomeFeaturedProps {
testID?: string;
}

const PredictHomeFeatured: React.FC<PredictHomeFeaturedProps> = ({
testID = 'predict-home-featured',
}) => {
const variant = useSelector(selectPredictHomeFeaturedVariant);

if (variant === 'list') {
return <PredictHomeFeaturedList testID={testID} />;
}

return <PredictHomeFeaturedCarousel testID={testID} />;
};

export default PredictHomeFeatured;
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React from 'react';
import { screen, fireEvent, render } from '@testing-library/react-native';
import { useNavigation } from '@react-navigation/native';
import PredictHomeFeaturedCarousel from './PredictHomeFeaturedCarousel';
import Routes from '../../../../../constants/navigation/Routes';
import { PredictEventValues } from '../../constants/eventNames';

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: jest.fn(),
}));

jest.mock('@metamask/design-system-twrnc-preset', () => ({
useTailwind: () => ({
style: (...classes: (string | boolean | undefined)[]) => ({
testStyle: classes.filter(Boolean).join(' '),
}),
}),
}));

jest.mock('@metamask/design-system-react-native', () => {
const ReactNative = jest.requireActual('react-native');
return {
Box: ({
children,
testID,
...props
}: {
children?: React.ReactNode;
testID?: string;
[key: string]: unknown;
}) => (
<ReactNative.View testID={testID} {...props}>
{children}
</ReactNative.View>
),
BoxFlexDirection: { Row: 'row' },
BoxAlignItems: { Center: 'center' },
Text: ({
children,
testID,
...props
}: {
children?: React.ReactNode;
testID?: string;
[key: string]: unknown;
}) => (
<ReactNative.Text testID={testID} {...props}>
{children}
</ReactNative.Text>
),
TextVariant: { HeadingMd: 'heading-md' },
TextColor: { TextDefault: 'text-default' },
Icon: ({
name,
testID,
...props
}: {
name: string;
testID?: string;
[key: string]: unknown;
}) => (
<ReactNative.View testID={testID || `icon-${name}`} {...props}>
<ReactNative.Text>{name}</ReactNative.Text>
</ReactNative.View>
),
IconName: { ArrowRight: 'ArrowRight' },
IconSize: { Sm: 'sm' },
IconColor: { IconAlternative: 'icon-alternative' },
};
});

const mockSectionFn = jest.fn();
jest.mock(
'../../../../Views/TrendingView/components/Sections/Section',
() =>
function MockSection(props: { sectionId: string }) {
mockSectionFn(props);
const ReactNative = jest.requireActual('react-native');
return (
<ReactNative.View testID="mock-section">
<ReactNative.Text>Section: {props.sectionId}</ReactNative.Text>
</ReactNative.View>
);
},
);

jest.mock('../../../../Views/TrendingView/sections.config', () => ({
SECTIONS_CONFIG: {
predictions: {
id: 'predictions',
},
},
}));

jest.mock('../../../../../../locales/i18n', () => ({
strings: (key: string) => {
const translations: Record<string, string> = {
'predict.category.trending': 'Trending',
};
return translations[key] || key;
},
}));

jest.mock('../../contexts', () => ({
PredictEntryPointProvider: ({ children }: { children?: React.ReactNode }) => {
const ReactNative = jest.requireActual('react-native');
return <ReactNative.View>{children}</ReactNative.View>;
},
}));

describe('PredictHomeFeaturedCarousel', () => {
const mockNavigation = {
navigate: jest.fn(),
dispatch: jest.fn(),
reset: jest.fn(),
goBack: jest.fn(),
isFocused: jest.fn(() => true),
canGoBack: jest.fn(() => false),
getId: jest.fn(),
getParent: jest.fn(),
getState: jest.fn(),
};

const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;

beforeEach(() => {
jest.clearAllMocks();
mockUseNavigation.mockReturnValue(
mockNavigation as unknown as ReturnType<typeof useNavigation>,
);
});

afterEach(() => {
jest.resetAllMocks();
});

describe('rendering', () => {
it('renders container with correct testID', () => {
render(<PredictHomeFeaturedCarousel />);

expect(
screen.getByTestId('predict-home-featured-carousel'),
).toBeOnTheScreen();
});

it('renders section header with trending text', () => {
render(<PredictHomeFeaturedCarousel />);

expect(screen.getByText('Trending')).toBeOnTheScreen();
});

it('renders Section component with predictions sectionId', () => {
render(<PredictHomeFeaturedCarousel />);

expect(screen.getByTestId('mock-section')).toBeOnTheScreen();
expect(screen.getByText('Section: predictions')).toBeOnTheScreen();
});

it('renders header with correct testID', () => {
render(<PredictHomeFeaturedCarousel />);

expect(
screen.getByTestId('predict-home-featured-carousel-header'),
).toBeOnTheScreen();
});

it('renders arrow icon', () => {
render(<PredictHomeFeaturedCarousel />);

expect(screen.getByTestId('icon-ArrowRight')).toBeOnTheScreen();
});
});

describe('navigation', () => {
it('navigates to market list with homepage_featured entryPoint when header is pressed', () => {
render(<PredictHomeFeaturedCarousel />);

fireEvent.press(
screen.getByTestId('predict-home-featured-carousel-header'),
);

expect(mockNavigation.navigate).toHaveBeenCalledTimes(1);
expect(mockNavigation.navigate).toHaveBeenCalledWith(
Routes.PREDICT.ROOT,
{
screen: Routes.PREDICT.MARKET_LIST,
params: {
entryPoint:
PredictEventValues.ENTRY_POINT.HOMEPAGE_FEATURED_CAROUSEL,
},
},
);
});
});

describe('Section integration', () => {
it('passes correct props to Section component', () => {
render(<PredictHomeFeaturedCarousel />);

expect(mockSectionFn).toHaveBeenCalledWith(
expect.objectContaining({
sectionId: 'predictions',
refreshConfig: { trigger: 0, silentRefresh: true },
toggleSectionEmptyState: expect.any(Function),
toggleSectionLoadingState: expect.any(Function),
}),
);
});
});
});
Loading
Loading