@@ -56,3 +56,186 @@ testGateReact19('resolves timer-controlled promise', async () => {
56
56
expect ( screen . getByTestId ( 'sibling' ) ) . toBeOnTheScreen ( ) ;
57
57
expect ( screen . queryByText ( 'Loading...' ) ) . not . toBeOnTheScreen ( ) ;
58
58
} ) ;
59
+
60
+ function SuspendingWithError ( { promise } : { promise : Promise < unknown > } ) {
61
+ React . use ( promise ) ;
62
+ return < View testID = "error-content" /> ;
63
+ }
64
+
65
+ class ErrorBoundary extends React . Component <
66
+ { children : React . ReactNode ; fallback ?: React . ReactNode } ,
67
+ { hasError : boolean }
68
+ > {
69
+ constructor ( props : { children : React . ReactNode ; fallback ?: React . ReactNode } ) {
70
+ super ( props ) ;
71
+ this . state = { hasError : false } ;
72
+ }
73
+
74
+ static getDerivedStateFromError ( ) {
75
+ return { hasError : true } ;
76
+ }
77
+
78
+ render ( ) {
79
+ if ( this . state . hasError ) {
80
+ return this . props . fallback || < Text > Something went wrong.</ Text > ;
81
+ }
82
+
83
+ return this . props . children ;
84
+ }
85
+ }
86
+
87
+ testGateReact19 ( 'handles promise rejection with error boundary' , async ( ) => {
88
+ let rejectPromise : ( error : Error ) => void ;
89
+ const promise = new Promise < unknown > ( ( _ , reject ) => {
90
+ rejectPromise = reject ;
91
+ } ) ;
92
+
93
+ await renderAsync (
94
+ < ErrorBoundary fallback = { < Text > Error occurred</ Text > } >
95
+ < React . Suspense fallback = { < Text > Loading...</ Text > } >
96
+ < SuspendingWithError promise = { promise } />
97
+ </ React . Suspense >
98
+ </ ErrorBoundary > ,
99
+ ) ;
100
+
101
+ expect ( screen . getByText ( 'Loading...' ) ) . toBeOnTheScreen ( ) ;
102
+ expect ( screen . queryByTestId ( 'error-content' ) ) . not . toBeOnTheScreen ( ) ;
103
+
104
+ // eslint-disable-next-line require-await
105
+ await act ( async ( ) => rejectPromise ( new Error ( 'Test error' ) ) ) ;
106
+
107
+ expect ( screen . getByText ( 'Error occurred' ) ) . toBeOnTheScreen ( ) ;
108
+ expect ( screen . queryByText ( 'Loading...' ) ) . not . toBeOnTheScreen ( ) ;
109
+ expect ( screen . queryByTestId ( 'error-content' ) ) . not . toBeOnTheScreen ( ) ;
110
+ } ) ;
111
+
112
+ function NestedSuspending ( { promise } : { promise : Promise < unknown > } ) {
113
+ React . use ( promise ) ;
114
+ return (
115
+ < React . Suspense fallback = { < Text > Inner Loading...</ Text > } >
116
+ < View testID = "inner-resolved" />
117
+ </ React . Suspense >
118
+ ) ;
119
+ }
120
+
121
+ testGateReact19 ( 'handles nested suspense boundaries' , async ( ) => {
122
+ let resolveOuterPromise : ( value : unknown ) => void ;
123
+ const outerPromise = new Promise ( ( resolve ) => {
124
+ resolveOuterPromise = resolve ;
125
+ } ) ;
126
+
127
+ await renderAsync (
128
+ < React . Suspense fallback = { < Text > Outer Loading...</ Text > } >
129
+ < NestedSuspending promise = { outerPromise } />
130
+ </ React . Suspense > ,
131
+ ) ;
132
+
133
+ expect ( screen . getByText ( 'Outer Loading...' ) ) . toBeOnTheScreen ( ) ;
134
+ expect ( screen . queryByText ( 'Inner Loading...' ) ) . not . toBeOnTheScreen ( ) ;
135
+
136
+ // eslint-disable-next-line require-await
137
+ await act ( async ( ) => resolveOuterPromise ( null ) ) ;
138
+
139
+ expect ( screen . getByTestId ( 'inner-resolved' ) ) . toBeOnTheScreen ( ) ;
140
+ expect ( screen . queryByText ( 'Outer Loading...' ) ) . not . toBeOnTheScreen ( ) ;
141
+ expect ( screen . queryByText ( 'Inner Loading...' ) ) . not . toBeOnTheScreen ( ) ;
142
+ } ) ;
143
+
144
+ function MultipleSuspending ( { promises } : { promises : Promise < unknown > [ ] } ) {
145
+ promises . forEach ( ( promise ) => React . use ( promise ) ) ;
146
+ return < View testID = "multiple-content" /> ;
147
+ }
148
+
149
+ testGateReact19 ( 'handles multiple suspending promises in same boundary' , async ( ) => {
150
+ let resolvePromise1 : ( value : unknown ) => void ;
151
+ let resolvePromise2 : ( value : unknown ) => void ;
152
+
153
+ const promise1 = new Promise ( ( resolve ) => {
154
+ resolvePromise1 = resolve ;
155
+ } ) ;
156
+ const promise2 = new Promise ( ( resolve ) => {
157
+ resolvePromise2 = resolve ;
158
+ } ) ;
159
+
160
+ await renderAsync (
161
+ < React . Suspense fallback = { < Text > Multiple Loading...</ Text > } >
162
+ < MultipleSuspending promises = { [ promise1 , promise2 ] } />
163
+ </ React . Suspense > ,
164
+ ) ;
165
+
166
+ expect ( screen . getByText ( 'Multiple Loading...' ) ) . toBeOnTheScreen ( ) ;
167
+ expect ( screen . queryByTestId ( 'multiple-content' ) ) . not . toBeOnTheScreen ( ) ;
168
+
169
+ // Resolve first promise - should still be loading
170
+ // eslint-disable-next-line require-await
171
+ await act ( async ( ) => resolvePromise1 ( null ) ) ;
172
+ expect ( screen . getByText ( 'Multiple Loading...' ) ) . toBeOnTheScreen ( ) ;
173
+ expect ( screen . queryByTestId ( 'multiple-content' ) ) . not . toBeOnTheScreen ( ) ;
174
+
175
+ // Resolve second promise - should now render content
176
+ // eslint-disable-next-line require-await
177
+ await act ( async ( ) => resolvePromise2 ( null ) ) ;
178
+ expect ( screen . getByTestId ( 'multiple-content' ) ) . toBeOnTheScreen ( ) ;
179
+ expect ( screen . queryByText ( 'Multiple Loading...' ) ) . not . toBeOnTheScreen ( ) ;
180
+ } ) ;
181
+
182
+ function ConditionalSuspending ( { shouldSuspend } : { shouldSuspend : boolean } ) {
183
+ if ( shouldSuspend ) {
184
+ const promise = new Promise ( ( ) => { } ) ; // Never resolves
185
+ React . use ( promise ) ;
186
+ }
187
+ return < View testID = "conditional-content" /> ;
188
+ }
189
+
190
+ testGateReact19 ( 'handles conditional suspense' , async ( ) => {
191
+ const result = await renderAsync (
192
+ < React . Suspense fallback = { < Text > Conditional Loading...</ Text > } >
193
+ < ConditionalSuspending shouldSuspend = { false } />
194
+ </ React . Suspense > ,
195
+ ) ;
196
+
197
+ // Should render immediately when not suspending
198
+ expect ( screen . getByTestId ( 'conditional-content' ) ) . toBeOnTheScreen ( ) ;
199
+ expect ( screen . queryByText ( 'Conditional Loading...' ) ) . not . toBeOnTheScreen ( ) ;
200
+
201
+ // Re-render with suspense
202
+ await result . rerenderAsync (
203
+ < React . Suspense fallback = { < Text > Conditional Loading...</ Text > } >
204
+ < ConditionalSuspending shouldSuspend = { true } />
205
+ </ React . Suspense > ,
206
+ ) ;
207
+
208
+ expect ( screen . getByText ( 'Conditional Loading...' ) ) . toBeOnTheScreen ( ) ;
209
+ expect ( screen . queryByTestId ( 'conditional-content' ) ) . not . toBeOnTheScreen ( ) ;
210
+ } ) ;
211
+
212
+ function SuspendingWithState ( ) {
213
+ const [ count , setCount ] = React . useState ( 0 ) ;
214
+
215
+ React . useEffect ( ( ) => {
216
+ const timer = setTimeout ( ( ) => setCount ( 1 ) , 50 ) ;
217
+ return ( ) => clearTimeout ( timer ) ;
218
+ } , [ ] ) ;
219
+
220
+ if ( count === 0 ) {
221
+ const promise = new Promise ( ( ) => { } ) ; // Never resolves
222
+ React . use ( promise ) ;
223
+ }
224
+
225
+ return < View testID = { `state-content-${ count } ` } /> ;
226
+ }
227
+
228
+ testGateReact19 ( 'handles suspense with state updates' , async ( ) => {
229
+ await renderAsync (
230
+ < React . Suspense fallback = { < Text > State Loading...</ Text > } >
231
+ < SuspendingWithState />
232
+ </ React . Suspense > ,
233
+ ) ;
234
+
235
+ expect ( screen . getByText ( 'State Loading...' ) ) . toBeOnTheScreen ( ) ;
236
+ expect ( screen . queryByTestId ( 'state-content-0' ) ) . not . toBeOnTheScreen ( ) ;
237
+
238
+ // Wait for state update to resolve suspense
239
+ expect ( await screen . findByTestId ( 'state-content-1' ) ) . toBeOnTheScreen ( ) ;
240
+ expect ( screen . queryByText ( 'State Loading...' ) ) . not . toBeOnTheScreen ( ) ;
241
+ } ) ;
0 commit comments