Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe('PredictMarketOutcome', () => {
{ state: initialState },
);

expect(getByText('Unknown Market')).toBeOnTheScreen();
// The component now shows the groupItemTitle directly, even if it's null/undefined
expect(getByText('0%')).toBeOnTheScreen();
expect(getByText(/\$0.*Vol\./)).toBeOnTheScreen();
});
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('PredictMarketOutcome', () => {
expect(getByText('No • 0.00¢')).toBeOnTheScreen();
});

it('displays Unknown Market when groupItemTitle is missing', () => {
it('displays empty title when groupItemTitle is missing', () => {
const outcomeWithNoTitle: PredictOutcome = {
...mockOutcome,
groupItemTitle: undefined as unknown as string,
Expand All @@ -254,6 +254,127 @@ describe('PredictMarketOutcome', () => {
{ state: initialState },
);

expect(getByText('Unknown Market')).toBeOnTheScreen();
// The component now shows the groupItemTitle directly, even if it's undefined
// We can verify the component renders without errors by checking other elements
expect(getByText('+65%')).toBeOnTheScreen();
expect(getByText(/\$1M.*Vol\./)).toBeOnTheScreen();
});

describe('Closed Market States', () => {
it('displays winner badge and check icon when market is closed with winning token', () => {
const winningToken = {
id: 'winning-token',
title: 'Yes',
price: 1.0,
};

const { getByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed
outcomeToken={winningToken}
/>,
{ state: initialState },
);

expect(getByText('Yes')).toBeOnTheScreen(); // Winner token title
expect(getByText('Winner')).toBeOnTheScreen(); // Winner badge
// Check icon is rendered (mocked as SvgMock)
});

it('does not display winner badge when market is closed but no winning token provided', () => {
const { queryByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed
/>,
{ state: initialState },
);

expect(queryByText('Winner')).not.toBeOnTheScreen();
});

it('does not display winner badge when market is not closed', () => {
const winningToken = {
id: 'winning-token',
title: 'Yes',
price: 1.0,
};

const { queryByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed={false}
outcomeToken={winningToken}
/>,
{ state: initialState },
);

expect(queryByText('Winner')).not.toBeOnTheScreen();
});

it('hides action buttons when market is closed', () => {
const { queryByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed
/>,
{ state: initialState },
);

expect(queryByText('Yes • 65.00¢')).not.toBeOnTheScreen();
expect(queryByText('No • 35.00¢')).not.toBeOnTheScreen();
});

it('shows action buttons when market is not closed', () => {
const { getByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed={false}
/>,
{ state: initialState },
);

expect(getByText('Yes • 65.00¢')).toBeOnTheScreen();
expect(getByText('No • 35.00¢')).toBeOnTheScreen();
});

it('uses outcomeToken title when market is closed and outcomeToken is provided', () => {
const winningToken = {
id: 'winning-token',
title: 'Winning Option',
price: 1.0,
};

const { getByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed
outcomeToken={winningToken}
/>,
{ state: initialState },
);

expect(getByText('Winning Option')).toBeOnTheScreen();
});

it('uses groupItemTitle when market is closed but no outcomeToken provided', () => {
const { getByText } = renderWithProvider(
<PredictMarketOutcome
outcome={mockOutcome}
market={mockMarket}
isClosed
/>,
{ state: initialState },
);

expect(getByText('Crypto Markets')).toBeOnTheScreen();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ import Text, {
TextColor,
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import Icon, {
IconName,
IconSize,
} from '../../../../../component-library/components/Icons/Icon';
import { useStyles } from '../../../../../component-library/hooks';
import Routes from '../../../../../constants/navigation/Routes';
import {
PredictMarket,
PredictOutcomeToken,
PredictOutcome as PredictOutcomeType,
} from '../../types';
import { PredictNavigationParamList } from '../../types/navigation';
Expand All @@ -30,13 +35,16 @@ import { usePredictBalance } from '../../hooks/usePredictBalance';
interface PredictMarketOutcomeProps {
market: PredictMarket;
outcome: PredictOutcomeType;
outcomeToken?: PredictOutcomeToken;
isClosed?: boolean;
}

const PredictMarketOutcome: React.FC<PredictMarketOutcomeProps> = ({
market,
outcome,
isClosed = false,
outcomeToken,
}) => {
// const outcome = market.outcomes[0];
const { styles } = useStyles(styleSheet, {});
const tw = useTailwind();
const navigation =
Expand All @@ -55,7 +63,13 @@ const PredictMarketOutcome: React.FC<PredictMarketOutcomeProps> = ({
return '0%';
};

const getTitle = (): string => outcome.groupItemTitle ?? 'Unknown Market';
const getTitle = (): string => {
if (isClosed && outcomeToken) {
return outcomeToken.title;
}
return outcome.groupItemTitle;

};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Title Function Returns Undefined/Null

The getTitle() function is typed as string but can return undefined or null. This occurs because the ?? 'Unknown Market' fallback was removed, and outcome.groupItemTitle or outcomeToken.title (when isClosed) can be undefined. This creates a type safety issue and can cause runtime errors when rendering text.

Fix in Cursor Fix in Web


const getImageUrl = (): string => outcome.image;

Expand Down Expand Up @@ -117,48 +131,75 @@ const PredictMarketOutcome: React.FC<PredictMarketOutcomeProps> = ({
)}
</Box>
<View style={tw.style('flex-1')}>
<Text
variant={TextVariant.HeadingMD}
color={TextColor.Default}
style={tw.style('font-medium')}
<Box
flexDirection={BoxFlexDirection.Row}
alignItems={BoxAlignItems.Center}
twClassName="gap-2"
>
{getTitle()}
</Text>
<Text
variant={TextVariant.HeadingMD}
color={TextColor.Default}
style={tw.style('font-medium')}
>
{getTitle()}
</Text>
{isClosed && outcomeToken && (
<Text
variant={TextVariant.BodyXS}
color={TextColor.Success}
style={tw.style('bg-success-muted px-1 py-0.5 rounded-sm')}
>
Winner
</Text>
)}
</Box>
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
${getVolumeDisplay()} {strings('predict.volume_abbreviated')}
</Text>
</View>
<Text>{getYesPercentage()}</Text>
<Text>
{isClosed && outcomeToken ? (
<Icon
name={IconName.CheckBold}
size={IconSize.Md}
color={TextColor.Success}
/>
) : (
<Text>{getYesPercentage()}</Text>
)}
</Text>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid Child Nesting in React Native Text Component

The Text component at line 160 conditionally renders an Icon or another Text component. React Native's Text components are strict, expecting only text or other Text components as children. Nesting an Icon or a Text component directly inside another Text component in this way is invalid and can cause runtime errors or rendering issues.

Fix in Cursor Fix in Web

</Box>
</View>
<View style={styles.buttonContainer}>
<Button
variant={ButtonVariants.Secondary}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={
<Text style={tw.style('font-medium')} color={TextColor.Success}>
{strings('predict.buy_yes')} •{' '}
{(outcome.tokens[0].price * 100).toFixed(2)}¢
</Text>
}
onPress={handleYes}
style={styles.buttonYes}
/>
<Button
variant={ButtonVariants.Secondary}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={
<Text style={tw.style('font-medium')} color={TextColor.Error}>
{strings('predict.buy_no')} •{' '}
{(outcome.tokens[1].price * 100).toFixed(2)}¢
</Text>
}
onPress={handleNo}
style={styles.buttonNo}
/>
</View>
{!isClosed && (
<View style={styles.buttonContainer}>
<Button
variant={ButtonVariants.Secondary}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={
<Text style={tw.style('font-medium')} color={TextColor.Success}>
{strings('predict.buy_yes')} •{' '}
{(outcome.tokens[0].price * 100).toFixed(2)}¢
</Text>
}
onPress={handleYes}
style={styles.buttonYes}
/>
<Button
variant={ButtonVariants.Secondary}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={
<Text style={tw.style('font-medium')} color={TextColor.Error}>
{strings('predict.buy_no')} •{' '}
{(outcome.tokens[1].price * 100).toFixed(2)}¢
</Text>
}
onPress={handleNo}
style={styles.buttonNo}
/>
</View>
)}
</View>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from '@testing-library/react-native';
import { screen, act } from '@testing-library/react-native';
import React from 'react';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { usePredictPositions } from '../../hooks/usePredictPositions';
Expand Down Expand Up @@ -91,11 +91,16 @@ describe('PredictPositions', () => {

it('renders loading state when isLoading is true', () => {
// Arrange
mockUsePredictPositions.mockReturnValue({
...defaultMockHookReturn,
isLoading: true,
positions: [],
});
mockUsePredictPositions
.mockReturnValueOnce({
...defaultMockHookReturn,
isLoading: true,
positions: [],
})
.mockReturnValueOnce({
...defaultMockHookReturn,
positions: [],
});

// Act
renderWithProvider(<PredictPositions />);
Expand All @@ -106,11 +111,16 @@ describe('PredictPositions', () => {

it('renders loading state when isRefreshing and no positions', () => {
// Arrange
mockUsePredictPositions.mockReturnValue({
...defaultMockHookReturn,
isRefreshing: true,
positions: [],
});
mockUsePredictPositions
.mockReturnValueOnce({
...defaultMockHookReturn,
isRefreshing: true,
positions: [],
})
.mockReturnValueOnce({
...defaultMockHookReturn,
positions: [],
});

// Act
renderWithProvider(<PredictPositions />);
Expand All @@ -121,10 +131,15 @@ describe('PredictPositions', () => {

it('renders FlashList when no positions and not loading', () => {
// Arrange
mockUsePredictPositions.mockReturnValue({
...defaultMockHookReturn,
positions: [],
});
mockUsePredictPositions
.mockReturnValueOnce({
...defaultMockHookReturn,
positions: [],
})
.mockReturnValueOnce({
...defaultMockHookReturn,
positions: [],
});

// Act
renderWithProvider(<PredictPositions />);
Expand Down Expand Up @@ -211,10 +226,13 @@ describe('PredictPositions', () => {
});

const ref = React.createRef<PredictPositionsHandle>();

renderWithProvider(<PredictPositions ref={ref} />);

// Act
ref.current?.refresh();
act(() => {
ref.current?.refresh();
});

// Assert
expect(mockLoadPositions).toHaveBeenCalledWith({ isRefresh: true });
Expand Down
Loading
Loading