diff --git a/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx b/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx index 487a84bfb..0d203ab9b 100644 --- a/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/trends-panel.test.tsx @@ -1,85 +1,74 @@ -import { List as ImmutableList, Record as ImmutableRecord } from 'immutable'; import React from 'react'; -import { render, screen } from '../../../../jest/test-helpers'; -import { normalizeTag } from '../../../../normalizers'; +import { __stub } from 'soapbox/api'; + +import { queryClient, render, screen, waitFor } from '../../../../jest/test-helpers'; import TrendsPanel from '../trends-panel'; describe('', () => { - it('renders trending hashtags', () => { - const store = { - trends: ImmutableRecord({ - items: ImmutableList([ - normalizeTag({ - name: 'hashtag 1', - history: [{ - day: '1652745600', - uses: '294', - accounts: '180', - }], - }), - ]), - isLoading: false, - })(), - }; - - render(, undefined, store); - expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i); - expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i); - expect(screen.getByTestId('sparklines')).toBeInTheDocument(); + beforeEach(() => { + queryClient.clear(); }); - it('renders multiple trends', () => { - const store = { - trends: ImmutableRecord({ - items: ImmutableList([ - normalizeTag({ - name: 'hashtag 1', - history: ImmutableList([{ accounts: [] }]), - }), - normalizeTag({ - name: 'hashtag 2', - history: ImmutableList([{ accounts: [] }]), - }), - ]), - isLoading: false, - })(), - }; + describe('with hashtags', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/trends') + .reply(200, [ + { + name: 'hashtag 1', + url: 'https://example.com', + history: [{ + day: '1652745600', + uses: '294', + accounts: '180', + }], + }, + { name: 'hashtag 2', url: 'https://example.com' }, + ]); + }); + }); - render(, undefined, store); - expect(screen.queryAllByTestId('hashtag')).toHaveLength(2); + it('renders trending hashtags', async() => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i); + expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i); + expect(screen.getByTestId('sparklines')).toBeInTheDocument(); + }); + }); + + it('renders multiple trends', async() => { + render(); + + await waitFor(() => { + expect(screen.queryAllByTestId('hashtag')).toHaveLength(2); + }); + }); + + it('respects the limit prop', async() => { + render(); + + await waitFor(() => { + expect(screen.queryAllByTestId('hashtag')).toHaveLength(1); + }); + }); }); - it('respects the limit prop', () => { - const store = { - trends: ImmutableRecord({ - items: ImmutableList([ - normalizeTag({ - name: 'hashtag 1', - history: [{ accounts: [] }], - }), - normalizeTag({ - name: 'hashtag 2', - history: [{ accounts: [] }], - }), - ]), - isLoading: false, - })(), - }; + describe('without hashtags', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/trends').reply(200, []); + }); + }); - render(, undefined, store); - expect(screen.queryAllByTestId('hashtag')).toHaveLength(1); - }); + it('renders empty', async() => { + render(); - it('renders empty', () => { - const store = { - trends: ImmutableRecord({ - items: ImmutableList([]), - isLoading: false, - })(), - }; - - render(, undefined, store); - expect(screen.queryAllByTestId('hashtag')).toHaveLength(0); + await waitFor(() => { + expect(screen.queryAllByTestId('hashtag')).toHaveLength(0); + }); + }); }); }); diff --git a/app/soapbox/features/ui/components/trends-panel.tsx b/app/soapbox/features/ui/components/trends-panel.tsx index 49b68887d..9f582d891 100644 --- a/app/soapbox/features/ui/components/trends-panel.tsx +++ b/app/soapbox/features/ui/components/trends-panel.tsx @@ -1,40 +1,24 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch } from 'react-redux'; -import { fetchTrends } from 'soapbox/actions/trends'; import Hashtag from 'soapbox/components/hashtag'; import { Widget } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; +import useTrends from 'soapbox/queries/trends'; interface ITrendsPanel { limit: number } const TrendsPanel = ({ limit }: ITrendsPanel) => { - const dispatch = useDispatch(); + const { data: trends, isFetching } = useTrends(); - const trends = useAppSelector((state) => state.trends.items); - - const sortedTrends = React.useMemo(() => { - return trends.sort((a, b) => { - const num_a = Number(a.getIn(['history', 0, 'accounts'])); - const num_b = Number(b.getIn(['history', 0, 'accounts'])); - return num_b - num_a; - }).slice(0, limit); - }, [trends, limit]); - - React.useEffect(() => { - dispatch(fetchTrends()); - }, []); - - if (sortedTrends.isEmpty()) { + if (trends?.length === 0 || isFetching) { return null; } return ( }> - {sortedTrends.map((hashtag) => ( + {trends?.slice(0, limit).map((hashtag) => ( ))} diff --git a/app/soapbox/jest/test-helpers.tsx b/app/soapbox/jest/test-helpers.tsx index 0894d4d40..721783879 100644 --- a/app/soapbox/jest/test-helpers.tsx +++ b/app/soapbox/jest/test-helpers.tsx @@ -37,6 +37,8 @@ const queryClient = new QueryClient({ }, defaultOptions: { queries: { + staleTime: 0, + cacheTime: Infinity, retry: false, }, }, @@ -123,4 +125,5 @@ export { rootReducer, mockWindowProperty, createTestStore, + queryClient, }; diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts index 15977dbb9..f38bf0dbc 100644 --- a/app/soapbox/queries/__tests__/suggestions.test.ts +++ b/app/soapbox/queries/__tests__/suggestions.test.ts @@ -4,7 +4,7 @@ import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import useOnboardingSuggestions from '../suggestions'; describe('useCarouselAvatars', () => { - describe('with a successul query', () => { + describe('with a successful query', () => { beforeEach(() => { __stub((mock) => { mock.onGet('/api/v2/suggestions') @@ -26,7 +26,7 @@ describe('useCarouselAvatars', () => { }); }); - describe('with an unsuccessul query', () => { + describe('with an unsuccessful query', () => { beforeEach(() => { __stub((mock) => { mock.onGet('/api/v2/suggestions').networkError(); diff --git a/app/soapbox/queries/__tests__/trends.test.ts b/app/soapbox/queries/__tests__/trends.test.ts new file mode 100644 index 000000000..784f928cb --- /dev/null +++ b/app/soapbox/queries/__tests__/trends.test.ts @@ -0,0 +1,46 @@ +import { __stub } from 'soapbox/api'; +import { queryClient, renderHook, waitFor } from 'soapbox/jest/test-helpers'; + +import useTrends from '../trends'; + +describe('useTrends', () => { + beforeEach(() => { + queryClient.clear(); + }); + + describe('with a successful query', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/trends') + .reply(200, [ + { name: '#golf', url: 'https://example.com' }, + { name: '#tennis', url: 'https://example.com' }, + ]); + }); + }); + + it('is successful', async() => { + const { result } = renderHook(() => useTrends()); + + await waitFor(() => expect(result.current.isFetching).toBe(false)); + + expect(result.current.data?.length).toBe(2); + }); + }); + + describe('with an unsuccessful query', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/trends').networkError(); + }); + }); + + it('is successful', async() => { + const { result } = renderHook(() => useTrends()); + + await waitFor(() => expect(result.current.isFetching).toBe(false)); + + expect(result.current.error).toBeDefined(); + }); + }); +}); diff --git a/app/soapbox/queries/trends.ts b/app/soapbox/queries/trends.ts new file mode 100644 index 000000000..afe780991 --- /dev/null +++ b/app/soapbox/queries/trends.ts @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query'; + +import { fetchTrendsSuccess } from 'soapbox/actions/trends'; +import { useApi, useAppDispatch } from 'soapbox/hooks'; +import { normalizeTag } from 'soapbox/normalizers'; + +import type { Tag } from 'soapbox/types/entities'; + +export default function useTrends() { + const api = useApi(); + const dispatch = useAppDispatch(); + + const getTrends = async() => { + const { data } = await api.get('/api/v1/trends'); + + dispatch(fetchTrendsSuccess(data)); + + const normalizedData = data.map((tag) => normalizeTag(tag)); + return normalizedData; + }; + + const result = useQuery>(['trends'], getTrends, { + placeholderData: [], + staleTime: 600000, // 10 minutes + }); + + return result; +}