Skip to content

Commit 45e7e0e

Browse files
committed
suspense test 2
1 parent 3121bd8 commit 45e7e0e

File tree

2 files changed

+89
-59
lines changed

2 files changed

+89
-59
lines changed

src/__tests__/suspense-fake-timers.test.tsx

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,70 +60,86 @@ testGateReact19('resolves timer-controlled promise', async () => {
6060
expect(screen.queryByText('Loading...')).not.toBeOnTheScreen();
6161
});
6262

63-
function DelayedSuspending({ delay }: { delay: number }) {
63+
function DelayedSuspending({ delay, id }: { delay: number; id: string }) {
64+
let resolvePromise: (value: string) => void;
6465
const promise = React.useMemo(() =>
65-
new Promise((resolve) => {
66-
setTimeout(() => resolve(`data-${delay}`), delay);
67-
}), [delay]
66+
new Promise<string>((resolve) => {
67+
resolvePromise = resolve;
68+
setTimeout(() => resolve(`data-${id}`), delay);
69+
}), [delay, id]
6870
);
6971

7072
const data = React.use(promise);
7173
return <View testID={`delayed-content-${data}`} />;
7274
}
7375

74-
testGateReact19('handles multiple delays with fake timers', async () => {
76+
testGateReact19('handles timer-based promises with fake timers', async () => {
77+
let resolveManual: (value: unknown) => void;
78+
const manualPromise = new Promise((resolve) => {
79+
resolveManual = resolve;
80+
});
81+
7582
await renderAsync(
7683
<View>
77-
<React.Suspense fallback={<Text>Fast Loading...</Text>}>
78-
<DelayedSuspending delay={50} />
79-
</React.Suspense>
80-
<React.Suspense fallback={<Text>Slow Loading...</Text>}>
81-
<DelayedSuspending delay={200} />
84+
<React.Suspense fallback={<Text>Manual Loading...</Text>}>
85+
<Suspending promise={manualPromise} />
8286
</React.Suspense>
87+
<View testID="outside-suspense" />
8388
</View>,
8489
);
8590

86-
expect(screen.getByText('Fast Loading...')).toBeOnTheScreen();
87-
expect(screen.getByText('Slow Loading...')).toBeOnTheScreen();
91+
expect(screen.getByText('Manual Loading...')).toBeOnTheScreen();
92+
expect(screen.getByTestId('outside-suspense')).toBeOnTheScreen();
8893

89-
// Fast timer completes first
90-
expect(await screen.findByTestId('delayed-content-data-50')).toBeOnTheScreen();
91-
expect(screen.queryByText('Fast Loading...')).not.toBeOnTheScreen();
92-
expect(screen.getByText('Slow Loading...')).toBeOnTheScreen();
93-
94-
// Slow timer completes later
95-
expect(await screen.findByTestId('delayed-content-data-200')).toBeOnTheScreen();
96-
expect(screen.queryByText('Slow Loading...')).not.toBeOnTheScreen();
94+
// eslint-disable-next-line require-await
95+
await act(async () => resolveManual(null));
96+
expect(screen.getByTestId('content')).toBeOnTheScreen();
97+
expect(screen.queryByText('Manual Loading...')).not.toBeOnTheScreen();
9798
});
9899

99-
function IntervalSuspending({ interval }: { interval: number }) {
100-
const [count, setCount] = React.useState(0);
101-
102-
React.useEffect(() => {
103-
const timer = setInterval(() => setCount(c => c + 1), interval);
104-
return () => clearInterval(timer);
105-
}, [interval]);
100+
class ErrorBoundary extends React.Component<
101+
{ children: React.ReactNode; fallback?: React.ReactNode },
102+
{ hasError: boolean }
103+
> {
104+
constructor(props: { children: React.ReactNode; fallback?: React.ReactNode }) {
105+
super(props);
106+
this.state = { hasError: false };
107+
}
106108

107-
if (count < 3) {
108-
const promise = new Promise(() => {}); // Never resolves until count >= 3
109-
React.use(promise);
109+
static getDerivedStateFromError() {
110+
return { hasError: true };
111+
}
112+
113+
render() {
114+
if (this.state.hasError) {
115+
return this.props.fallback || <Text>Something went wrong.</Text>;
116+
}
117+
118+
return this.props.children;
110119
}
111-
112-
return <View testID={`interval-content-${count}`} />;
113120
}
114121

115-
testGateReact19('handles interval-based suspense with fake timers', async () => {
122+
testGateReact19('handles suspense with error boundary in fake timers', async () => {
123+
let rejectPromise: (error: Error) => void;
124+
const promise = new Promise<unknown>((_, reject) => {
125+
rejectPromise = reject;
126+
});
127+
116128
await renderAsync(
117-
<React.Suspense fallback={<Text>Interval Loading...</Text>}>
118-
<IntervalSuspending interval={100} />
119-
</React.Suspense>,
129+
<ErrorBoundary fallback={<Text>Error occurred</Text>}>
130+
<React.Suspense fallback={<Text>Loading...</Text>}>
131+
<Suspending promise={promise} />
132+
</React.Suspense>
133+
</ErrorBoundary>,
120134
);
121135

122-
expect(screen.getByText('Interval Loading...')).toBeOnTheScreen();
123-
124-
// Should resolve after enough intervals pass
125-
expect(await screen.findByTestId('interval-content-3')).toBeOnTheScreen();
126-
expect(screen.queryByText('Interval Loading...')).not.toBeOnTheScreen();
136+
expect(screen.getByText('Loading...')).toBeOnTheScreen();
137+
138+
// eslint-disable-next-line require-await
139+
await act(async () => rejectPromise(new Error('Test error')));
140+
141+
expect(screen.getByText('Error occurred')).toBeOnTheScreen();
142+
expect(screen.queryByText('Loading...')).not.toBeOnTheScreen();
127143
});
128144

129145
function AnimationSuspending() {

src/__tests__/suspense.test.tsx

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,14 @@ testGateReact19('handles nested suspense boundaries', async () => {
141141
expect(screen.queryByText('Inner Loading...')).not.toBeOnTheScreen();
142142
});
143143

144-
function MultipleSuspending({ promises }: { promises: Promise<unknown>[] }) {
145-
promises.forEach((promise) => React.use(promise));
146-
return <View testID="multiple-content" />;
144+
function FirstSuspending({ promise }: { promise: Promise<unknown> }) {
145+
React.use(promise);
146+
return <View testID="first-resolved" />;
147+
}
148+
149+
function SecondSuspending({ promise }: { promise: Promise<unknown> }) {
150+
React.use(promise);
151+
return <View testID="second-resolved" />;
147152
}
148153

149154
testGateReact19('handles multiple suspending promises in same boundary', async () => {
@@ -159,29 +164,37 @@ testGateReact19('handles multiple suspending promises in same boundary', async (
159164

160165
await renderAsync(
161166
<React.Suspense fallback={<Text>Multiple Loading...</Text>}>
162-
<MultipleSuspending promises={[promise1, promise2]} />
167+
<FirstSuspending promise={promise1} />
168+
<SecondSuspending promise={promise2} />
163169
</React.Suspense>,
164170
);
165171

166172
expect(screen.getByText('Multiple Loading...')).toBeOnTheScreen();
167-
expect(screen.queryByTestId('multiple-content')).not.toBeOnTheScreen();
173+
expect(screen.queryByTestId('first-resolved')).not.toBeOnTheScreen();
174+
expect(screen.queryByTestId('second-resolved')).not.toBeOnTheScreen();
168175

169-
// Resolve first promise - should still be loading
176+
// Resolve first promise - should still be loading because second is pending
170177
// eslint-disable-next-line require-await
171178
await act(async () => resolvePromise1(null));
172179
expect(screen.getByText('Multiple Loading...')).toBeOnTheScreen();
173-
expect(screen.queryByTestId('multiple-content')).not.toBeOnTheScreen();
180+
expect(screen.queryByTestId('first-resolved')).not.toBeOnTheScreen();
181+
expect(screen.queryByTestId('second-resolved')).not.toBeOnTheScreen();
174182

175-
// Resolve second promise - should now render content
183+
// Resolve second promise - should now render all content
176184
// eslint-disable-next-line require-await
177185
await act(async () => resolvePromise2(null));
178-
expect(screen.getByTestId('multiple-content')).toBeOnTheScreen();
186+
expect(screen.getByTestId('first-resolved')).toBeOnTheScreen();
187+
expect(screen.getByTestId('second-resolved')).toBeOnTheScreen();
179188
expect(screen.queryByText('Multiple Loading...')).not.toBeOnTheScreen();
180189
});
181190

182-
function ConditionalSuspending({ shouldSuspend }: { shouldSuspend: boolean }) {
191+
function ConditionalSuspending({ shouldSuspend, promiseResolver }: { shouldSuspend: boolean; promiseResolver?: () => void }) {
183192
if (shouldSuspend) {
184-
const promise = new Promise(() => {}); // Never resolves
193+
const promise = React.useMemo(() => new Promise<void>((resolve) => {
194+
if (promiseResolver) {
195+
promiseResolver = resolve;
196+
}
197+
}), [promiseResolver]);
185198
React.use(promise);
186199
}
187200
return <View testID="conditional-content" />;
@@ -198,31 +211,32 @@ testGateReact19('handles conditional suspense', async () => {
198211
expect(screen.getByTestId('conditional-content')).toBeOnTheScreen();
199212
expect(screen.queryByText('Conditional Loading...')).not.toBeOnTheScreen();
200213

201-
// Re-render with suspense
214+
// Re-render with suspense - this creates a new component that will suspend
202215
await result.rerenderAsync(
203216
<React.Suspense fallback={<Text>Conditional Loading...</Text>}>
204217
<ConditionalSuspending shouldSuspend={true} />
205218
</React.Suspense>,
206219
);
207220

221+
// Should now be suspended
208222
expect(screen.getByText('Conditional Loading...')).toBeOnTheScreen();
209223
expect(screen.queryByTestId('conditional-content')).not.toBeOnTheScreen();
210224
});
211225

212226
function SuspendingWithState() {
213-
const [count, setCount] = React.useState(0);
227+
const [isReady, setIsReady] = React.useState(false);
214228

215229
React.useEffect(() => {
216-
const timer = setTimeout(() => setCount(1), 50);
230+
const timer = setTimeout(() => setIsReady(true), 100);
217231
return () => clearTimeout(timer);
218232
}, []);
219233

220-
if (count === 0) {
234+
if (!isReady) {
221235
const promise = new Promise(() => {}); // Never resolves
222236
React.use(promise);
223237
}
224238

225-
return <View testID={`state-content-${count}`} />;
239+
return <View testID="state-ready-content" />;
226240
}
227241

228242
testGateReact19('handles suspense with state updates', async () => {
@@ -233,9 +247,9 @@ testGateReact19('handles suspense with state updates', async () => {
233247
);
234248

235249
expect(screen.getByText('State Loading...')).toBeOnTheScreen();
236-
expect(screen.queryByTestId('state-content-0')).not.toBeOnTheScreen();
250+
expect(screen.queryByTestId('state-ready-content')).not.toBeOnTheScreen();
237251

238252
// Wait for state update to resolve suspense
239-
expect(await screen.findByTestId('state-content-1')).toBeOnTheScreen();
253+
expect(await screen.findByTestId('state-ready-content')).toBeOnTheScreen();
240254
expect(screen.queryByText('State Loading...')).not.toBeOnTheScreen();
241255
});

0 commit comments

Comments
 (0)