Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from '../../utils/format';
import styleSheet from './PredictMarketOutcome.styles';
import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
import PredictOutcomeRules from '../PredictOutcomeRules';
interface PredictMarketOutcomeProps {
market: PredictMarket;
outcome: PredictOutcomeType;
Expand Down Expand Up @@ -224,6 +225,9 @@ const PredictMarketOutcome: React.FC<PredictMarketOutcomeProps> = ({
/>
</View>
)}
{outcome.description && (
<PredictOutcomeRules description={outcome.description} />
)}
</View>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import PredictOutcomeRules from '.';

const initialState = {
engine: { backgroundState },
};

describe('PredictOutcomeRules', () => {
const mockDescription = 'This market resolves to Yes if BTC hits $100k.';

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

it('renders nothing when description is empty', () => {
renderWithProvider(<PredictOutcomeRules description="" />, {
state: initialState,
});

expect(screen.queryByText('Show rules')).toBeNull();
});

it('renders collapsed state initially', () => {
renderWithProvider(
<PredictOutcomeRules description={mockDescription} />,
{ state: initialState },
);

expect(screen.getByText('Show rules')).toBeOnTheScreen();
expect(screen.queryByText(mockDescription)).toBeNull();
});

it('expands to show rules when pressed', () => {
renderWithProvider(
<PredictOutcomeRules description={mockDescription} />,
{ state: initialState },
);

fireEvent.press(screen.getByText('Show rules'));

expect(screen.getByText('Hide rules')).toBeOnTheScreen();
expect(screen.getByText(mockDescription)).toBeOnTheScreen();
});

it('collapses when pressed again', () => {
renderWithProvider(
<PredictOutcomeRules description={mockDescription} />,
{ state: initialState },
);

fireEvent.press(screen.getByText('Show rules'));
fireEvent.press(screen.getByText('Hide rules'));

expect(screen.getByText('Show rules')).toBeOnTheScreen();
expect(screen.queryByText(mockDescription)).toBeNull();
});

it('displays title when provided', () => {
renderWithProvider(
<PredictOutcomeRules description={mockDescription} title="BTC $100k" />,
{ state: initialState },
);

fireEvent.press(screen.getByText('Show rules'));

expect(screen.getByText('BTC $100k')).toBeOnTheScreen();
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import { Pressable } from 'react-native';
import {
Box,
BoxFlexDirection,
BoxAlignItems,
Text,
TextColor,
TextVariant,
} from '@metamask/design-system-react-native';
import Icon, {
IconName,
IconSize,
} from '../../../../../component-library/components/Icons/Icon';
import { strings } from '../../../../../../locales/i18n';
import { useTheme } from '../../../../../util/theme';

interface PredictOutcomeRulesProps {
description: string;
title?: string;
}

const PredictOutcomeRules: React.FC<PredictOutcomeRulesProps> = ({
description,
title,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const { colors } = useTheme();

if (!description) return null;

return (
<Box twClassName="mt-3 border-t border-muted pt-3">
<Pressable onPress={() => setIsExpanded(!isExpanded)}>
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
twClassName="gap-2"
>
<Icon
name={IconName.Book}
size={IconSize.Sm}
color={colors.primary.default}
/>
<Text variant={TextVariant.BodySm} color={TextColor.PrimaryDefault}>
{isExpanded
? strings('predict.market_details.hide_rules')
: strings('predict.market_details.show_rules')}
</Text>
<Icon
name={isExpanded ? IconName.ArrowUp : IconName.ArrowDown}
size={IconSize.Xs}
color={colors.primary.default}
/>
</Box>
</Pressable>
{isExpanded && (
<Box twClassName="mt-3 p-3 bg-muted rounded-lg">
{title && (
<Text
variant={TextVariant.BodySmBold}
color={TextColor.TextDefault}
twClassName="mb-2"
>
{title}
</Text>
)}
<Text variant={TextVariant.BodySm} color={TextColor.TextAlternative}>
{description}
</Text>
</Box>
)}
</Box>
);
};

export default PredictOutcomeRules;

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './PredictOutcomeRules';

Original file line number Diff line number Diff line change
Expand Up @@ -3444,4 +3444,116 @@ describe('PredictMarketDetails', () => {
});
});
});

describe('About Tab - Rules Display', () => {
it('displays rules disclaimer with Polymarket link', () => {
setupPredictMarketDetailsTest();

const aboutTab = screen.getByTestId(
'predict-market-details-tab-bar-tab-1',
);
fireEvent.press(aboutTab);

expect(
screen.getByText('predict.market_details.rules_more_info'),
).toBeOnTheScreen();
expect(
screen.getByText('predict.market_details.rules_disclaimer'),
).toBeOnTheScreen();
expect(
screen.getByText('predict.market_details.view_rules'),
).toBeOnTheScreen();
});

it('navigates to Polymarket event page when view rules is pressed', () => {
mockRunAfterInteractions.mockImplementation(runAfterInteractionsMockImpl);
// Need to provide a slug for handleViewMarketOnPolymarket to work
const marketWithSlug = createMockMarket({
slug: 'bitcoin-100k-2024',
});
const { mockNavigate } = setupPredictMarketDetailsTest(marketWithSlug);

const aboutTab = screen.getByTestId(
'predict-market-details-tab-bar-tab-1',
);
fireEvent.press(aboutTab);

const viewRulesButton = screen.getByText(
'predict.market_details.view_rules',
);
act(() => {
fireEvent.press(viewRulesButton);
});

const callback =
runAfterInteractionsCallbacks[
runAfterInteractionsCallbacks.length - 1
];
act(() => {
callback?.();
});

expect(mockNavigate).toHaveBeenCalledWith('Webview', {
screen: 'SimpleWebview',
params: {
url: expect.stringContaining('polymarket.com/event/'),
title: expect.any(String),
},
});
});

it('displays first outcome description as rules for multi-outcome markets', () => {
const multiOutcomeMarket = createMockMarket({
description: 'Event level description',
outcomes: [
{
id: 'outcome-1',
title: 'BTC $100k',
groupItemTitle: '↑ $100,000',
description: 'Resolves Yes if BTC reaches $100k on Binance.',
status: 'open',
tokens: [
{ id: 'token-1', title: 'Yes', price: 0.65 },
{ id: 'token-2', title: 'No', price: 0.35 },
],
volume: 1000000,
},
{
id: 'outcome-2',
title: 'BTC $120k',
groupItemTitle: '↑ $120,000',
description: 'Resolves Yes if BTC reaches $120k on Binance.',
status: 'open',
tokens: [
{ id: 'token-3', title: 'Yes', price: 0.25 },
{ id: 'token-4', title: 'No', price: 0.75 },
],
volume: 500000,
},
],
});

setupPredictMarketDetailsTest(multiOutcomeMarket);

const aboutTab = screen.getByTestId(
'predict-market-details-tab-bar-tab-1',
);
fireEvent.press(aboutTab);

// Event description is NOT shown for multi-outcome markets (to avoid duplication)
expect(screen.queryByText('Event level description')).toBeNull();

// Rules section shows first outcome's description only (not repeated per-outcome)
expect(
screen.getByText('predict.market_details.rules'),
).toBeOnTheScreen();
expect(
screen.getByText('Resolves Yes if BTC reaches $100k on Binance.'),
).toBeOnTheScreen();
// Second outcome's description is NOT shown (avoiding repetition)
expect(
screen.queryByText('Resolves Yes if BTC reaches $120k on Binance.'),
).toBeNull();
});
});
});
Loading
Loading