| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @emails react-core |
| 8 | */ |
| 9 | |
| 10 | 'use strict'; |
| 11 | |
| 12 | import { |
| 13 | insertNodesAndExecuteScripts, |
| 14 | getVisibleChildren, |
| 15 | } from '../test-utils/FizzTestUtils'; |
| 16 | import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel'; |
| 17 | |
| 18 | // Polyfills for test environment |
| 19 | global.ReadableStream = |
| 20 | require('web-streams-polyfill/ponyfill/es6').ReadableStream; |
| 21 | global.TextEncoder = require('util').TextEncoder; |
| 22 | |
| 23 | let act; |
| 24 | let serverAct; |
| 25 | let assertLog; |
| 26 | let waitForPaint; |
| 27 | let container; |
| 28 | let React; |
| 29 | let Scheduler; |
| 30 | let ReactDOMServer; |
| 31 | let ReactDOMClient; |
| 32 | let useDeferredValue; |
| 33 | let Suspense; |
| 34 | |
| 35 | describe('ReactDOMFizzForm', () => { |
| 36 | beforeEach(() => { |
| 37 | jest.resetModules(); |
| 38 | Scheduler = require('scheduler'); |
| 39 | patchMessageChannel(); |
| 40 | act = require('internal-test-utils').act; |
| 41 | serverAct = require('internal-test-utils').serverAct; |
| 42 | React = require('react'); |
| 43 | ReactDOMServer = require('react-dom/server.browser'); |
| 44 | ReactDOMClient = require('react-dom/client'); |
| 45 | useDeferredValue = React.useDeferredValue; |
| 46 | Suspense = React.Suspense; |
| 47 | assertLog = require('internal-test-utils').assertLog; |
| 48 | waitForPaint = require('internal-test-utils').waitForPaint; |
| 49 | container = document.createElement('div'); |
| 50 | document.body.appendChild(container); |
| 51 | }); |
| 52 | |
| 53 | afterEach(() => { |
| 54 | document.body.removeChild(container); |
| 55 | }); |
| 56 | |
| 57 | async function readIntoContainer(stream) { |
| 58 | const reader = stream.getReader(); |
| 59 | let result = ''; |
| 60 | while (true) { |
| 61 | const {done, value} = await reader.read(); |
| 62 | if (done) { |
| 63 | break; |
| 64 | } |
| 65 | result += Buffer.from(value).toString('utf8'); |
| 66 | } |
| 67 | const temp = document.createElement('div'); |
| 68 | temp.innerHTML = result; |
| 69 | insertNodesAndExecuteScripts(temp, container, null); |
| 70 | } |
| 71 | |
| 72 | function Text({text}) { |
| 73 | Scheduler.log(text); |
| 74 | return text; |
| 75 | } |
| 76 | |
| 77 | it('returns initialValue argument, if provided', async () => { |
| 78 | function App() { |
| 79 | return useDeferredValue('Final', 'Initial'); |
| 80 | } |
| 81 | |
| 82 | const stream = await serverAct(() => |
| 83 | ReactDOMServer.renderToReadableStream(<App />), |
| 84 | ); |
| 85 | await readIntoContainer(stream); |
| 86 | expect(container.textContent).toEqual('Initial'); |
| 87 | |
| 88 | // After hydration, it's updated to the final value |
| 89 | await act(() => ReactDOMClient.hydrateRoot(container, <App />)); |
| 90 | expect(container.textContent).toEqual('Final'); |
| 91 | }); |
| 92 | |
| 93 | it( |
| 94 | 'useDeferredValue during hydration has higher priority than remaining ' + |
| 95 | 'incremental hydration', |
| 96 | async () => { |
| 97 | function B() { |
| 98 | const text = useDeferredValue('B [Final]', 'B [Initial]'); |
| 99 | return <Text text={text} />; |
| 100 | } |
| 101 | |
| 102 | function App() { |
| 103 | return ( |
| 104 | <div> |
| 105 | <span> |
| 106 | <Text text="A" /> |
| 107 | </span> |
| 108 | <Suspense fallback={<Text text="Loading..." />}> |
| 109 | <span> |
| 110 | <B /> |
| 111 | </span> |
| 112 | <div> |
| 113 | <Suspense fallback={<Text text="Loading..." />}> |
| 114 | <span id="C" ref={cRef}> |
| 115 | <Text text="C" /> |
| 116 | </span> |
| 117 | </Suspense> |
| 118 | </div> |
| 119 | </Suspense> |
| 120 | </div> |
| 121 | ); |
| 122 | } |
| 123 | |
| 124 | const cRef = React.createRef(); |
| 125 | |
| 126 | const stream = await serverAct(() => |
| 127 | ReactDOMServer.renderToReadableStream(<App />), |
| 128 | ); |
| 129 | await readIntoContainer(stream); |
| 130 | assertLog(['A', 'B [Initial]', 'C']); |
| 131 | expect(getVisibleChildren(container)).toEqual( |
| 132 | <div> |
| 133 | <span>A</span> |
| 134 | <span>B [Initial]</span> |
| 135 | <div> |
| 136 | <span id="C">C</span> |
| 137 | </div> |
| 138 | </div>, |
| 139 | ); |
| 140 | |
| 141 | const serverRenderedC = document.getElementById('C'); |
| 142 | |
| 143 | // On the client, we first hydrate the initial value, then upgrade |
| 144 | // to final. |
| 145 | await act(async () => { |
| 146 | ReactDOMClient.hydrateRoot(container, <App />); |
| 147 | |
| 148 | // First the outermost Suspense boundary hydrates. |
| 149 | await waitForPaint(['A']); |
| 150 | expect(cRef.current).toBe(null); |
| 151 | |
| 152 | // Then the next level hydrates. This level includes a useDeferredValue, |
| 153 | // so we should prioritize upgrading it before we proceed to hydrating |
| 154 | // additional levels. |
| 155 | await waitForPaint(['B [Initial]']); |
| 156 | expect(getVisibleChildren(container)).toEqual( |
| 157 | <div> |
| 158 | <span>A</span> |
| 159 | <span>B [Initial]</span> |
| 160 | <div> |
| 161 | <span id="C">C</span> |
| 162 | </div> |
| 163 | </div>, |
| 164 | ); |
| 165 | expect(cRef.current).toBe(null); |
| 166 | |
| 167 | // This paint should only update B. C should still be dehydrated. |
| 168 | await waitForPaint(['B [Final]']); |
| 169 | expect(getVisibleChildren(container)).toEqual( |
| 170 | <div> |
| 171 | <span>A</span> |
| 172 | <span>B [Final]</span> |
| 173 | <div> |
| 174 | <span id="C">C</span> |
| 175 | </div> |
| 176 | </div>, |
| 177 | ); |
| 178 | expect(cRef.current).toBe(null); |
| 179 | }); |
| 180 | // Finally we can hydrate C |
| 181 | assertLog(['C']); |
| 182 | expect(getVisibleChildren(container)).toEqual( |
| 183 | <div> |
| 184 | <span>A</span> |
| 185 | <span>B [Final]</span> |
| 186 | <div> |
| 187 | <span id="C">C</span> |
| 188 | </div> |
| 189 | </div>, |
| 190 | ); |
| 191 | expect(cRef.current).toBe(serverRenderedC); |
| 192 | }, |
| 193 | ); |
| 194 | }); |