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();
});
}
}