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
20 changes: 20 additions & 0 deletions app/components/UI/Predict/hooks/useFeaturedCarouselData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ describe('useFeaturedCarouselData', () => {
expect(result.current.error).toBeNull();
});

it('filters child more-market cards', async () => {
const { Wrapper } = createWrapper();
const parentMarket = createMockMarket({ id: 'parent-market' });
const childMarket = createMockMarket({
id: 'child-market',
parentMarketId: 'parent-market',
});
mockGetCarouselMarkets.mockResolvedValue([parentMarket, childMarket]);

const { result } = renderHook(() => useFeaturedCarouselData(), {
wrapper: Wrapper,
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.markets).toEqual([parentMarket]);
});

it('returns error when controller throws', async () => {
const { Wrapper } = createWrapper();
mockGetCarouselMarkets.mockRejectedValue(new Error('Network error'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { predictQueries } from '../queries';
import { selectPredictUpDownEnabledFlag } from '../selectors/featureFlags';
import type { PredictMarket } from '../types';
import { isCryptoUpDown } from '../utils/cryptoUpDown';
import { filterStandaloneMarkets } from '../utils/feed';
import { ensureError } from '../utils/predictErrorHandler';

export interface UseFeaturedCarouselDataResult {
Expand Down Expand Up @@ -40,7 +41,7 @@ export const useFeaturedCarouselData = (): UseFeaturedCarouselDataResult => {
}, [query.error]);

const markets = useMemo(() => {
const data = query.data ?? [];
const data = filterStandaloneMarkets(query.data ?? []);
if (upDownEnabled) {
return data;
}
Expand Down
57 changes: 57 additions & 0 deletions app/components/UI/Predict/hooks/usePredictMarketData.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,63 @@ describe('usePredictMarketData', () => {
);
});

it('filters child more-market cards without disabling pagination', async () => {
const rawMarkets = Array.from({ length: 20 }, (_, index) => ({
...mockMarketData[0],
id: `market-${index}`,
slug: `market-${index}`,
parentMarketId: index >= 18 ? 'parent-market' : undefined,
}));
mockGetMarkets.mockResolvedValue(rawMarkets);

const { result } = renderHook(() => usePredictMarketData({ pageSize: 20 }));

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

expect(result.current.marketData).toHaveLength(18);
expect(result.current.marketData.map((market) => market.id)).toEqual(
rawMarkets.slice(0, 18).map((market) => market.id),
);
expect(result.current.hasMore).toBe(true);
});

it('uses raw page offsets when loading more after child cards are filtered', async () => {
const firstRawPage = Array.from({ length: 20 }, (_, index) => ({
...mockMarketData[0],
id: `first-page-market-${index}`,
slug: `first-page-market-${index}`,
parentMarketId: index >= 18 ? 'parent-market' : undefined,
}));
const secondRawPage = Array.from({ length: 5 }, (_, index) => ({
...mockMarketData[0],
id: `second-page-market-${index}`,
slug: `second-page-market-${index}`,
}));

mockGetMarkets
.mockResolvedValueOnce(firstRawPage)
.mockResolvedValueOnce(secondRawPage);

const { result } = renderHook(() => usePredictMarketData({ pageSize: 20 }));

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

await act(async () => {
await result.current.fetchMore();
});

expect(mockGetMarkets).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ limit: 20, offset: 20 }),
);
expect(result.current.marketData).toHaveLength(23);
expect(result.current.hasMore).toBe(false);
});

it('handle null market data', async () => {
mockGetMarkets.mockResolvedValue(null);

Expand Down
6 changes: 4 additions & 2 deletions app/components/UI/Predict/hooks/usePredictMarketData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Logger from '../../../../util/Logger';
import { PREDICT_CONSTANTS } from '../constants/errors';
import { ensureError } from '../utils/predictErrorHandler';
import { PredictCategory, PredictMarket } from '../types';
import { filterStandaloneMarkets } from '../utils/feed';

export interface UsePredictMarketDataOptions {
q?: string;
Expand Down Expand Up @@ -147,12 +148,13 @@ export const usePredictMarketData = (

const hasMoreData = markets.length >= pageSize;
setHasMore(hasMoreData);
const visibleMarkets = filterStandaloneMarkets(markets);

if (isLoadMore) {
setMarketData((prevData) => {
// Use a Set to efficiently deduplicate by ID
const existingIds = new Set(prevData.map((event) => event.id));
const newEvents = markets.filter(
const newEvents = visibleMarkets.filter(
(event) => !existingIds.has(event.id),
);
const accumulated = [...prevData, ...newEvents];
Expand All @@ -162,7 +164,7 @@ export const usePredictMarketData = (
currentOffsetRef.current += pageSize;
} else {
// Replace data for initial load or refresh
setMarketData(refine ? refine(markets) : markets);
setMarketData(refine ? refine(visibleMarkets) : visibleMarkets);
setCurrentOffset(pageSize);
currentOffsetRef.current = pageSize;
}
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/Predict/providers/polymarket/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export interface PolymarketApiEvent {
period?: PredictGamePeriod;
live?: boolean;
ended?: boolean;
parentEventId?: string | number;
parentEventId?: string | number | null;
}

export interface PolymarketApiActivity {
Expand Down
24 changes: 24 additions & 0 deletions app/components/UI/Predict/providers/polymarket/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,4 +687,28 @@ describe('polymarket utils', () => {
}),
).resolves.toBe(false);
});

it('preserves parent market id when parsing Polymarket events', () => {
const event: PolymarketApiEvent = {
id: 'child-event',
slug: 'child-event',
title: 'Child Event',
description: 'Child event description',
icon: '',
closed: false,
series: [],
markets: [],
tags: [],
liquidity: 0,
volume: 0,
parentEventId: 'parent-market',
};

expect(parsePolymarketEvents([event], 'trending')).toEqual([
expect.objectContaining({
id: 'child-event',
parentMarketId: 'parent-market',
}),
]);
});
});
3 changes: 3 additions & 0 deletions app/components/UI/Predict/providers/polymarket/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,9 @@ export const parsePolymarketEvents = (
volume: event.volume,
game,
...(seriesData && { series: seriesData }),
...(event.parentEventId !== undefined && {
parentMarketId: event.parentEventId,
}),
};
},
);
Expand Down
1 change: 1 addition & 0 deletions app/components/UI/Predict/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export type PredictMarket = {
volume: number;
game?: PredictMarketGame;
series?: PredictSeries;
parentMarketId?: string | number | null;
childMarketIds?: string[];
};

Expand Down
30 changes: 29 additions & 1 deletion app/components/UI/Predict/utils/feed.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deduplicateSeriesMarkets } from './feed';
import { deduplicateSeriesMarkets, filterStandaloneMarkets } from './feed';
import { Recurrence, type PredictMarket } from '../types';

const createMockMarket = (
Expand Down Expand Up @@ -122,3 +122,31 @@ describe('deduplicateSeriesMarkets', () => {
expect(result).toEqual([single]);
});
});

describe('filterStandaloneMarkets', () => {
it('removes markets with a parent market id', () => {
const parent = createMockMarket('parent');
const emptyParent = createMockMarket('empty-parent', {
parentMarketId: '',
});
const nullParent = createMockMarket('null-parent', {
parentMarketId: null,
});
const child = createMockMarket('child', {
parentMarketId: 'parent',
});
const numericChild = createMockMarket('numeric-child', {
parentMarketId: 123,
});

const result = filterStandaloneMarkets([
parent,
emptyParent,
nullParent,
child,
numericChild,
]);

expect(result).toEqual([parent, emptyParent, nullParent]);
});
});
15 changes: 15 additions & 0 deletions app/components/UI/Predict/utils/feed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import type { PredictMarket } from '../types';
import { isCryptoUpDown } from './cryptoUpDown';

export function isStandaloneMarket(market: PredictMarket): boolean {
const { parentMarketId } = market;
return (
parentMarketId === undefined ||
parentMarketId === null ||
String(parentMarketId).trim() === ''
);
}

export function filterStandaloneMarkets(
markets: PredictMarket[],
): PredictMarket[] {
return markets.filter(isStandaloneMarket);
}

/**
* Keeps only the first occurrence of each Crypto Up/Down series slug.
* Non-Up/Down markets pass through unchanged.
Expand Down
Loading