import { AsyncBoundary, DataProvider, GCOptions, GCPolicy, useQuery, useSuspense, } from '@data-client/react'; import { MockResolver } from '@data-client/test'; import { render, screen, act } from '@testing-library/react'; import { Article, ArticleResource } from '__tests__/new'; import '@testing-library/jest-dom'; import { useState } from 'react'; const mockGetList = jest.fn(); const mockGet = jest.fn(); const GC_INTERVAL = 5000; const ArticleList = ({ articles, onSelect, }: { articles: Article[]; onSelect: (id: number) => void; }) => { return (
{articles.map(article => (
onSelect(article.id ?? 0)}> {article.title}
))}
); }; const ArticleDetail = ({ id }: { id: number }) => { const article = useSuspense(ArticleResource.get, { id }); return (

{article.title}

{article.content}

); }; const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => { const articles = useSuspense(ArticleResource.getList); return ; }; const QueryArticleList = ({ onSelect }: { onSelect: (id: number) => void }) => { const articles = useQuery(ArticleResource.getList.schema); if (articles === undefined) return
Articles not found
; return ; }; const ListQueryView = ({ onSelect }: { onSelect: (id: number) => void }) => { const [toggle, setToggle] = useState(false); return ( <> {' '} {toggle &&
Toggle state: {toggle.toString()}
} ); }; const DetailView = ({ id }: { id: number }) => { const [toggle, setToggle] = useState(false); return ( <> {' '} {toggle &&
Toggle state: {toggle.toString()}
} ); }; const TestComponent = ({ intervalMS = GC_INTERVAL, expiryMultiplier = 2, }: GCOptions) => { const [view, setView] = useState<'list' | 'detail' | 'querylist' | 'blank'>( 'list', ); const [selectedId, setSelectedId] = useState(null); return (
setView('list')}>Home
setView('querylist')}>Query
setView('blank')}>Blank
{ setSelectedId(1); setView('detail'); }} > Try first article
Loading...}> {view === 'list' ? { setSelectedId(id); setView('detail'); }} /> : view === 'querylist' ? { setSelectedId(id); setView('detail'); }} /> : view === 'blank' ?
This is an empty page
: selectedId !== null && }
); }; describe('Integration Garbage Collection Web', () => { mockGetList.mockReturnValue([ { id: 1, title: 'Article 1', content: 'Content 1' }, { id: 2, title: 'Article 2', content: 'Content 2' }, ]); mockGet.mockReturnValue({ id: 1, title: 'Article 1', content: 'Content 1', }); it('should count properly with useSuspense', async () => { jest.useFakeTimers(); await testSuspenseFlow(); jest.useRealTimers(); }); it('should expiryMultiplier GC option should work', async () => { jest.useFakeTimers(); await testSuspenseFlow( (ArticleResource.getList.dataExpiryLength ?? 60000) * 4, { intervalMS: GC_INTERVAL, expiryMultiplier: 4 }, ); jest.useRealTimers(); }); it('should count properly with useQuery', async () => { jest.useFakeTimers(); render(); // only needed for react 17 await act(async () => { await jest.runOnlyPendingTimersAsync(); }); // Initial render, should show list view expect(await screen.findByText('Article 1')).toBeInTheDocument(); // Switch to detail view act(() => { screen.getByText('Article 1').click(); }); // Detail view should render expect(await screen.findByText('Content 1')).toBeInTheDocument(); // Jest time pass to trigger sweep but not expired act(() => { jest.advanceTimersByTime(GC_INTERVAL); }); // Switch to useQuery() list view act(() => { screen.getByText('Query').click(); }); // List view should instantly render expect(await screen.findByText('Article 1')).toBeInTheDocument(); // Jest time pass to expiry act(() => { jest.advanceTimersByTime( Math.max( (ArticleResource.getList.dataExpiryLength ?? 60000) * 2, GC_INTERVAL, ), ); }); expect(await screen.findByText('Article 1')).toBeInTheDocument(); // Re-render query list view to make sure it still renders act(() => { screen.getByText('Toggle Re-render').click(); }); expect(await screen.findByText('Toggle state: true')).toBeInTheDocument(); expect(await screen.findByText('Article 1')).toBeInTheDocument(); act(() => { screen.getByText('Blank').click(); }); // enough time to GC act(() => { jest.advanceTimersByTime(GC_INTERVAL); }); // Visit detail view and see suspense fallback act(() => { screen.getByText('Try first article').click(); }); expect(screen.getByText('Loading...')).toBeInTheDocument(); // Visit useQuery() list view and see query fallback act(() => { screen.getByText('Query').click(); }); expect(screen.getByText('Articles not found')).toBeInTheDocument(); jest.useRealTimers(); }); }); async function testSuspenseFlow( expiryTime = (ArticleResource.getList.dataExpiryLength ?? 60000) * 2, gcOptions: GCOptions = { intervalMS: GC_INTERVAL }, ) { render(); // only needed for react 17 await act(async () => { await jest.runOnlyPendingTimersAsync(); }); // Initial render, should show list view expect(await screen.findByText('Article 1')).toBeInTheDocument(); // Switch to detail view act(() => { screen.getByText('Article 1').click(); }); // Detail view should render expect(await screen.findByText('Content 1')).toBeInTheDocument(); // Jest time pass to trigger sweep but not expired act(() => { jest.advanceTimersByTime(GC_INTERVAL); }); // Switch back to list view act(() => { screen.getByText('Home').click(); }); // List view should instantly render expect(await screen.findByText('Article 1')).toBeInTheDocument(); // Switch back to detail view act(() => { screen.getByText('Article 1').click(); }); // Jest should not expire yet act(() => { jest.advanceTimersByTime(expiryTime / 2 + GC_INTERVAL); }); await testGCSweep(); expect(screen.getByText('Article 1')).toBeInTheDocument(); act(() => { screen.getByText('Article 1').click(); }); // Now it's enough time to expire act(() => { jest.advanceTimersByTime(expiryTime / 2 + GC_INTERVAL); }); await testGCSweep(); expect(screen.getByText('Loading...')).toBeInTheDocument(); async function testGCSweep() { expect(await screen.findByText('Content 1')).toBeInTheDocument(); // Re-render detail view to make sure it still renders act(() => { screen.getByText('Toggle Re-render').click(); }); expect(await screen.findByText('Toggle state: true')).toBeInTheDocument(); expect(await screen.findByText('Content 1')).toBeInTheDocument(); // Visit list view and see suspense fallback act(() => { screen.getByText('Home').click(); }); } }