import React from 'react'; import { render, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; // Mock useFetchFeed so we can control its return values and inspect calls const mockUseFetchFeed = jest.fn(); jest.mock('../useFetchFeed', () => ({ useFetchFeed: (...args: any[]) => mockUseFetchFeed(...args) })); import { useAdaptiveMatchFeed } from '../useAdaptiveMatchFeed'; // tslint:disable-next-line:no-empty const noop = () => {}; const makeFeed = (matchStatus: string) => ({ liveData: { matchDetails: { matchStatus } } }); // Helper component that renders the hook and exposes state const HookRenderer: React.FC<{ url: string | undefined; options?: any; onState: (state: any) => void; }> = ({ url, options, onState }) => { const state = useAdaptiveMatchFeed(url, options); React.useEffect(() => { onState(state); }); return null; }; describe('useAdaptiveMatchFeed', () => { afterEach(() => { mockUseFetchFeed.mockReset(); cleanup(); }); it('returns data and no error when fetch succeeds', () => { const feed = makeFeed('Playing'); // First call = initial fetch, second call = polling fetch mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: feed, loading: false, error: undefined }); let latestState: any; render( (latestState = s)} />); expect(latestState.data).toEqual(feed); expect(latestState.loading).toBe(false); expect(latestState.error).toBeUndefined(); }); it('returns loading true when initial fetch is loading', () => { mockUseFetchFeed .mockReturnValueOnce({ data: undefined, loading: true, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); let latestState: any; render( (latestState = s)} />); expect(latestState.loading).toBe(true); expect(latestState.data).toBeUndefined(); }); it('returns error when fetch fails', () => { mockUseFetchFeed .mockReturnValueOnce({ data: undefined, loading: false, error: 'HTTP 403' }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); let latestState: any; render( (latestState = s)} />); expect(latestState.error).toBe('HTTP 403'); expect(latestState.data).toBeUndefined(); }); it('passes undefined url to initial fetch when url is undefined', () => { mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); // First call (initial fetch) should receive undefined expect(mockUseFetchFeed.mock.calls[0][0]).toBeUndefined(); }); it('passes url to polling hook with preMatchInterval for Fixture status', () => { const feed = makeFeed('Fixture'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render( ); // Second call = polling hook const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBe('/api/match'); expect(pollingCall[1]).toEqual({ pollingInterval: 45000 }); }); it('passes url to polling hook with liveInterval for Playing status', () => { const feed = makeFeed('Playing'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render( ); // Second call = polling hook const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBe('/api/match'); expect(pollingCall[1]).toEqual({ pollingInterval: 15000 }); }); it('passes undefined url to polling hook for Played (terminal) status', () => { const feed = makeFeed('Played'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); // Second call = polling hook — should have undefined url to stop polling const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBeUndefined(); }); it('passes undefined url to polling hook for Cancelled (terminal) status', () => { const feed = makeFeed('Cancelled'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBeUndefined(); }); it('passes undefined url to polling hook for Postponed (terminal) status', () => { const feed = makeFeed('Postponed'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBeUndefined(); }); it('passes undefined url to polling hook for Abandoned (terminal) status', () => { const feed = makeFeed('Abandoned'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBeUndefined(); }); it('uses preMatchInterval when matchStatus is Suspended', () => { const feed = makeFeed('Suspended'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render( ); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBe('/api/match'); expect(pollingCall[1]).toEqual({ pollingInterval: 50000 }); }); it('uses preMatchInterval when there is an error and no data', () => { mockUseFetchFeed .mockReturnValueOnce({ data: undefined, loading: false, error: 'HTTP 403' }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render( ); // No data + error → uses preMatchInterval for polling const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[0]).toBe('/api/match'); expect(pollingCall[1]).toEqual({ pollingInterval: 60000 }); }); it('uses default liveInterval of 30000 when not specified', () => { const feed = makeFeed('Playing'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[1]).toEqual({ pollingInterval: 30000 }); }); it('uses default preMatchInterval of 60000 when not specified', () => { const feed = makeFeed('Fixture'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: undefined, loading: false, error: undefined }); render(); const pollingCall = mockUseFetchFeed.mock.calls[1]; expect(pollingCall[1]).toEqual({ pollingInterval: 60000 }); }); it('prefers polled data over initial data', () => { const initialFeed = makeFeed('Playing'); const polledFeed = { ...makeFeed('Playing'), extra: 'updated' }; mockUseFetchFeed .mockReturnValueOnce({ data: initialFeed, loading: false, error: undefined }) .mockReturnValueOnce({ data: polledFeed, loading: false, error: undefined }); let latestState: any; render( (latestState = s)} />); expect(latestState.data).toEqual(polledFeed); }); it('falls back to initial data when polled data is undefined', () => { const initialFeed = makeFeed('Playing'); mockUseFetchFeed .mockReturnValueOnce({ data: initialFeed, loading: false, error: undefined }) .mockReturnValueOnce({ data: undefined, loading: false, error: undefined }); let latestState: any; render( (latestState = s)} />); expect(latestState.data).toEqual(initialFeed); }); it('stops polling after maxRetries consecutive errors', () => { // Simulate: loading transitions from true→false with error repeatedly. // We need to re-render the component multiple times. mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: undefined }); const { rerender } = render( ); // Simulate first error: loading=true then loading=false with error mockUseFetchFeed.mockReturnValue({ data: undefined, loading: true, error: undefined }); rerender( ); mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: 'HTTP 403' }); rerender( ); // First error counted — should still poll const callsLen = mockUseFetchFeed.mock.calls.length; const lastPollingUrl = mockUseFetchFeed.mock.calls[callsLen - 1][0]; // After 1 error, still polling expect(lastPollingUrl).toBe('/api/match'); // Simulate second error: loading=true then loading=false mockUseFetchFeed.mockReturnValue({ data: undefined, loading: true, error: 'HTTP 403' }); rerender( ); mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: 'HTTP 403' }); rerender( ); // Extra rerender so the useEffect error-count update is picked up rerender( ); // After 2 errors (= maxRetries), polling should stop const finalCallsLen = mockUseFetchFeed.mock.calls.length; const finalPollingUrl = mockUseFetchFeed.mock.calls[finalCallsLen - 1][0]; expect(finalPollingUrl).toBeUndefined(); }); it('resets error count when data is received', () => { mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: undefined }); const { rerender } = render( ); // Simulate one error cycle mockUseFetchFeed.mockReturnValue({ data: undefined, loading: true, error: undefined }); rerender( ); mockUseFetchFeed.mockReturnValue({ data: undefined, loading: false, error: 'HTTP 403' }); rerender( ); // Now data arrives (success) — should reset error count const feed = makeFeed('Playing'); mockUseFetchFeed .mockReturnValueOnce({ data: feed, loading: false, error: undefined }) .mockReturnValue({ data: feed, loading: false, error: undefined }); rerender( ); // Should still be polling (error count reset) const callsLen = mockUseFetchFeed.mock.calls.length; const pollingUrl = mockUseFetchFeed.mock.calls[callsLen - 1][0]; expect(pollingUrl).toBe('/api/match'); }); });