import {
AsyncBoundary,
DataProvider,
GCPolicy,
useSuspense,
} from '@data-client/react';
import { MockResolver } from '@data-client/test';
import { render, screen, act, fireEvent } from '@testing-library/react-native';
import { ArticleResource } from '__tests__/new';
import { useState } from 'react';
import {
View,
Text,
Button,
TouchableOpacity,
InteractionManager,
} from 'react-native';
const mockGetList = jest.fn();
const mockGet = jest.fn();
const GC_INTERVAL = 5000;
const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => {
const articles = useSuspense(ArticleResource.getList);
return (
{articles.map(article => (
onSelect(article.id ?? 0)}
>
{article.title}
))}
);
};
const DetailView = ({ id }: { id: number }) => {
const article = useSuspense(ArticleResource.get, { id });
const [toggle, setToggle] = useState(false);
return (
{article.title}
{article.content}
);
};
const TestComponent = () => {
const [view, setView] = useState<'list' | 'detail'>('list');
const [selectedId, setSelectedId] = useState(null);
return (
setView('list')}>
Home
Loading...}>
{view === 'list' ?
{
setSelectedId(id);
setView('detail');
}}
/>
: selectedId !== null && }
);
};
// Test cases
describe('Integration Garbage Collection React Native', () => {
let warnSpy: jest.SpyInstance;
let errorSpy: jest.SpyInstance;
beforeAll(() => {
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterAll(() => {
warnSpy.mockRestore();
errorSpy.mockRestore();
});
it('should count properly with useSuspense', async () => {
jest.useFakeTimers();
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',
});
render();
await act(async () => {
jest.advanceTimersByTime(1000);
InteractionManager.setDeadline(0);
await jest.runOnlyPendingTimersAsync();
});
// Initial render, should show list view
expect(await screen.findByText('Article 1')).toBeTruthy();
// Switch to detail view
act(() => {
fireEvent.press(screen.getByText('Article 1'));
});
// Detail view should render
expect(await screen.findByText('Content 1')).toBeTruthy();
// Jest time pass to trigger sweep but not expired
act(() => {
jest.advanceTimersByTime(GC_INTERVAL);
InteractionManager.setDeadline(0);
});
// Switch back to list view
act(() => {
fireEvent.press(screen.getByText('Home'));
});
// List view should instantly render
expect(await screen.findByText('Article 1')).toBeTruthy();
// Switch back to detail view
act(() => {
fireEvent.press(screen.getByText('Article 1'));
});
// Jest time pass to expiry
await act(async () => {
jest.advanceTimersByTime(
Math.max(
(ArticleResource.getList.dataExpiryLength ?? 60000) * 2,
GC_INTERVAL,
),
);
InteractionManager.setDeadline(0);
await jest.runOnlyPendingTimersAsync();
});
expect(await screen.findByText('Content 1')).toBeTruthy();
// Re-render detail view to make sure it still renders
act(() => {
fireEvent.press(screen.getByText('Toggle Re-render'));
});
expect(await screen.findByText('Toggle state: true')).toBeTruthy();
expect(await screen.findByText('Content 1')).toBeTruthy();
// Visit list view and see suspense fallback
act(() => {
fireEvent.press(screen.getByText('Home'));
});
expect(screen.getByText('Loading...')).toBeTruthy();
await jest.runOnlyPendingTimersAsync();
jest.useRealTimers();
});
});