@@ -4220,6 +4220,7 @@ describe('ReactDOMFizzServer', () => {
42204220 ) ;
42214221 } ) ;
42224222
4223+ // @gate enableFloat
42234224 it ( 'emits html and head start tags (the preamble) before other content if rendered in the shell' , async ( ) => {
42244225 await actIntoEmptyDocument ( ( ) => {
42254226 const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
@@ -4245,7 +4246,7 @@ describe('ReactDOMFizzServer', () => {
42454246 // Hydrate the same thing on the client. We expect this to still fail because <title> is not a Resource
42464247 // and is unmatched on hydration
42474248 const errors = [ ] ;
4248- const root = ReactDOMClient . hydrateRoot (
4249+ ReactDOMClient . hydrateRoot (
42494250 document ,
42504251 < >
42514252 < title data-baz = "baz" > a title</ title >
@@ -4280,8 +4281,13 @@ describe('ReactDOMFizzServer', () => {
42804281 'Hydration failed because the initial UI does not match what was rendered on the server.' ,
42814282 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' ,
42824283 ] ) ;
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.' ) ;
42834288 } ) ;
42844289
4290+ // @gate enableFloat
42854291 it ( 'holds back body and html closing tags (the postamble) until all pending tasks are completed' , async ( ) => {
42864292 const chunks = [ ] ;
42874293 writable . on ( 'data' , chunk => {
@@ -4327,6 +4333,119 @@ describe('ReactDOMFizzServer', () => {
43274333 expect ( chunks . pop ( ) ) . toEqual ( '</body></html>' ) ;
43284334 } ) ;
43294335
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+
43304449 describe ( 'text separators' , ( ) => {
43314450 // To force performWork to start before resolving AsyncText but before piping we need to wait until
43324451 // after scheduleWork which currently uses setImmediate to delay performWork
0 commit comments