From 1196e3809b0df2fce3cbef73cbe3e6164e074801 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 30 Oct 2023 15:45:58 -0700 Subject: [PATCH 1/3] Assert onHeadersReady behavior --- .../src/__tests__/ReactDOMFizzServer-test.js | 151 ++++++++++++++++++ .../src/__tests__/ReactDOMFizzStatic-test.js | 73 +++++++++ .../ReactDOMFizzStaticBrowser-test.js | 103 ++++++++++++ 3 files changed, 327 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 2caf48662549d..6ed04b64a50e1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3690,6 +3690,157 @@ describe('ReactDOMFizzServer', () => { ); }); + it('provides headers after initial work if onHeaders option used', async () => { + let headers = null; + function onHeaders(x) { + headers = x; + } + + function Preloads() { + ReactDOM.preload('font2', {as: 'font'}); + ReactDOM.preload('imagepre2', {as: 'image', fetchPriority: 'high'}); + ReactDOM.preconnect('pre2', {crossOrigin: 'use-credentials'}); + ReactDOM.prefetchDNS('dns2'); + } + + function Blocked() { + readText('blocked'); + return ( + <> + + + + ); + } + + function App() { + ReactDOM.preload('font', {as: 'font'}); + ReactDOM.preload('imagepre', {as: 'image', fetchPriority: 'high'}); + ReactDOM.preconnect('pre', {crossOrigin: 'use-credentials'}); + ReactDOM.prefetchDNS('dns'); + return ( + + + + + + + ); + } + + await act(() => { + renderToPipeableStream(, {onHeaders}); + }); + + expect(headers).toEqual({ + Link: ` +
; rel=preconnect; crossorigin="use-credentials",
+ ; rel=dns-prefetch,
+ ; rel=preload; as="font"; crossorigin="",
+ ; rel=preload; as="image"; fetchpriority="high",
+ ; rel=preload; as="image"
+`
+        .replaceAll('\n', '')
+        .trim(),
+    });
+  });
+
+  it('encodes img srcset and sizes into preload header params', async () => {
+    let headers = null;
+    function onHeaders(x) {
+      headers = x;
+    }
+
+    function App() {
+      ReactDOM.preload('presrc', {
+        as: 'image',
+        fetchPriority: 'high',
+        imageSrcSet: 'presrcset',
+        imageSizes: 'presizes',
+      });
+      return (
+        
+          
+            
+          
+        
+      );
+    }
+
+    await act(() => {
+      renderToPipeableStream(, {onHeaders});
+    });
+
+    expect(headers).toEqual({
+      Link: `
+; rel=preload; as="image"; fetchpriority="high"; imagesrcset="presrcset"; imagesizes="presizes",
+ ; rel=preload; as="image"; imagesrcset="srcset"; imagesizes="sizes"
+`
+        .replaceAll('\n', '')
+        .trim(),
+    });
+  });
+
+  it('emits nothing for headers if you pipe before work begins', async () => {
+    let headers = null;
+    function onHeaders(x) {
+      headers = x;
+    }
+
+    function App() {
+      ReactDOM.preload('presrc', {
+        as: 'image',
+        fetchPriority: 'high',
+        imageSrcSet: 'presrcset',
+        imageSizes: 'presizes',
+      });
+      return (
+        
+          
+            
+          
+        
+      );
+    }
+
+    await act(() => {
+      renderToPipeableStream(, {onHeaders}).pipe(writable);
+    });
+
+    expect(headers).toEqual({});
+  });
+
+  it('stops accumulating new headers once the maxHeadersLength limit is satisifed', async () => {
+    let headers = null;
+    function onHeaders(x) {
+      headers = x;
+    }
+
+    function App() {
+      ReactDOM.preconnect('foo');
+      ReactDOM.preconnect('bar');
+      ReactDOM.preconnect('baz');
+      return (
+        
+          hello
+        
+      );
+    }
+
+    await act(() => {
+      renderToPipeableStream(, {onHeaders, maxHeadersLength: 44});
+    });
+
+    expect(headers).toEqual({
+      Link: `
+; rel=preconnect,
+ ; rel=preconnect
+`
+        .replaceAll('\n', '')
+        .trim(),
+    });
+  });
+
   describe('error escaping', () => {
     it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => {
       window.__outlet = {};
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
index f71d18e3c94ef..49afa26f65e02 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
@@ -13,6 +13,7 @@
 let JSDOM;
 let Stream;
 let React;
+let ReactDOM;
 let ReactDOMClient;
 let ReactDOMFizzStatic;
 let Suspense;
@@ -29,6 +30,7 @@ describe('ReactDOMFizzStatic', () => {
     jest.resetModules();
     JSDOM = require('jsdom').JSDOM;
     React = require('react');
+    ReactDOM = require('react-dom');
     ReactDOMClient = require('react-dom/client');
     if (__EXPERIMENTAL__) {
       ReactDOMFizzStatic = require('react-dom/static');
@@ -262,4 +264,75 @@ describe('ReactDOMFizzStatic', () => {
       'hello world',
     ]);
   });
+
+  // @gate experimental
+  it('supports onHeaders', async () => {
+    let headers;
+    function onHeaders(x) {
+      headers = x;
+    }
+
+    function App() {
+      ReactDOM.preload('image', {as: 'image', fetchPriority: 'high'});
+      ReactDOM.preload('font', {as: 'font'});
+      return (
+        
+          hello
+        
+      );
+    }
+
+    const result = await ReactDOMFizzStatic.prerenderToNodeStream(, {
+      onHeaders,
+    });
+    expect(headers).toEqual({
+      Link: `
+; rel=preload; as="font"; crossorigin="",
+ ; rel=preload; as="image"; fetchpriority="high"
+`
+        .replaceAll('\n', '')
+        .trim(),
+    });
+
+    await act(async () => {
+      result.prelude.pipe(writable);
+    });
+    expect(getVisibleChildren(container)).toEqual('hello');
+  });
+
+  // @gate experimental && enablePostpone
+  it('includes stylesheet preloads in onHeaders when postponing in the Shell', async () => {
+    let headers;
+    function onHeaders(x) {
+      headers = x;
+    }
+
+    function App() {
+      ReactDOM.preload('image', {as: 'image', fetchPriority: 'high'});
+      ReactDOM.preinit('style', {as: 'style'});
+      React.unstable_postpone();
+      return (
+        
+          hello
+        
+      );
+    }
+
+    const result = await ReactDOMFizzStatic.prerenderToNodeStream(, {
+      onHeaders,
+    });
+    expect(headers).toEqual({
+      Link: `
+; rel=preload; as="image"; fetchpriority="high",
+