@@ -4220,6 +4220,7 @@ describe('ReactDOMFizzServer', () => {
4220
4220
) ;
4221
4221
} ) ;
4222
4222
4223
+ // @gate enableFloat
4223
4224
it ( 'emits html and head start tags (the preamble) before other content if rendered in the shell' , async ( ) => {
4224
4225
await actIntoEmptyDocument ( ( ) => {
4225
4226
const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
@@ -4245,7 +4246,7 @@ describe('ReactDOMFizzServer', () => {
4245
4246
// Hydrate the same thing on the client. We expect this to still fail because <title> is not a Resource
4246
4247
// and is unmatched on hydration
4247
4248
const errors = [ ] ;
4248
- const root = ReactDOMClient . hydrateRoot (
4249
+ ReactDOMClient . hydrateRoot (
4249
4250
document ,
4250
4251
< >
4251
4252
< title data-baz = "baz" > a title</ title >
@@ -4280,8 +4281,13 @@ describe('ReactDOMFizzServer', () => {
4280
4281
'Hydration failed because the initial UI does not match what was rendered on the server.' ,
4281
4282
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
4282
4283
] ) ;
4284
+ expect ( getVisibleChildren ( document ) ) . toEqual ( ) ;
4285
+ expect ( ( ) => {
4286
+ expect ( Scheduler ) . toFlushWithoutYielding ( ) ;
4287
+ } ) . toThrow ( 'The node to be removed is not a child of this node.' ) ;
4283
4288
} ) ;
4284
4289
4290
+ // @gate enableFloat
4285
4291
it ( 'holds back body and html closing tags (the postamble) until all pending tasks are completed' , async ( ) => {
4286
4292
const chunks = [ ] ;
4287
4293
writable . on ( 'data' , chunk => {
@@ -4327,6 +4333,119 @@ describe('ReactDOMFizzServer', () => {
4327
4333
expect ( chunks . pop ( ) ) . toEqual ( '</body></html>' ) ;
4328
4334
} ) ;
4329
4335
4336
+ // @gate enableFloat
4337
+ it ( 'recognizes stylesheet links as attributes during hydration' , async ( ) => {
4338
+ await actIntoEmptyDocument ( ( ) => {
4339
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
4340
+ < >
4341
+ < link rel = "stylesheet" href = "foo" precedence = "default" />
4342
+ < html >
4343
+ < head >
4344
+ < link rel = "author" precedence = "this is a nonsense prop" />
4345
+ </ head >
4346
+ < body > a body</ body >
4347
+ </ html >
4348
+ </ > ,
4349
+ ) ;
4350
+ pipe ( writable ) ;
4351
+ } ) ;
4352
+ // precedence for stylesheets is mapped to a valid data attribute that is recognized on the client
4353
+ // as opting this node into resource semantics. the use of precedence on the author link is just a
4354
+ // non standard attribute which React allows but is not given any special treatment.
4355
+ expect ( getVisibleChildren ( document ) ) . toEqual (
4356
+ < html >
4357
+ < head >
4358
+ < link rel = "stylesheet" href = "foo" data-rprec = "default" />
4359
+ < link rel = "author" precedence = "this is a nonsense prop" />
4360
+ </ head >
4361
+ < body > a body</ body >
4362
+ </ html > ,
4363
+ ) ;
4364
+
4365
+ // It hydrates successfully
4366
+ ReactDOMClient . hydrateRoot (
4367
+ document ,
4368
+ < >
4369
+ < link rel = "stylesheet" href = "foo" precedence = "default" />
4370
+ < html >
4371
+ < head >
4372
+ < link rel = "author" precedence = "this is a nonsense prop" />
4373
+ </ head >
4374
+ < body > a body</ body >
4375
+ </ html >
4376
+ </ > ,
4377
+ ) ;
4378
+ expect ( Scheduler ) . toFlushWithoutYielding ( ) ;
4379
+ expect ( getVisibleChildren ( document ) ) . toEqual (
4380
+ < html >
4381
+ < head >
4382
+ < link rel = "stylesheet" href = "foo" data-rprec = "default" />
4383
+ < link rel = "author" precedence = "this is a nonsense prop" />
4384
+ </ head >
4385
+ < body > a body</ body >
4386
+ </ html > ,
4387
+ ) ;
4388
+ } ) ;
4389
+
4390
+ // @gate __DEV__ && enableFloat
4391
+ it ( 'should error in dev when rendering more than one resource for a given location (href)' , async ( ) => {
4392
+ await actIntoEmptyDocument ( ( ) => {
4393
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
4394
+ < >
4395
+ < link rel = "stylesheet" href = "foo" precedence = "low" />
4396
+ < link rel = "stylesheet" href = "foo" precedence = "high" />
4397
+ < html >
4398
+ < head />
4399
+ < body > a body</ body >
4400
+ </ html >
4401
+ </ > ,
4402
+ ) ;
4403
+ pipe ( writable ) ;
4404
+ } ) ;
4405
+ expect ( getVisibleChildren ( document ) ) . toEqual (
4406
+ < html >
4407
+ < head >
4408
+ < link rel = "stylesheet" href = "foo" data-rprec = "low" />
4409
+ < link rel = "stylesheet" href = "foo" data-rprec = "high" />
4410
+ </ head >
4411
+ < body > a body</ body >
4412
+ </ html > ,
4413
+ ) ;
4414
+
4415
+ const errors = [ ] ;
4416
+ ReactDOMClient . hydrateRoot (
4417
+ document ,
4418
+ < >
4419
+ < html >
4420
+ < head >
4421
+ < link rel = "stylesheet" href = "foo" precedence = "low" />
4422
+ < link rel = "stylesheet" href = "foo" precedence = "high" />
4423
+ </ head >
4424
+ < body > a body</ body >
4425
+ </ html >
4426
+ </ > ,
4427
+ {
4428
+ onRecoverableError ( err , errInfo ) {
4429
+ errors . push ( err . message ) ;
4430
+ } ,
4431
+ } ,
4432
+ ) ;
4433
+ expect ( ( ) => {
4434
+ expect ( Scheduler ) . toFlushWithoutYielding ( ) ;
4435
+ } ) . toErrorDev (
4436
+ [
4437
+ 'An error occurred during hydration. The server HTML was replaced with client content in <#document>.' ,
4438
+ ] ,
4439
+ { withoutStack : true } ,
4440
+ ) ;
4441
+ expect ( errors ) . toEqual ( [
4442
+ 'Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo".' ,
4443
+ 'Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo".' ,
4444
+ 'Hydration failed because the initial UI does not match what was rendered on the server.' ,
4445
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
4446
+ ] ) ;
4447
+ } ) ;
4448
+
4330
4449
describe ( 'text separators' , ( ) => {
4331
4450
// To force performWork to start before resolving AsyncText but before piping we need to wait until
4332
4451
// after scheduleWork which currently uses setImmediate to delay performWork
0 commit comments