225
);
226
});
227
228
+ it('resets property-backed state when a singleton is released', async () => {
229
+ const root = ReactDOMClient.createRoot(document);
230
+ const head = document.head;
231
+ const body = document.body;
232
+
233
+ root.render(
234
+ <html>
235
+ <head onClick={() => {}} />
236
+ <body
237
+ data-react-owned="true"
238
+ onClick={() => {}}
239
+ style={{color: 'red'}}
240
+ dangerouslySetInnerHTML={{__html: '<div>managed content</div>'}}
241
+ />
242
+ </html>,
243
+ );
244
+ await waitForAll([]);
245
+
246
+ expect(document.body).toBe(body);
247
+ expect(head.onclick).not.toBe(null);
248
+ expect(body.onclick).not.toBe(null);
249
+ expect(body.textContent).toBe('managed content');
250
+ expect(body.getAttribute('data-react-owned')).toBe('true');
251
+ expect(body.style.color).toBe('red');
252
+
253
+ // Simulate an inline script or third-party code adding its own attribute,
254
+ // style, and click listener while React owns the singleton.
255
+ const externalClickHandler = jest.fn();
256
+ body.setAttribute('data-external', 'true');
257
+ body.style.backgroundColor = 'blue';
258
+ body.onclick = externalClickHandler;
259
+
260
+ root.render(<html />);
261
+ await waitForAll([]);
262
+
263
+ expect(document.head).toBe(head);
264
+ expect(document.body).toBe(body);
265
+ expect(head.onclick).toBe(null);
266
+ expect(body.onclick).toBe(externalClickHandler);
267
+ expect(body.textContent).toBe('');
268
+ expect(body.hasAttribute('data-react-owned')).toBe(false);
269
+ expect(body.getAttribute('data-external')).toBe('true');
270
+ expect(body.style.color).toBe('');
271
+ expect(body.style.backgroundColor).toBe('blue');
272
+ });
273
+
274
+ // @gate TODO
275
+ it('clears dangerouslySetInnerHTML when it becomes undefined', async () => {
276
+ const root = ReactDOMClient.createRoot(document);
277
+ const body = document.body;
278
+ const undefinedHTML = undefined;
279
+
280
+ root.render(
281
+ <html>
282
+ <head />
283
+ <body
284
+ dangerouslySetInnerHTML={{__html: '<div>managed content</div>'}}
285
+ />
286
+ </html>,
287
+ );
288
+ await waitForAll([]);
289
+ expect(body.textContent).toBe('managed content');
290
+
291
+ root.render(
292
+ <html>
293
+ <head />
294
+ <body dangerouslySetInnerHTML={undefinedHTML} />
295
+ </html>,
296
+ );
297
+ await waitForAll([]);
298
+
299
+ expect(body.textContent).toBe('');
300
+ });
301
+
302
+ // @gate TODO
303
+ it('clears dangerouslySetInnerHTML when __html becomes undefined', async () => {
304
+ const root = ReactDOMClient.createRoot(document);
305
+ const body = document.body;
306
+
307
+ root.render(
308
+ <html>
309
+ <head />
310
+ <body
311
+ dangerouslySetInnerHTML={{__html: '<div>managed content</div>'}}
312
+ />
313
+ </html>,
314
+ );
315
+ await waitForAll([]);
316
+ expect(body.textContent).toBe('managed content');
317
+
318
+ root.render(
319
+ <html>
320
+ <head />
321
+ <body dangerouslySetInnerHTML={{__html: undefined}} />
322
+ </html>,
323
+ );
324
+ await waitForAll([]);
325
+
326
+ expect(body.textContent).toBe('');
327
+ });
328
+
329
+ it('updates dangerouslySetInnerHTML on a singleton', async () => {
330
+ const root = ReactDOMClient.createRoot(document);
331
+ const body = document.body;
332
+
333
+ root.render(
334
+ <html>
335
+ <head />
336
+ <body dangerouslySetInnerHTML={{__html: '<div>first</div>'}} />
337
+ </html>,
338
+ );
339
+ await waitForAll([]);
340
+ expect(body.innerHTML).toBe('<div>first</div>');
341
+
342
+ root.render(
343
+ <html>
344
+ <head />
345
+ <body dangerouslySetInnerHTML={{__html: '<span>second</span>'}} />
346
+ </html>,
347
+ );
348
+ await waitForAll([]);
349
+
350
+ expect(body.innerHTML).toBe('<span>second</span>');
351
+ });
352
+
353
+ it('replaces singleton children with dangerouslySetInnerHTML', async () => {
354
+ const root = ReactDOMClient.createRoot(document);
355
+ const body = document.body;
356
+
357
+ root.render(
358
+ <html>
359
+ <head />
360
+ <body>
361
+ <div>managed child</div>
362
+ </body>
363
+ </html>,
364
+ );
365
+ await waitForAll([]);
366
+ expect(body.innerHTML).toBe('<div>managed child</div>');
367
+
368
+ root.render(
369
+ <html>
370
+ <head />
371
+ <body dangerouslySetInnerHTML={{__html: '<span>managed HTML</span>'}} />
372
+ </html>,
373
+ );
374
+ await waitForAll([]);
375
+
376
+ expect(body.innerHTML).toBe('<span>managed HTML</span>');
377
+ });
378
+
379
+ // @gate TODO
380
+ it('replaces dangerouslySetInnerHTML with singleton children', async () => {
381
+ const root = ReactDOMClient.createRoot(document);
382
+ const body = document.body;
383
+
384
+ root.render(
385
+ <html>
386
+ <head />
387
+ <body
388
+ dangerouslySetInnerHTML={{__html: '<div>managed content</div>'}}
389
+ />
390
+ </html>,
391
+ );
392
+ await waitForAll([]);
393
+ expect(body.innerHTML).toBe('<div>managed content</div>');
394
+
395
+ root.render(
396
+ <html>
397
+ <head />
398
+ <body>
399
+ <span>managed child</span>
400
+ </body>
401
+ </html>,
402
+ );
403
+ await waitForAll([]);
404
+
405
+ expect(body.innerHTML).toBe('<span>managed child</span>');
406
+ });
407
+
408
+ // @gate TODO
409
+ it('preserves imperative attributes when acquiring a singleton', async () => {
410
+ const body = document.body;
411
+ body.setAttribute('data-external', 'true');
412
+
413
+ const root = ReactDOMClient.createRoot(document);
414
+ root.render(
415
+ <html>
416
+ <head />
417
+ <body />
418
+ </html>,
419
+ );
420
+ await waitForAll([]);
421
+
422
+ expect(document.body).toBe(body);
423
+ expect(body.getAttribute('data-external')).toBe('true');
424
+ });
425
+
426
+ // @gate TODO
427
+ it('preserves imperative attributes when clearing a preamble contribution', async () => {
428
+ const body = document.body;
429
+ body.setAttribute('data-react-owned', 'true');
430
+ body.setAttribute('data-external', 'true');
431
+ // This is the shape Fizz emits when a completed Suspense boundary
432
+ // contributes props to the body singleton.
433
+ body.innerHTML = '<!--$--><!--body--><div>server</div><!--/$-->';
434
+
435
+ ReactDOMClient.hydrateRoot(
436
+ document,
437
+ <html>
438
+ <head />
439
+ <body data-react-owned="true" suppressHydrationWarning={true}>
440
+ <React.Suspense fallback={null}>
441
+ <span>client</span>
442
+ </React.Suspense>
443
+ </body>
444
+ </html>,
445
+ {
446
+ onRecoverableError() {},
447
+ },
448
+ );
449
+ await waitForAll([]);
450
+
451
+ expect(body.textContent).toBe('client');
452
+ expect(body.getAttribute('data-external')).toBe('true');
453
+ });
454
+
455
+ it('does not duplicate native listeners when a singleton is reacquired', async () => {
456
+ const root = ReactDOMClient.createRoot(document);
457
+ const body = document.body;
458
+ const onScroll = jest.fn();
459
+
460
+ root.render(
461
+ <html>
462
+ <head />
463
+ <body onScroll={onScroll} />
464
+ </html>,
465
+ );
466
+ await waitForAll([]);
467
+
468
+ body.dispatchEvent(new document.defaultView.Event('scroll'));
469
+ expect(onScroll).toHaveBeenCalledTimes(1);
470
+
471
+ root.render(
472
+ <html>
473
+ <head />
474
+ </html>,
475
+ );
476
+ await waitForAll([]);
477
+
478
+ body.dispatchEvent(new document.defaultView.Event('scroll'));
479
+ expect(onScroll).toHaveBeenCalledTimes(1);
480
+
481
+ root.render(
482
+ <html>
483
+ <head />
484
+ <body onScroll={onScroll} />
485
+ </html>,
486
+ );
487
+ await waitForAll([]);
488
+
489
+ expect(document.body).toBe(body);
490
+ body.dispatchEvent(new document.defaultView.Event('scroll'));
491
+ expect(onScroll).toHaveBeenCalledTimes(2);
492
+ });
493
+
494
it('renders into html, head, and body persistently so the node identities never change and extraneous styles are retained', async () => {
495
// Server render some html that will get replaced with a client render
496
await actIntoEmptyDocument(() => {