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
108 changes: 101 additions & 7 deletions src/containers/MyCompetitions/MyCompetitions.query.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { dehydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, renderHook, waitFor } from '@testing-library/react';
import { PropsWithChildren } from 'react';
import { fetchUserWithCompetitions } from '@/lib/api';
import { fetchUserWithCompetitions, UserCompsResponse } from '@/lib/api';
import { QueryProvider, queryClient } from '@/providers/QueryProvider';
import { useMyCompetitionsQuery } from './MyCompetitions.query';

jest.mock('@/hooks/UsePinnedCompetitions', () => ({
Expand All @@ -15,12 +16,10 @@ jest.mock('@/lib/api', () => ({
}));

jest.mock('@/lib/localStorage', () => {
const storage = new Map<string, string>();

return {
getLocalStorage: jest.fn((key: string) => storage.get(key) ?? null),
getLocalStorage: jest.fn((key: string) => window.localStorage.getItem(key)),
setLocalStorage: jest.fn((key: string, value: string) => {
storage.set(key, value);
window.localStorage.setItem(key, value);
}),
};
});
Expand Down Expand Up @@ -59,6 +58,12 @@ const createWrapper = () => {

beforeEach(() => {
jest.clearAllMocks();
window.localStorage.clear();
});

afterEach(() => {
queryClient.clear();
window.localStorage.clear();
});

it('loads my competitions from the public user competitions endpoint', async () => {
Expand Down Expand Up @@ -102,3 +107,92 @@ it('uses cached competitions as stale startup data and still refetches', async (
await waitFor(() => expect(result.current.competitions).toEqual([ongoingCompetition]));
expect(fetchUserWithCompetitions).toHaveBeenCalledWith(String(user.id));
});

it.each([
{ name: 'empty', cachedCompetitions: [] },
{ name: 'populated', cachedCompetitions: [upcomingCompetition] },
])(
'refreshes a fresh $name persisted cache while keeping its data visible',
async ({ cachedCompetitions }) => {
const reloadedUser = { ...user, id: 8184 };
const previousClient = new QueryClient();
previousClient.setQueryData(['userCompetitions', reloadedUser.id], {
user: reloadedUser,
upcoming_competitions: cachedCompetitions,
ongoing_competitions: [],
});
window.localStorage.setItem('user', JSON.stringify(reloadedUser));
window.localStorage.setItem(
'REACT_QUERY_OFFLINE_CACHE',
JSON.stringify({
timestamp: Date.now(),
buster: 'wcif-v2',
clientState: dehydrate(previousClient),
}),
);
previousClient.clear();
let resolveResponse!: (response: UserCompsResponse) => void;
jest.mocked(fetchUserWithCompetitions).mockReturnValue(
new Promise((resolve) => {
resolveResponse = resolve;
}),
);

const { result } = renderHook(() => useMyCompetitionsQuery(reloadedUser.id), {
wrapper: QueryProvider,
});

await waitFor(() => expect(fetchUserWithCompetitions).toHaveBeenCalledWith('8184'));
expect(result.current.competitions).toEqual(cachedCompetitions);
expect(result.current.isFetching).toBe(true);
expect(result.current.isLoading).toBe(false);

await act(async () => {
resolveResponse({
user: reloadedUser,
upcoming_competitions: [],
ongoing_competitions: [ongoingCompetition],
});
});

await waitFor(() => expect(result.current.competitions).toEqual([ongoingCompetition]));
},
);

it('refreshes again on a return visit even when the previous request is still fresh', async () => {
const wrapper = createWrapper();
jest.mocked(fetchUserWithCompetitions).mockResolvedValueOnce({
user,
upcoming_competitions: [upcomingCompetition],
ongoing_competitions: [],
});

const firstVisit = renderHook(() => useMyCompetitionsQuery(user.id), { wrapper });
await waitFor(() =>
expect(firstVisit.result.current.competitions).toEqual([upcomingCompetition]),
);
firstVisit.unmount();

let resolveResponse!: (response: UserCompsResponse) => void;
jest.mocked(fetchUserWithCompetitions).mockReturnValueOnce(
new Promise((resolve) => {
resolveResponse = resolve;
}),
);
const returnVisit = renderHook(() => useMyCompetitionsQuery(user.id), { wrapper });

await waitFor(() => expect(fetchUserWithCompetitions).toHaveBeenCalledTimes(2));
expect(returnVisit.result.current.competitions).toEqual([upcomingCompetition]);
expect(returnVisit.result.current.isFetching).toBe(true);

await act(async () => {
resolveResponse({
user,
upcoming_competitions: [],
ongoing_competitions: [ongoingCompetition],
});
});
await waitFor(() =>
expect(returnVisit.result.current.competitions).toEqual([ongoingCompetition]),
);
});
2 changes: 2 additions & 0 deletions src/containers/MyCompetitions/MyCompetitions.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const useMyCompetitionsQuery = (userId?: number, options: { enabled?: boo
},
staleTime: FIVE_MINUTES,
gcTime: FIVE_MINUTES,
// Recheck persisted results on each visit, even when a cached empty list is fresh.
refetchOnMount: 'always',
enabled: Boolean(userId && (options.enabled ?? true)),
initialData: () => getCachedUserCompetitions(userId),
initialDataUpdatedAt: 0,
Expand Down
6 changes: 4 additions & 2 deletions src/containers/MyCompetitions/MyCompetitions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIsRestoring } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { CompetitionListFragment } from '@/components';
import { useAuth } from '@/providers/AuthProvider';
Expand All @@ -7,7 +8,8 @@ import { useMyCompetitionsQuery } from './MyCompetitions.query';
export function MyCompetitions() {
const { t } = useTranslation();
const { user } = useAuth();
const { error, status, isLoading, competitions, pinnedCompetitions } = useMyCompetitionsQuery(
const isRestoring = useIsRestoring();
const { error, status, isFetching, competitions, pinnedCompetitions } = useMyCompetitionsQuery(
user?.id,
);

Expand All @@ -20,7 +22,7 @@ export function MyCompetitions() {
<CompetitionListFragment
title={t('home.myCompetitions')}
competitions={[...pinnedCompetitions, ...competitions]}
loading={isLoading && !pinnedCompetitions.length}
loading={Boolean(user) && (isRestoring || isFetching)}
liveCompetitionIds={NotifyCompetitions?.competitions?.map((c) => c.id) || []}
showWhenEmpty={Boolean(user)}
/>
Expand Down
Loading