11
11
12
12
let React ;
13
13
let ReactDOM ;
14
+ let ReactDOMClient ;
14
15
let ReactDOMServer ;
16
+ let act ;
17
+ let Scheduler ;
18
+ let assertLog ;
15
19
16
20
function getTestDocument ( markup ) {
17
21
const doc = document . implementation . createHTMLDocument ( '' ) ;
@@ -28,11 +32,15 @@ describe('rendering React components at document', () => {
28
32
beforeEach ( ( ) => {
29
33
React = require ( 'react' ) ;
30
34
ReactDOM = require ( 'react-dom' ) ;
35
+ ReactDOMClient = require ( 'react-dom/client' ) ;
31
36
ReactDOMServer = require ( 'react-dom/server' ) ;
37
+ act = require ( 'internal-test-utils' ) . act ;
38
+ assertLog = require ( 'internal-test-utils' ) . assertLog ;
39
+ Scheduler = require ( 'scheduler' ) ;
32
40
} ) ;
33
41
34
42
describe ( 'with new explicit hydration API' , ( ) => {
35
- it ( 'should be able to adopt server markup' , ( ) => {
43
+ it ( 'should be able to adopt server markup' , async ( ) => {
36
44
class Root extends React . Component {
37
45
render ( ) {
38
46
return (
@@ -51,16 +59,21 @@ describe('rendering React components at document', () => {
51
59
const testDocument = getTestDocument ( markup ) ;
52
60
const body = testDocument . body ;
53
61
54
- ReactDOM . hydrate ( < Root hello = "world" /> , testDocument ) ;
62
+ let root ;
63
+ await act ( ( ) => {
64
+ root = ReactDOMClient . hydrateRoot ( testDocument , < Root hello = "world" /> ) ;
65
+ } ) ;
55
66
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
56
67
57
- ReactDOM . hydrate ( < Root hello = "moon" /> , testDocument ) ;
68
+ await act ( ( ) => {
69
+ root . render ( < Root hello = "moon" /> ) ;
70
+ } ) ;
58
71
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello moon' ) ;
59
72
60
73
expect ( body === testDocument . body ) . toBe ( true ) ;
61
74
} ) ;
62
75
63
- it ( 'should be able to unmount component from document node, but leaves singleton nodes intact' , ( ) => {
76
+ it ( 'should be able to unmount component from document node, but leaves singleton nodes intact' , async ( ) => {
64
77
class Root extends React . Component {
65
78
render ( ) {
66
79
return (
@@ -76,23 +89,26 @@ describe('rendering React components at document', () => {
76
89
77
90
const markup = ReactDOMServer . renderToString ( < Root /> ) ;
78
91
const testDocument = getTestDocument ( markup ) ;
79
- ReactDOM . hydrate ( < Root /> , testDocument ) ;
92
+ let root ;
93
+ await act ( ( ) => {
94
+ root = ReactDOMClient . hydrateRoot ( testDocument , < Root /> ) ;
95
+ } ) ;
80
96
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
81
97
82
98
const originalDocEl = testDocument . documentElement ;
83
99
const originalHead = testDocument . head ;
84
100
const originalBody = testDocument . body ;
85
101
86
102
// When we unmount everything is removed except the singleton nodes of html, head, and body
87
- ReactDOM . unmountComponentAtNode ( testDocument ) ;
103
+ root . unmount ( ) ;
88
104
expect ( testDocument . firstChild ) . toBe ( originalDocEl ) ;
89
105
expect ( testDocument . head ) . toBe ( originalHead ) ;
90
106
expect ( testDocument . body ) . toBe ( originalBody ) ;
91
107
expect ( originalBody . firstChild ) . toEqual ( null ) ;
92
108
expect ( originalHead . firstChild ) . toEqual ( null ) ;
93
109
} ) ;
94
110
95
- it ( 'should not be able to switch root constructors' , ( ) => {
111
+ it ( 'should not be able to switch root constructors' , async ( ) => {
96
112
class Component extends React . Component {
97
113
render ( ) {
98
114
return (
@@ -122,17 +138,21 @@ describe('rendering React components at document', () => {
122
138
const markup = ReactDOMServer . renderToString ( < Component /> ) ;
123
139
const testDocument = getTestDocument ( markup ) ;
124
140
125
- ReactDOM . hydrate ( < Component /> , testDocument ) ;
141
+ let root ;
142
+ await act ( ( ) => {
143
+ root = ReactDOMClient . hydrateRoot ( testDocument , < Component /> ) ;
144
+ } ) ;
126
145
127
146
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
128
147
129
- // This works but is probably a bad idea.
130
- ReactDOM . hydrate ( < Component2 /> , testDocument ) ;
148
+ await act ( ( ) => {
149
+ root . render ( < Component2 /> ) ;
150
+ } ) ;
131
151
132
152
expect ( testDocument . body . innerHTML ) . toBe ( 'Goodbye world' ) ;
133
153
} ) ;
134
154
135
- it ( 'should be able to mount into document' , ( ) => {
155
+ it ( 'should be able to mount into document' , async ( ) => {
136
156
class Component extends React . Component {
137
157
render ( ) {
138
158
return (
@@ -151,40 +171,80 @@ describe('rendering React components at document', () => {
151
171
) ;
152
172
const testDocument = getTestDocument ( markup ) ;
153
173
154
- ReactDOM . hydrate ( < Component text = "Hello world" /> , testDocument ) ;
174
+ await act ( ( ) => {
175
+ ReactDOMClient . hydrateRoot (
176
+ testDocument ,
177
+ < Component text = "Hello world" /> ,
178
+ ) ;
179
+ } ) ;
155
180
156
181
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
157
182
} ) ;
158
183
159
- it ( 'cannot render over an existing text child at the root' , ( ) => {
184
+ it ( 'cannot render over an existing text child at the root' , async ( ) => {
160
185
const container = document . createElement ( 'div' ) ;
161
186
container . textContent = 'potato' ;
162
- expect ( ( ) => ReactDOM . hydrate ( < div > parsnip</ div > , container ) ) . toErrorDev (
163
- 'Expected server HTML to contain a matching <div> in <div>.' ,
187
+
188
+ expect ( ( ) => {
189
+ ReactDOM . flushSync ( ( ) => {
190
+ ReactDOMClient . hydrateRoot ( container , < div > parsnip</ div > , {
191
+ onRecoverableError : error => {
192
+ Scheduler . log ( 'Log recoverable error: ' + error . message ) ;
193
+ } ,
194
+ } ) ;
195
+ } ) ;
196
+ } ) . toErrorDev (
197
+ [
198
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.' ,
199
+ 'Expected server HTML to contain a matching <div> in <div>.' ,
200
+ ] ,
201
+ { withoutStack : 1 } ,
164
202
) ;
203
+
204
+ assertLog ( [
205
+ 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.' ,
206
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
207
+ ] ) ;
208
+
165
209
// This creates an unfortunate double text case.
166
- expect ( container . textContent ) . toBe ( 'potatoparsnip ' ) ;
210
+ expect ( container . textContent ) . toBe ( 'parsnip ' ) ;
167
211
} ) ;
168
212
169
- it ( 'renders over an existing nested text child without throwing' , ( ) => {
213
+ it ( 'renders over an existing nested text child without throwing' , async ( ) => {
170
214
const container = document . createElement ( 'div' ) ;
171
215
const wrapper = document . createElement ( 'div' ) ;
172
216
wrapper . textContent = 'potato' ;
173
217
container . appendChild ( wrapper ) ;
174
- expect ( ( ) =>
175
- ReactDOM . hydrate (
176
- < div >
177
- < div > parsnip</ div >
178
- </ div > ,
179
- container ,
180
- ) ,
181
- ) . toErrorDev (
182
- 'Expected server HTML to contain a matching <div> in <div>.' ,
218
+ expect ( ( ) => {
219
+ ReactDOM . flushSync ( ( ) => {
220
+ ReactDOMClient . hydrateRoot (
221
+ container ,
222
+ < div >
223
+ < div > parsnip</ div >
224
+ </ div > ,
225
+ {
226
+ onRecoverableError : error => {
227
+ Scheduler . log ( 'Log recoverable error: ' + error . message ) ;
228
+ } ,
229
+ } ,
230
+ ) ;
231
+ } ) ;
232
+ } ) . toErrorDev (
233
+ [
234
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.' ,
235
+ 'Expected server HTML to contain a matching <div> in <div>.' ,
236
+ ] ,
237
+ { withoutStack : 1 } ,
183
238
) ;
239
+
240
+ assertLog ( [
241
+ 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.' ,
242
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
243
+ ] ) ;
184
244
expect ( container . textContent ) . toBe ( 'parsnip' ) ;
185
245
} ) ;
186
246
187
- it ( 'should give helpful errors on state desync' , ( ) => {
247
+ it ( 'should give helpful errors on state desync' , async ( ) => {
188
248
class Component extends React . Component {
189
249
render ( ) {
190
250
return (
@@ -203,13 +263,45 @@ describe('rendering React components at document', () => {
203
263
) ;
204
264
const testDocument = getTestDocument ( markup ) ;
205
265
206
- expect ( ( ) =>
207
- ReactDOM . hydrate ( < Component text = "Hello world" /> , testDocument ) ,
208
- ) . toErrorDev ( 'Warning: Text content did not match.' ) ;
266
+ const enableClientRenderFallbackOnTextMismatch = gate (
267
+ flags => flags . enableClientRenderFallbackOnTextMismatch ,
268
+ ) ;
269
+ expect ( ( ) => {
270
+ ReactDOM . flushSync ( ( ) => {
271
+ ReactDOMClient . hydrateRoot (
272
+ testDocument ,
273
+ < Component text = "Hello world" /> ,
274
+ {
275
+ onRecoverableError : error => {
276
+ Scheduler . log ( 'Log recoverable error: ' + error . message ) ;
277
+ } ,
278
+ } ,
279
+ ) ;
280
+ } ) ;
281
+ } ) . toErrorDev (
282
+ enableClientRenderFallbackOnTextMismatch
283
+ ? [
284
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.' ,
285
+ 'Warning: Text content did not match.' ,
286
+ ]
287
+ : [ 'Warning: Text content did not match.' ] ,
288
+ {
289
+ withoutStack : enableClientRenderFallbackOnTextMismatch ? 1 : 0 ,
290
+ } ,
291
+ ) ;
292
+
293
+ assertLog (
294
+ enableClientRenderFallbackOnTextMismatch
295
+ ? [
296
+ 'Log recoverable error: Text content does not match server-rendered HTML.' ,
297
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
298
+ ]
299
+ : [ ] ,
300
+ ) ;
209
301
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
210
302
} ) ;
211
303
212
- it ( 'should render w/ no markup to full document' , ( ) => {
304
+ it ( 'should render w/ no markup to full document' , async ( ) => {
213
305
const testDocument = getTestDocument ( ) ;
214
306
215
307
class Component extends React . Component {
@@ -227,23 +319,59 @@ describe('rendering React components at document', () => {
227
319
228
320
if ( gate ( flags => flags . enableFloat ) ) {
229
321
// with float the title no longer is a hydration mismatch so we get an error on the body mismatch
230
- expect ( ( ) =>
231
- ReactDOM . hydrate ( < Component text = "Hello world" /> , testDocument ) ,
232
- ) . toErrorDev (
233
- 'Expected server HTML to contain a matching text node for "Hello world" in <body>' ,
322
+ expect ( ( ) => {
323
+ ReactDOM . flushSync ( ( ) => {
324
+ ReactDOMClient . hydrateRoot (
325
+ testDocument ,
326
+ < Component text = "Hello world" /> ,
327
+ {
328
+ onRecoverableError : error => {
329
+ Scheduler . log ( 'Log recoverable error: ' + error . message ) ;
330
+ } ,
331
+ } ,
332
+ ) ;
333
+ } ) ;
334
+ } ) . toErrorDev (
335
+ [
336
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.' ,
337
+ 'Expected server HTML to contain a matching text node for "Hello world" in <body>' ,
338
+ ] ,
339
+ { withoutStack : 1 } ,
234
340
) ;
341
+ assertLog ( [
342
+ 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.' ,
343
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
344
+ ] ) ;
235
345
} else {
236
346
// getTestDocument() has an extra <meta> that we didn't render.
237
- expect ( ( ) =>
238
- ReactDOM . hydrate ( < Component text = "Hello world" /> , testDocument ) ,
239
- ) . toErrorDev (
240
- 'Did not expect server HTML to contain a <meta> in <head>.' ,
347
+ expect ( ( ) => {
348
+ ReactDOM . flushSync ( ( ) => {
349
+ ReactDOMClient . hydrateRoot (
350
+ testDocument ,
351
+ < Component text = "Hello world" /> ,
352
+ {
353
+ onRecoverableError : error => {
354
+ Scheduler . log ( 'Log recoverable error: ' + error . message ) ;
355
+ } ,
356
+ } ,
357
+ ) ;
358
+ } ) ;
359
+ } ) . toErrorDev (
360
+ [
361
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.' ,
362
+ 'Warning: Text content did not match. Server: "test doc" Client: "Hello World"' ,
363
+ ] ,
364
+ { withoutStack : 1 } ,
241
365
) ;
366
+ assertLog ( [
367
+ 'Log recoverable error: Text content does not match server-rendered HTML.' ,
368
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
369
+ ] ) ;
242
370
}
243
371
expect ( testDocument . body . innerHTML ) . toBe ( 'Hello world' ) ;
244
372
} ) ;
245
373
246
- it ( 'supports findDOMNode on full-page components' , ( ) => {
374
+ it ( 'supports findDOMNode on full-page components in legacy mode ' , ( ) => {
247
375
const tree = (
248
376
< html >
249
377
< head >
0 commit comments