@@ -24,6 +24,7 @@ describe('ReactDOMServerPartialHydration', () => {
2424
2525 ReactFeatureFlags = require ( 'shared/ReactFeatureFlags' ) ;
2626 ReactFeatureFlags . enableSuspenseServerRenderer = true ;
27+ ReactFeatureFlags . enableSuspenseCallback = true ;
2728
2829 React = require ( 'react' ) ;
2930 ReactDOM = require ( 'react-dom' ) ;
@@ -92,6 +93,153 @@ describe('ReactDOMServerPartialHydration', () => {
9293 expect ( ref . current ) . toBe ( span ) ;
9394 } ) ;
9495
96+ it ( 'calls the hydration callbacks after hydration or deletion' , async ( ) => {
97+ let suspend = false ;
98+ let resolve ;
99+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
100+ function Child ( ) {
101+ if ( suspend ) {
102+ throw promise ;
103+ } else {
104+ return 'Hello' ;
105+ }
106+ }
107+
108+ let suspend2 = false ;
109+ let promise2 = new Promise ( ( ) => { } ) ;
110+ function Child2 ( ) {
111+ if ( suspend2 ) {
112+ throw promise2 ;
113+ } else {
114+ return 'World' ;
115+ }
116+ }
117+
118+ function App ( { value} ) {
119+ return (
120+ < div >
121+ < Suspense fallback = "Loading..." >
122+ < Child />
123+ </ Suspense >
124+ < Suspense fallback = "Loading..." >
125+ < Child2 value = { value } />
126+ </ Suspense >
127+ </ div >
128+ ) ;
129+ }
130+
131+ // First we render the final HTML. With the streaming renderer
132+ // this may have suspense points on the server but here we want
133+ // to test the completed HTML. Don't suspend on the server.
134+ suspend = false ;
135+ suspend2 = false ;
136+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
137+
138+ let container = document . createElement ( 'div' ) ;
139+ container . innerHTML = finalHTML ;
140+
141+ let hydrated = [ ] ;
142+ let deleted = [ ] ;
143+
144+ // On the client we don't have all data yet but we want to start
145+ // hydrating anyway.
146+ suspend = true ;
147+ suspend2 = true ;
148+ let root = ReactDOM . unstable_createRoot ( container , {
149+ hydrate : true ,
150+ hydrationOptions : {
151+ onHydrated ( node ) {
152+ hydrated . push ( node ) ;
153+ } ,
154+ onDeleted ( node ) {
155+ deleted . push ( node ) ;
156+ } ,
157+ } ,
158+ } ) ;
159+ act ( ( ) => {
160+ root . render ( < App /> ) ;
161+ } ) ;
162+
163+ expect ( hydrated . length ) . toBe ( 0 ) ;
164+ expect ( deleted . length ) . toBe ( 0 ) ;
165+
166+ await act ( async ( ) => {
167+ // Resolving the promise should continue hydration
168+ suspend = false ;
169+ resolve ( ) ;
170+ await promise ;
171+ } ) ;
172+
173+ expect ( hydrated . length ) . toBe ( 1 ) ;
174+ expect ( deleted . length ) . toBe ( 0 ) ;
175+
176+ // Performing an update should force it to delete the boundary
177+ root . render ( < App value = { true } /> ) ;
178+
179+ Scheduler . unstable_flushAll ( ) ;
180+ jest . runAllTimers ( ) ;
181+
182+ expect ( hydrated . length ) . toBe ( 1 ) ;
183+ expect ( deleted . length ) . toBe ( 1 ) ;
184+ } ) ;
185+
186+ it ( 'calls the onDeleted hydration callback if the parent gets deleted' , async ( ) => {
187+ let suspend = false ;
188+ let promise = new Promise ( ( ) => { } ) ;
189+ function Child ( ) {
190+ if ( suspend ) {
191+ throw promise ;
192+ } else {
193+ return 'Hello' ;
194+ }
195+ }
196+
197+ function App ( { deleted} ) {
198+ if ( deleted ) {
199+ return null ;
200+ }
201+ return (
202+ < div >
203+ < Suspense fallback = "Loading..." >
204+ < Child />
205+ </ Suspense >
206+ </ div >
207+ ) ;
208+ }
209+
210+ suspend = false ;
211+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
212+
213+ let container = document . createElement ( 'div' ) ;
214+ container . innerHTML = finalHTML ;
215+
216+ let deleted = [ ] ;
217+
218+ // On the client we don't have all data yet but we want to start
219+ // hydrating anyway.
220+ suspend = true ;
221+ let root = ReactDOM . unstable_createRoot ( container , {
222+ hydrate : true ,
223+ hydrationOptions : {
224+ onDeleted ( node ) {
225+ deleted . push ( node ) ;
226+ } ,
227+ } ,
228+ } ) ;
229+ act ( ( ) => {
230+ root . render ( < App /> ) ;
231+ } ) ;
232+
233+ expect ( deleted . length ) . toBe ( 0 ) ;
234+
235+ act ( ( ) => {
236+ root . render ( < App deleted = { true } /> ) ;
237+ } ) ;
238+
239+ // The callback should have been invoked.
240+ expect ( deleted . length ) . toBe ( 1 ) ;
241+ } ) ;
242+
95243 it ( 'warns and replaces the boundary content in legacy mode' , async ( ) => {
96244 let suspend = false ;
97245 let resolve ;
0 commit comments