@@ -60,70 +60,86 @@ testGateReact19('resolves timer-controlled promise', async () => {
60
60
expect ( screen . queryByText ( 'Loading...' ) ) . not . toBeOnTheScreen ( ) ;
61
61
} ) ;
62
62
63
- function DelayedSuspending ( { delay } : { delay : number } ) {
63
+ function DelayedSuspending ( { delay, id } : { delay : number ; id : string } ) {
64
+ let resolvePromise : ( value : string ) => void ;
64
65
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 ]
68
70
) ;
69
71
70
72
const data = React . use ( promise ) ;
71
73
return < View testID = { `delayed-content-${ data } ` } /> ;
72
74
}
73
75
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
+
75
82
await renderAsync (
76
83
< 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 } />
82
86
</ React . Suspense >
87
+ < View testID = "outside-suspense" />
83
88
</ View > ,
84
89
) ;
85
90
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 ( ) ;
88
93
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 ( ) ;
97
98
} ) ;
98
99
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
+ }
106
108
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 ;
110
119
}
111
-
112
- return < View testID = { `interval-content-${ count } ` } /> ;
113
120
}
114
121
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
+
116
128
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 > ,
120
134
) ;
121
135
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 ( ) ;
127
143
} ) ;
128
144
129
145
function AnimationSuspending ( ) {
0 commit comments