diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js index 0497a696716cb..a66a86866776f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js @@ -905,1072 +905,1084 @@ describe('ReactDOMServerHooks', () => { expect(container.children[0].textContent).toEqual('0'); }); - if (__EXPERIMENTAL__) { - describe('useOpaqueIdentifier', () => { - it('generates unique ids for server string render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } - - const domNode = await serverRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), + describe('useOpaqueIdentifier', () => { + // @gate experimental + it('generates unique ids for server string render', async () => { + function App(props) { + const idOne = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + return ( +
+
+
+ + +
); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); + } - it('generates unique ids for server stream render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } + const domNode = await serverRender(); + expect(domNode.children.length).toEqual(4); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + domNode.children[1].getAttribute('id'), + ); + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( + domNode.children[3].getAttribute('id'), + ); + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( + domNode.children[2].getAttribute('aria-labelledby'), + ); + expect( + domNode.children[0].getAttribute('aria-labelledby'), + ).not.toBeNull(); + expect( + domNode.children[2].getAttribute('aria-labelledby'), + ).not.toBeNull(); + }); - const domNode = await streamRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), + // @gate experimental + it('generates unique ids for server stream render', async () => { + function App(props) { + const idOne = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + return ( +
+
+
+ + +
); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); + } - it('generates unique ids for client render', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } + const domNode = await streamRender(); + expect(domNode.children.length).toEqual(4); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + domNode.children[1].getAttribute('id'), + ); + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( + domNode.children[3].getAttribute('id'), + ); + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( + domNode.children[2].getAttribute('aria-labelledby'), + ); + expect( + domNode.children[0].getAttribute('aria-labelledby'), + ).not.toBeNull(); + expect( + domNode.children[2].getAttribute('aria-labelledby'), + ).not.toBeNull(); + }); - const domNode = await clientCleanRender(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), + // @gate experimental + it('generates unique ids for client render', async () => { + function App(props) { + const idOne = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + return ( +
+
+
+ + +
); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); + } - it('generates unique ids for client render on good server markup', async () => { - function App(props) { - const idOne = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); - return ( -
-
-
- - -
- ); - } + const domNode = await clientCleanRender(); + expect(domNode.children.length).toEqual(4); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + domNode.children[1].getAttribute('id'), + ); + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( + domNode.children[3].getAttribute('id'), + ); + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( + domNode.children[2].getAttribute('aria-labelledby'), + ); + expect( + domNode.children[0].getAttribute('aria-labelledby'), + ).not.toBeNull(); + expect( + domNode.children[2].getAttribute('aria-labelledby'), + ).not.toBeNull(); + }); - const domNode = await clientRenderOnServerString(); - expect(domNode.children.length).toEqual(4); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( - domNode.children[3].getAttribute('id'), + // @gate experimental + it('generates unique ids for client render on good server markup', async () => { + function App(props) { + const idOne = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + return ( +
+
+
+ + +
); - expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( - domNode.children[2].getAttribute('aria-labelledby'), + } + + const domNode = await clientRenderOnServerString(); + expect(domNode.children.length).toEqual(4); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + domNode.children[1].getAttribute('id'), + ); + expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual( + domNode.children[3].getAttribute('id'), + ); + expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual( + domNode.children[2].getAttribute('aria-labelledby'), + ); + expect( + domNode.children[0].getAttribute('aria-labelledby'), + ).not.toBeNull(); + expect( + domNode.children[2].getAttribute('aria-labelledby'), + ).not.toBeNull(); + }); + + // @gate experimental + it('useOpaqueIdentifier does not change id even if the component updates during client render', async () => { + let _setShowId; + function App() { + const id = useOpaqueIdentifier(); + const [showId, setShowId] = useState(false); + _setShowId = setShowId; + return ( +
+
+ {showId &&
} +
); - expect( - domNode.children[0].getAttribute('aria-labelledby'), - ).not.toBeNull(); - expect( - domNode.children[2].getAttribute('aria-labelledby'), - ).not.toBeNull(); - }); + } - it('useOpaqueIdentifier does not change id even if the component updates during client render', async () => { - let _setShowId; - function App() { - const id = useOpaqueIdentifier(); - const [showId, setShowId] = useState(false); - _setShowId = setShowId; - return ( -
-
- {showId &&
} -
- ); - } + const domNode = await clientCleanRender(); + const oldClientId = domNode.children[0].getAttribute('aria-labelledby'); - const domNode = await clientCleanRender(); - const oldClientId = domNode.children[0].getAttribute('aria-labelledby'); + expect(domNode.children.length).toEqual(1); + expect(oldClientId).not.toBeNull(); - expect(domNode.children.length).toEqual(1); - expect(oldClientId).not.toBeNull(); + await ReactTestUtils.act(async () => _setShowId(true)); - await ReactTestUtils.act(async () => _setShowId(true)); + expect(domNode.children.length).toEqual(2); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + domNode.children[1].getAttribute('id'), + ); + expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( + oldClientId, + ); + }); - expect(domNode.children.length).toEqual(2); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - domNode.children[1].getAttribute('id'), - ); - expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual( - oldClientId, + // @gate experimental + it('useOpaqueIdentifier identifierPrefix works for server renderer and does not clash', async () => { + function ChildTwo({id}) { + return
Child Three
; + } + function App() { + const id = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + + return ( +
+
Child One
+ +
Child Three
+
Child Four
+
); + } + + const containerOne = document.createElement('div'); + document.body.append(containerOne); + + containerOne.innerHTML = ReactDOMServer.renderToString(, { + identifierPrefix: 'one', }); - it('useOpaqueIdentifier identifierPrefix works for server renderer and does not clash', async () => { - function ChildTwo({id}) { - return
Child Three
; - } - function App() { - const id = useOpaqueIdentifier(); - const idTwo = useOpaqueIdentifier(); + const containerTwo = document.createElement('div'); + document.body.append(containerTwo); - return ( -
-
Child One
- -
Child Three
-
Child Four
-
- ); - } + containerTwo.innerHTML = ReactDOMServer.renderToString(, { + identifierPrefix: 'two', + }); - const containerOne = document.createElement('div'); - document.body.append(containerOne); + expect(document.body.children.length).toEqual(2); + const childOne = document.body.children[0]; + const childTwo = document.body.children[1]; + + expect( + childOne.children[0].children[0].getAttribute('aria-labelledby'), + ).toEqual(childOne.children[0].children[1].getAttribute('id')); + expect( + childOne.children[0].children[2].getAttribute('aria-labelledby'), + ).toEqual(childOne.children[0].children[3].getAttribute('id')); + + expect( + childOne.children[0].children[0].getAttribute('aria-labelledby'), + ).not.toEqual( + childOne.children[0].children[2].getAttribute('aria-labelledby'), + ); - containerOne.innerHTML = ReactDOMServer.renderToString(, { - identifierPrefix: 'one', - }); + expect( + childOne.children[0].children[0] + .getAttribute('aria-labelledby') + .startsWith('one'), + ).toBe(true); + expect( + childOne.children[0].children[2] + .getAttribute('aria-labelledby') + .includes('one'), + ).toBe(true); + + expect( + childTwo.children[0].children[0].getAttribute('aria-labelledby'), + ).toEqual(childTwo.children[0].children[1].getAttribute('id')); + expect( + childTwo.children[0].children[2].getAttribute('aria-labelledby'), + ).toEqual(childTwo.children[0].children[3].getAttribute('id')); + + expect( + childTwo.children[0].children[0].getAttribute('aria-labelledby'), + ).not.toEqual( + childTwo.children[0].children[2].getAttribute('aria-labelledby'), + ); - const containerTwo = document.createElement('div'); - document.body.append(containerTwo); + expect( + childTwo.children[0].children[0] + .getAttribute('aria-labelledby') + .startsWith('two'), + ).toBe(true); + expect( + childTwo.children[0].children[2] + .getAttribute('aria-labelledby') + .startsWith('two'), + ).toBe(true); + }); - containerTwo.innerHTML = ReactDOMServer.renderToString(, { - identifierPrefix: 'two', - }); + // @gate experimental + it('useOpaqueIdentifier identifierPrefix works for multiple reads on a streaming server renderer', async () => { + function ChildTwo() { + const id = useOpaqueIdentifier(); - expect(document.body.children.length).toEqual(2); - const childOne = document.body.children[0]; - const childTwo = document.body.children[1]; - - expect( - childOne.children[0].children[0].getAttribute('aria-labelledby'), - ).toEqual(childOne.children[0].children[1].getAttribute('id')); - expect( - childOne.children[0].children[2].getAttribute('aria-labelledby'), - ).toEqual(childOne.children[0].children[3].getAttribute('id')); - - expect( - childOne.children[0].children[0].getAttribute('aria-labelledby'), - ).not.toEqual( - childOne.children[0].children[2].getAttribute('aria-labelledby'), - ); + return
Child Two
; + } - expect( - childOne.children[0].children[0] - .getAttribute('aria-labelledby') - .startsWith('one'), - ).toBe(true); - expect( - childOne.children[0].children[2] - .getAttribute('aria-labelledby') - .includes('one'), - ).toBe(true); - - expect( - childTwo.children[0].children[0].getAttribute('aria-labelledby'), - ).toEqual(childTwo.children[0].children[1].getAttribute('id')); - expect( - childTwo.children[0].children[2].getAttribute('aria-labelledby'), - ).toEqual(childTwo.children[0].children[3].getAttribute('id')); - - expect( - childTwo.children[0].children[0].getAttribute('aria-labelledby'), - ).not.toEqual( - childTwo.children[0].children[2].getAttribute('aria-labelledby'), + function App() { + const id = useOpaqueIdentifier(); + + return ( + <> +
Child One
+ +
Aria One
+ ); + } - expect( - childTwo.children[0].children[0] - .getAttribute('aria-labelledby') - .startsWith('two'), - ).toBe(true); - expect( - childTwo.children[0].children[2] - .getAttribute('aria-labelledby') - .startsWith('two'), - ).toBe(true); - }); + const container = document.createElement('div'); + document.body.append(container); - it('useOpaqueIdentifier identifierPrefix works for multiple reads on a streaming server renderer', async () => { - function ChildTwo() { - const id = useOpaqueIdentifier(); + const streamOne = ReactDOMServer.renderToNodeStream(, { + identifierPrefix: 'one', + }).setEncoding('utf8'); + const streamTwo = ReactDOMServer.renderToNodeStream(, { + identifierPrefix: 'two', + }).setEncoding('utf8'); - return
Child Two
; - } + const streamOneIsDone = new Promise((resolve, reject) => { + streamOne.on('end', () => resolve()); + streamOne.on('error', e => reject(e)); + }); + const streamTwoIsDone = new Promise((resolve, reject) => { + streamTwo.on('end', () => resolve()); + streamTwo.on('error', e => reject(e)); + }); - function App() { - const id = useOpaqueIdentifier(); + const containerOne = document.createElement('div'); + const containerTwo = document.createElement('div'); - return ( - <> -
Child One
- -
Aria One
- - ); - } + streamOne._read(10); + streamTwo._read(10); - const container = document.createElement('div'); - document.body.append(container); + containerOne.innerHTML = streamOne.read(); + containerTwo.innerHTML = streamTwo.read(); - const streamOne = ReactDOMServer.renderToNodeStream(, { - identifierPrefix: 'one', - }).setEncoding('utf8'); - const streamTwo = ReactDOMServer.renderToNodeStream(, { - identifierPrefix: 'two', - }).setEncoding('utf8'); + expect(containerOne.children[0].getAttribute('id')).not.toEqual( + containerOne.children[1].getAttribute('id'), + ); + expect(containerTwo.children[0].getAttribute('id')).not.toEqual( + containerTwo.children[1].getAttribute('id'), + ); + expect(containerOne.children[0].getAttribute('id')).not.toEqual( + containerTwo.children[0].getAttribute('id'), + ); + expect(containerOne.children[0].getAttribute('id').includes('one')).toBe( + true, + ); + expect(containerOne.children[1].getAttribute('id').includes('one')).toBe( + true, + ); + expect(containerTwo.children[0].getAttribute('id').includes('two')).toBe( + true, + ); + expect(containerTwo.children[1].getAttribute('id').includes('two')).toBe( + true, + ); + + expect(containerOne.children[1].getAttribute('id')).not.toEqual( + containerTwo.children[1].getAttribute('id'), + ); + expect(containerOne.children[0].getAttribute('id')).toEqual( + containerOne.children[2].getAttribute('aria-labelledby'), + ); + expect(containerTwo.children[0].getAttribute('id')).toEqual( + containerTwo.children[2].getAttribute('aria-labelledby'), + ); - const containerOne = document.createElement('div'); - const containerTwo = document.createElement('div'); + // Exhaust the rest of the stream + class Sink extends require('stream').Writable { + _write(chunk, encoding, done) { + done(); + } + } + streamOne.pipe(new Sink()); + streamTwo.pipe(new Sink()); - streamOne._read(10); - streamTwo._read(10); + await Promise.all([streamOneIsDone, streamTwoIsDone]); + }); - containerOne.innerHTML = streamOne.read(); - containerTwo.innerHTML = streamTwo.read(); + // @gate experimental + it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => { + let _setShowDiv; + function App() { + const id = useOpaqueIdentifier(); + const [showDiv, setShowDiv] = useState(false); + _setShowDiv = setShowDiv; - expect(containerOne.children[0].getAttribute('id')).not.toEqual( - containerOne.children[1].getAttribute('id'), - ); - expect(containerTwo.children[0].getAttribute('id')).not.toEqual( - containerTwo.children[1].getAttribute('id'), - ); - expect(containerOne.children[0].getAttribute('id')).not.toEqual( - containerTwo.children[0].getAttribute('id'), - ); - expect( - containerOne.children[0].getAttribute('id').includes('one'), - ).toBe(true); - expect( - containerOne.children[1].getAttribute('id').includes('one'), - ).toBe(true); - expect( - containerTwo.children[0].getAttribute('id').includes('two'), - ).toBe(true); - expect( - containerTwo.children[1].getAttribute('id').includes('two'), - ).toBe(true); - - expect(containerOne.children[1].getAttribute('id')).not.toEqual( - containerTwo.children[1].getAttribute('id'), - ); - expect(containerOne.children[0].getAttribute('id')).toEqual( - containerOne.children[2].getAttribute('aria-labelledby'), - ); - expect(containerTwo.children[0].getAttribute('id')).toEqual( - containerTwo.children[2].getAttribute('aria-labelledby'), + return ( +
+
Child One
+ {showDiv &&
Child Two
} +
); - }); + } - it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => { - let _setShowDiv; - function App() { - const id = useOpaqueIdentifier(); - const [showDiv, setShowDiv] = useState(false); - _setShowDiv = setShowDiv; + const container = document.createElement('div'); + document.body.append(container); - return ( -
-
Child One
- {showDiv &&
Child Two
} -
- ); - } + container.innerHTML = ReactDOMServer.renderToString(); + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); - const container = document.createElement('div'); - document.body.append(container); + expect(container.children[0].children.length).toEqual(1); + const oldServerId = container.children[0].children[0].getAttribute('id'); + expect(oldServerId).not.toBeNull(); - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - root.render(); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + await ReactTestUtils.act(async () => { + _setShowDiv(true); + }); + expect(container.children[0].children.length).toEqual(2); + expect(container.children[0].children[0].getAttribute('id')).toEqual( + container.children[0].children[1].getAttribute('id'), + ); + expect(container.children[0].children[0].getAttribute('id')).not.toEqual( + oldServerId, + ); + expect( + container.children[0].children[0].getAttribute('id'), + ).not.toBeNull(); + }); - expect(container.children[0].children.length).toEqual(1); - const oldServerId = container.children[0].children[0].getAttribute( - 'id', - ); - expect(oldServerId).not.toBeNull(); + // @gate experimental + it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered for legacy', async () => { + let _setShowDiv; + function App() { + const id = useOpaqueIdentifier(); + const [showDiv, setShowDiv] = useState(false); + _setShowDiv = setShowDiv; - await ReactTestUtils.act(async () => { - _setShowDiv(true); - }); - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), + return ( +
+
Child One
+ {showDiv &&
Child Two
} +
); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toEqual(oldServerId); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); + } - it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered for legacy', async () => { - let _setShowDiv; - function App() { - const id = useOpaqueIdentifier(); - const [showDiv, setShowDiv] = useState(false); - _setShowDiv = setShowDiv; + const container = document.createElement('div'); + document.body.append(container); - return ( -
-
Child One
- {showDiv &&
Child Two
} -
- ); - } + container.innerHTML = ReactDOMServer.renderToString(); + ReactDOM.hydrate(, container); - const container = document.createElement('div'); - document.body.append(container); + expect(container.children[0].children.length).toEqual(1); + const oldServerId = container.children[0].children[0].getAttribute('id'); + expect(oldServerId).not.toBeNull(); - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.hydrate(, container); + await ReactTestUtils.act(async () => { + _setShowDiv(true); + }); + expect(container.children[0].children.length).toEqual(2); + expect(container.children[0].children[0].getAttribute('id')).toEqual( + container.children[0].children[1].getAttribute('id'), + ); + expect(container.children[0].children[0].getAttribute('id')).not.toEqual( + oldServerId, + ); + expect( + container.children[0].children[0].getAttribute('id'), + ).not.toBeNull(); + }); - expect(container.children[0].children.length).toEqual(1); - const oldServerId = container.children[0].children[0].getAttribute( - 'id', + // @gate experimental + it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => { + let _setShow; + function App({unused}) { + Scheduler.unstable_yieldValue('App'); + const id = useOpaqueIdentifier(); + const [show, setShow] = useState(false); + _setShow = setShow; + return ( +
+ {'Child One'} +
); - expect(oldServerId).not.toBeNull(); + } - await ReactTestUtils.act(async () => { - _setShowDiv(true); - }); - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toEqual(oldServerId); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); + const container = document.createElement('div'); + document.body.append(container); + container.innerHTML = ReactDOMServer.renderToString(); + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + ReactTestUtils.act(() => { + root.render(); }); - - it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => { - let _setShow; - function App({unused}) { - Scheduler.unstable_yieldValue('App'); - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } - - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - ReactTestUtils.act(() => { - root.render(); - }); - expect(Scheduler).toHaveYielded(['App', 'App']); - // The ID goes from not being used to being added to the page - ReactTestUtils.act(() => { - _setShow(true); - }); - expect(Scheduler).toHaveYielded(['App', 'App']); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); + expect(Scheduler).toHaveYielded(['App', 'App']); + // The ID goes from not being used to being added to the page + ReactTestUtils.act(() => { + _setShow(true); }); + expect(Scheduler).toHaveYielded(['App', 'App']); + expect( + container.getElementsByTagName('span')[0].getAttribute('id'), + ).not.toBeNull(); + }); - it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => { - let _setShow; - function App({unused}) { - Scheduler.unstable_yieldValue('App'); - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } + // @gate experimental + it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => { + let _setShow; + function App({unused}) { + Scheduler.unstable_yieldValue('App'); + const id = useOpaqueIdentifier(); + const [show, setShow] = useState(false); + _setShow = setShow; + return ( +
+ {'Child One'} +
+ ); + } - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.hydrate(, container); - expect(Scheduler).toHaveYielded(['App', 'App']); - // The ID goes from not being used to being added to the page - ReactTestUtils.act(() => { - _setShow(true); - }); - expect(Scheduler).toHaveYielded(['App']); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); + const container = document.createElement('div'); + document.body.append(container); + container.innerHTML = ReactDOMServer.renderToString(); + ReactDOM.hydrate(, container); + expect(Scheduler).toHaveYielded(['App', 'App']); + // The ID goes from not being used to being added to the page + ReactTestUtils.act(() => { + _setShow(true); }); + expect(Scheduler).toHaveYielded(['App']); + expect( + container.getElementsByTagName('span')[0].getAttribute('id'), + ).not.toBeNull(); + }); - it('useOpaqueIdentifier: flushSync', async () => { - let _setShow; - function App() { - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- {'Child One'} -
- ); - } + // @gate experimental + it('useOpaqueIdentifier: flushSync', async () => { + let _setShow; + function App() { + const id = useOpaqueIdentifier(); + const [show, setShow] = useState(false); + _setShow = setShow; + return ( +
+ {'Child One'} +
+ ); + } - const container = document.createElement('div'); - document.body.append(container); - container.innerHTML = ReactDOMServer.renderToString(); - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - ReactTestUtils.act(() => { - root.render(); - }); + const container = document.createElement('div'); + document.body.append(container); + container.innerHTML = ReactDOMServer.renderToString(); + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + ReactTestUtils.act(() => { + root.render(); + }); - // The ID goes from not being used to being added to the page - ReactTestUtils.act(() => { - ReactDOM.flushSync(() => { - _setShow(true); - }); + // The ID goes from not being used to being added to the page + ReactTestUtils.act(() => { + ReactDOM.flushSync(() => { + _setShow(true); }); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); }); + expect( + container.getElementsByTagName('span')[0].getAttribute('id'), + ).not.toBeNull(); + }); - it('useOpaqueIdentifier: children with id hydrates before other children if ID updates', async () => { - let _setShow; - - const child1Ref = React.createRef(); - const childWithIDRef = React.createRef(); - const setShowRef = React.createRef(); + // @gate experimental + it('useOpaqueIdentifier: children with id hydrates before other children if ID updates', async () => { + let _setShow; - // RENAME THESE - function Child1() { - Scheduler.unstable_yieldValue('Child One'); - return {'Child One'}; - } + const child1Ref = React.createRef(); + const childWithIDRef = React.createRef(); + const setShowRef = React.createRef(); - function Child2() { - Scheduler.unstable_yieldValue('Child Two'); - return {'Child Two'}; - } + // RENAME THESE + function Child1() { + Scheduler.unstable_yieldValue('Child One'); + return {'Child One'}; + } - const Children = React.memo(function Children() { - return ( - - - - - ); - }); + function Child2() { + Scheduler.unstable_yieldValue('Child Two'); + return {'Child Two'}; + } - function ChildWithID({parentID}) { - Scheduler.unstable_yieldValue('Child with ID'); - return ( - - {'Child with ID'} - - ); - } + const Children = React.memo(function Children() { + return ( + + + + + ); + }); - const ChildrenWithID = React.memo(function ChildrenWithID({parentID}) { - return ( - - - - ); - }); + function ChildWithID({parentID}) { + Scheduler.unstable_yieldValue('Child with ID'); + return ( + + {'Child with ID'} + + ); + } - function App() { - const id = useOpaqueIdentifier(); - const [show, setShow] = useState(false); - _setShow = setShow; - return ( -
- - - {show && ( - - {'Child Three'} - - )} -
- ); - } + const ChildrenWithID = React.memo(function ChildrenWithID({parentID}) { + return ( + + + + ); + }); - const container = document.createElement('div'); - container.innerHTML = ReactDOMServer.renderToString(); - expect(Scheduler).toHaveYielded([ - 'Child One', - 'Child Two', - 'Child with ID', - ]); - expect(container.textContent).toEqual( - 'Child OneChild TwoChild with ID', + function App() { + const id = useOpaqueIdentifier(); + const [show, setShow] = useState(false); + _setShow = setShow; + return ( +
+ + + {show && ( + + {'Child Three'} + + )} +
); + } - const serverId = container - .getElementsByTagName('span')[2] - .getAttribute('id'); - expect(serverId).not.toBeNull(); + const container = document.createElement('div'); + container.innerHTML = ReactDOMServer.renderToString(); + expect(Scheduler).toHaveYielded([ + 'Child One', + 'Child Two', + 'Child with ID', + ]); + expect(container.textContent).toEqual('Child OneChild TwoChild with ID'); - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - root.render(); - expect(Scheduler).toHaveYielded([]); + const serverId = container + .getElementsByTagName('span')[2] + .getAttribute('id'); + expect(serverId).not.toBeNull(); - //Hydrate just child one before updating state - expect(Scheduler).toFlushAndYieldThrough(['Child One']); - expect(child1Ref.current).toBe(null); - expect(Scheduler).toHaveYielded([]); + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + expect(Scheduler).toHaveYielded([]); - ReactTestUtils.act(() => { - _setShow(true); + //Hydrate just child one before updating state + expect(Scheduler).toFlushAndYieldThrough(['Child One']); + expect(child1Ref.current).toBe(null); + expect(Scheduler).toHaveYielded([]); - // State update should trigger the ID to update, which changes the props - // of ChildWithID. This should cause ChildWithID to hydrate before Children - - expect(Scheduler).toFlushAndYieldThrough([ - 'Child with ID', - // Fallbacks are immediately committed in TestUtils version - // of act - // 'Child with ID', - // 'Child with ID', - 'Child One', - 'Child Two', - ]); - - expect(child1Ref.current).toBe(null); - expect(childWithIDRef.current).toEqual( - container.getElementsByTagName('span')[2], - ); + ReactTestUtils.act(() => { + _setShow(true); - expect(setShowRef.current).toEqual( - container.getElementsByTagName('span')[3], - ); + // State update should trigger the ID to update, which changes the props + // of ChildWithID. This should cause ChildWithID to hydrate before Children - expect(childWithIDRef.current.getAttribute('id')).toEqual( - setShowRef.current.getAttribute('aria-labelledby'), - ); - expect(childWithIDRef.current.getAttribute('id')).not.toEqual( - serverId, - ); - }); + expect(Scheduler).toFlushAndYieldThrough([ + 'Child with ID', + // Fallbacks are immediately committed in TestUtils version + // of act + // 'Child with ID', + // 'Child with ID', + 'Child One', + 'Child Two', + ]); - // Children hydrates after ChildWithID - expect(child1Ref.current).toBe( - container.getElementsByTagName('span')[0], + expect(child1Ref.current).toBe(null); + expect(childWithIDRef.current).toEqual( + container.getElementsByTagName('span')[2], ); - Scheduler.unstable_flushAll(); - - expect(Scheduler).toHaveYielded([]); - }); + expect(setShowRef.current).toEqual( + container.getElementsByTagName('span')[3], + ); - it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { - let suspend = true; - let resolve; - const promise = new Promise( - resolvePromise => (resolve = resolvePromise), + expect(childWithIDRef.current.getAttribute('id')).toEqual( + setShowRef.current.getAttribute('aria-labelledby'), ); + expect(childWithIDRef.current.getAttribute('id')).not.toEqual(serverId); + }); - function Child({text}) { - if (suspend) { - throw promise; - } else { - return text; - } - } + // Children hydrates after ChildWithID + expect(child1Ref.current).toBe(container.getElementsByTagName('span')[0]); - function RenderedChild() { - useEffect(() => { - Scheduler.unstable_yieldValue('Child did commit'); - }); - return null; - } + Scheduler.unstable_flushAll(); - function App() { - const id = useOpaqueIdentifier(); - useEffect(() => { - Scheduler.unstable_yieldValue('Did commit'); - }); - return ( -
-
Child One
- - -
- -
-
-
- ); - } + expect(Scheduler).toHaveYielded([]); + }); - const container = document.createElement('div'); - document.body.appendChild(container); + // @gate experimental + it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { + let suspend = true; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - container.innerHTML = ReactDOMServer.renderToString(); + function Child({text}) { + if (suspend) { + throw promise; + } else { + return text; + } + } - suspend = true; - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - await ReactTestUtils.act(async () => { - root.render(); - }); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); - expect(Scheduler).toFlushAndYield([]); - - const serverId = container.children[0].children[0].getAttribute('id'); - expect(container.children[0].children.length).toEqual(1); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - - await ReactTestUtils.act(async () => { - suspend = false; - resolve(); - await promise; + function RenderedChild() { + useEffect(() => { + Scheduler.unstable_yieldValue('Child did commit'); }); + return null; + } - expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); - expect(Scheduler).toFlushAndYield([]); - jest.runAllTimers(); - - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), + function App() { + const id = useOpaqueIdentifier(); + useEffect(() => { + Scheduler.unstable_yieldValue('Did commit'); + }); + return ( +
+
Child One
+ + +
+ +
+
+
); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toEqual(serverId); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); + } - it('useOpaqueIdentifier warn when there is a hydration error', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } + const container = document.createElement('div'); + document.body.appendChild(container); - const container = document.createElement('div'); - document.body.appendChild(container); + container.innerHTML = ReactDOMServer.renderToString(); - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'Warning: Expected server HTML to contain a matching
in
.', - ]); + suspend = true; + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + await ReactTestUtils.act(async () => { + root.render(); }); + jest.runAllTimers(); + expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); + expect(Scheduler).toFlushAndYield([]); - it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { - let suspend = true; + const serverId = container.children[0].children[0].getAttribute('id'); + expect(container.children[0].children.length).toEqual(1); + expect( + container.children[0].children[0].getAttribute('id'), + ).not.toBeNull(); - function Child({text}) { - if (suspend) { - throw new Promise(() => {}); - } else { - return text; - } - } + await ReactTestUtils.act(async () => { + suspend = false; + resolve(); + await promise; + }); - function RenderedChild() { - useEffect(() => { - Scheduler.unstable_yieldValue('Child did commit'); - }); - return null; - } + expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']); + expect(Scheduler).toFlushAndYield([]); + jest.runAllTimers(); - function App() { - const id = useOpaqueIdentifier(); - useEffect(() => { - Scheduler.unstable_yieldValue('Did commit'); - }); - return ( -
-
Child One
- - -
- -
-
-
- ); - } + expect(container.children[0].children.length).toEqual(2); + expect(container.children[0].children[0].getAttribute('id')).toEqual( + container.children[0].children[1].getAttribute('id'), + ); + expect(container.children[0].children[0].getAttribute('id')).not.toEqual( + serverId, + ); + expect( + container.children[0].children[0].getAttribute('id'), + ).not.toBeNull(); + }); - const container = document.createElement('div'); - document.body.appendChild(container); + // @gate experimental + it('useOpaqueIdentifier warn when there is a hydration error', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ; + } - container.innerHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + document.body.appendChild(container); - suspend = false; - const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - await ReactTestUtils.act(async () => { - root.render(); - }); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded([ - 'Child did commit', - 'Did commit', - 'Child did commit', - 'Did commit', - ]); - expect(Scheduler).toFlushAndYield([]); + // This is the wrong HTML string + container.innerHTML = ''; + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ + 'Warning: Expected server HTML to contain a matching
in
.', + ]); + }); - expect(container.children[0].children.length).toEqual(2); - expect(container.children[0].children[0].getAttribute('id')).toEqual( - container.children[0].children[1].getAttribute('id'), - ); - expect( - container.children[0].children[0].getAttribute('id'), - ).not.toBeNull(); - }); + // @gate experimental + it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => { + let suspend = true; - it('useOpaqueIdentifier warn when there is a hydration error', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; + function Child({text}) { + if (suspend) { + throw new Promise(() => {}); + } else { + return text; } + } - const container = document.createElement('div'); - document.body.appendChild(container); + function RenderedChild() { + useEffect(() => { + Scheduler.unstable_yieldValue('Child did commit'); + }); + return null; + } - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , + function App() { + const id = useOpaqueIdentifier(); + useEffect(() => { + Scheduler.unstable_yieldValue('Did commit'); + }); + return ( +
+
Child One
+ + +
+ +
+
+
); - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'Warning: Expected server HTML to contain a matching
in
.', - ]); - }); + } - it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } + const container = document.createElement('div'); + document.body.appendChild(container); - const container = document.createElement('div'); - document.body.appendChild(container); + container.innerHTML = ReactDOMServer.renderToString(); - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: Did not expect server HTML to contain a in
.', - ], - {withoutStack: 1}, - ); + suspend = false; + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + await ReactTestUtils.act(async () => { + root.render(); }); + jest.runAllTimers(); + expect(Scheduler).toHaveYielded([ + 'Child did commit', + 'Did commit', + 'Child did commit', + 'Did commit', + ]); + expect(Scheduler).toFlushAndYield([]); - it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } + expect(container.children[0].children.length).toEqual(2); + expect(container.children[0].children[0].getAttribute('id')).toEqual( + container.children[0].children[1].getAttribute('id'), + ); + expect( + container.children[0].children[0].getAttribute('id'), + ).not.toBeNull(); + }); - const container = document.createElement('div'); - document.body.appendChild(container); + // @gate experimental + it('useOpaqueIdentifier warn when there is a hydration error', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ; + } - // This is the wrong HTML string - container.innerHTML = ''; - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: Did not expect server HTML to contain a in
.', - ], - {withoutStack: 1}, - ); - }); + const container = document.createElement('div'); + document.body.appendChild(container); - it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ; - } + // This is the wrong HTML string + container.innerHTML = ''; + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ + 'Warning: Expected server HTML to contain a matching
in
.', + ]); + }); - const container = document.createElement('div'); - document.body.appendChild(container); + // @gate experimental + it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ; + } - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: Did not expect server HTML to contain a
in
.', - ], - {withoutStack: 1}, - ); - }); + const container = document.createElement('div'); + document.body.appendChild(container); + + // This is the wrong HTML string + container.innerHTML = ''; + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev( + [ + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', + 'Warning: Did not expect server HTML to contain a in
.', + ], + {withoutStack: 1}, + ); + }); - it('useOpaqueIdentifier warns if you try to use the result as a string', async () => { - function App() { - const id = useOpaqueIdentifier(); - return
; - } + // @gate experimental + it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ; + } - const container = document.createElement('div'); - document.body.appendChild(container); + const container = document.createElement('div'); + document.body.appendChild(container); + + // This is the wrong HTML string + container.innerHTML = ''; + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev( + [ + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', + 'Warning: Did not expect server HTML to contain a in
.', + ], + {withoutStack: 1}, + ); + }); - container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( - [ - 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', - 'Warning: Did not expect server HTML to contain a
in
.', - ], - {withoutStack: 1}, - ); - }); + // @gate experimental + it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ; + } - it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => { - function Child({appId}) { - return
; - } - function App() { - const id = useOpaqueIdentifier(); - return ( - - - - ); - } + const container = document.createElement('div'); + document.body.appendChild(container); + + container.innerHTML = ReactDOMServer.renderToString(); + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev( + [ + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', + 'Warning: Did not expect server HTML to contain a
in
.', + ], + {withoutStack: 1}, + ); + }); - const container = document.createElement('div'); - document.body.appendChild(container); + // @gate experimental + it('useOpaqueIdentifier warns if you try to use the result as a string', async () => { + function App() { + const id = useOpaqueIdentifier(); + return
; + } - container.innerHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + document.body.appendChild(container); + + container.innerHTML = ReactDOMServer.renderToString(); + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + expect(() => Scheduler.unstable_flushAll()).toErrorDev( + [ + 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.', + 'Warning: Did not expect server HTML to contain a
in
.', + ], + {withoutStack: 1}, + ); + }); - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , + // @gate experimental + it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => { + function Child({appId}) { + return
; + } + function App() { + const id = useOpaqueIdentifier(); + return ( + + + ); + } - if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } else { - // This error isn't surfaced to the user; only the warning is. - // The error is just the mechanism that restarts the render. - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ), - ).toErrorDev([ + const container = document.createElement('div'); + document.body.appendChild(container); + + container.innerHTML = ReactDOMServer.renderToString(); + + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + + if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } else { + // This error isn't surfaced to the user; only the warning is. + // The error is just the mechanism that restarts the render. + expect(() => + expect(() => Scheduler.unstable_flushAll()).toThrow( 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + 'Do not read the value directly.', - ]); - } - }); + ), + ).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } + }); - it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => { - function Child({appId}) { - return
; - } - function App() { - const [show] = useState(false); - const id = useOpaqueIdentifier(); - return ( - - {show &&
} - - - ); - } + // @gate experimental + it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => { + function Child({appId}) { + return
; + } + function App() { + const [show] = useState(false); + const id = useOpaqueIdentifier(); + return ( + + {show &&
} + + + ); + } - const container = document.createElement('div'); - document.body.appendChild(container); + const container = document.createElement('div'); + document.body.appendChild(container); - container.innerHTML = ReactDOMServer.renderToString(); + container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); - if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); - } else { - // This error isn't surfaced to the user; only the warning is. - // The error is just the mechanism that restarts the render. - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ), - ).toErrorDev([ + if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } else { + // This error isn't surfaced to the user; only the warning is. + // The error is just the mechanism that restarts the render. + expect(() => + expect(() => Scheduler.unstable_flushAll()).toThrow( 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + 'Do not read the value directly.', - ]); - } - }); - - it('useOpaqueIdentifier with two opaque identifiers on the same page', () => { - let _setShow; + ), + ).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } + }); - function App() { - const id1 = useOpaqueIdentifier(); - const id2 = useOpaqueIdentifier(); - const [show, setShow] = useState(true); - _setShow = setShow; + // @gate experimental + it('useOpaqueIdentifier with two opaque identifiers on the same page', () => { + let _setShow; - return ( -
- - {show ? ( - {'Child'} - ) : ( - {'Child'} - )} - - {'test'} -
- ); - } + function App() { + const id1 = useOpaqueIdentifier(); + const id2 = useOpaqueIdentifier(); + const [show, setShow] = useState(true); + _setShow = setShow; - const container = document.createElement('div'); - document.body.appendChild(container); + return ( +
+ + {show ? ( + {'Child'} + ) : ( + {'Child'} + )} + + {'test'} +
+ ); + } - container.innerHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + document.body.appendChild(container); - const serverID = container - .getElementsByTagName('span')[0] - .getAttribute('id'); - expect(serverID).not.toBeNull(); - expect( - container - .getElementsByTagName('span')[1] - .getAttribute('aria-labelledby'), - ).toEqual(serverID); + container.innerHTML = ReactDOMServer.renderToString(); - ReactDOM.unstable_createRoot(container, {hydrate: true}).render( - , - ); - jest.runAllTimers(); - expect(Scheduler).toHaveYielded([]); - expect(Scheduler).toFlushAndYield([]); + const serverID = container + .getElementsByTagName('span')[0] + .getAttribute('id'); + expect(serverID).not.toBeNull(); + expect( + container + .getElementsByTagName('span')[1] + .getAttribute('aria-labelledby'), + ).toEqual(serverID); - ReactTestUtils.act(() => { - _setShow(false); - }); + ReactDOM.unstable_createRoot(container, {hydrate: true}).render(); + jest.runAllTimers(); + expect(Scheduler).toHaveYielded([]); + expect(Scheduler).toFlushAndYield([]); - expect( - container - .getElementsByTagName('span')[1] - .getAttribute('aria-labelledby'), - ).toEqual(serverID); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toEqual(serverID); - expect( - container.getElementsByTagName('span')[0].getAttribute('id'), - ).not.toBeNull(); + ReactTestUtils.act(() => { + _setShow(false); }); + + expect( + container + .getElementsByTagName('span')[1] + .getAttribute('aria-labelledby'), + ).toEqual(serverID); + expect( + container.getElementsByTagName('span')[0].getAttribute('id'), + ).not.toEqual(serverID); + expect( + container.getElementsByTagName('span')[0].getAttribute('id'), + ).not.toBeNull(); }); - } + }); }); diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 30fddb8f94934..33fd350cb6e6f 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -144,9 +144,11 @@ module.exports = function(initModules) { async function renderIntoStream(reactElement, errorCount = 0) { return await expectErrors( () => - new Promise(resolve => { + new Promise((resolve, reject) => { const writable = new DrainWritable(); - ReactDOMServer.renderToNodeStream(reactElement).pipe(writable); + const s = ReactDOMServer.renderToNodeStream(reactElement); + s.on('error', e => reject(e)); + s.pipe(writable); writable.on('finish', () => resolve(writable.buffer)); }), errorCount,